mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-05 08:39:55 +03:00
Compare commits
588 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16117d329c | ||
|
|
9758872baf | ||
|
|
e90da18ada | ||
|
|
a08d80e1cc | ||
|
|
b711bc6816 | ||
|
|
247e6dba85 | ||
|
|
2b3d6e4e4a | ||
|
|
6b1980c4f3 | ||
|
|
9ba29770e1 | ||
|
|
3d375fae55 | ||
|
|
c99a50de2c | ||
|
|
1a32b25b5e | ||
|
|
481aa5b5b0 | ||
|
|
c943eb4d0d | ||
|
|
aca6de49b0 | ||
|
|
5fd04fa470 | ||
|
|
87339e4cd0 | ||
|
|
a9eb058dad | ||
|
|
61fad6a665 | ||
|
|
fa4bee2d98 | ||
|
|
ce63260fa6 | ||
|
|
a3557d5bb2 | ||
|
|
9ca22976c3 | ||
|
|
9e2934fe17 | ||
|
|
2259263214 | ||
|
|
762cf5f183 | ||
|
|
07175f2b3e | ||
|
|
6258175922 | ||
|
|
15736777a0 | ||
|
|
0c4ddf16a5 | ||
|
|
74a5e3113e | ||
|
|
df0a982433 | ||
|
|
212f924ffa | ||
|
|
09936566dd | ||
|
|
9469c04eab | ||
|
|
941bb73a68 | ||
|
|
8e652f5d8f | ||
|
|
eec1b21928 | ||
|
|
1baeb7bec9 | ||
|
|
61475ca0a3 | ||
|
|
244c5a3ebb | ||
|
|
39e7ac1c15 | ||
|
|
ab7f5def04 | ||
|
|
75915e8a94 | ||
|
|
9bde0ae4ea | ||
|
|
cd7e727f8c | ||
|
|
2329a5cedf | ||
|
|
d1a4ff9308 | ||
|
|
9a3bc27ef4 | ||
|
|
f8315fb9c4 | ||
|
|
bb62dee5a2 | ||
|
|
8acc188e16 | ||
|
|
874386ceab | ||
|
|
9dfbea8bf9 | ||
|
|
c1627a1468 | ||
|
|
576a59a693 | ||
|
|
fec6c65b78 | ||
|
|
f8c046d182 | ||
|
|
9ca2184f09 | ||
|
|
fd449582bd | ||
|
|
621142a46e | ||
|
|
0275d2ad58 | ||
|
|
41f56e659d | ||
|
|
fea5630ea4 | ||
|
|
e3f2bde26d | ||
|
|
756ee0b172 | ||
|
|
c81b63b56f | ||
|
|
70ee28ee13 | ||
|
|
1c9ecc3edd | ||
|
|
9bd5d6a422 | ||
|
|
1e41ccbc7a | ||
|
|
0a402e3c63 | ||
|
|
55759bd22a | ||
|
|
d8e1f52ddd | ||
|
|
baea92b206 | ||
|
|
cfc05c1b22 | ||
|
|
ebf78d49a8 | ||
|
|
4cb4c9e568 | ||
|
|
36f524a354 | ||
|
|
b60d2190ac | ||
|
|
2f8b8c580d | ||
|
|
5187d3fa78 | ||
|
|
9c07741972 | ||
|
|
8fcbe44d3e | ||
|
|
3966fb1df6 | ||
|
|
830614fb19 | ||
|
|
76ae5c7398 | ||
|
|
769935f99e | ||
|
|
6920d6eef1 | ||
|
|
b5cd3bff3c | ||
|
|
ac07cb41b6 | ||
|
|
e8fa58f201 | ||
|
|
1f6994b62c | ||
|
|
47d82a1ac2 | ||
|
|
703d579561 | ||
|
|
ed375bfaf7 | ||
|
|
3da8c01c1f | ||
|
|
295f520f21 | ||
|
|
fba7ae923d | ||
|
|
28a9bd514f | ||
|
|
666c86b108 | ||
|
|
194293664f | ||
|
|
039ee5d06c | ||
|
|
afc66b3c3d | ||
|
|
a04b31866d | ||
|
|
db3dde98ef | ||
|
|
0d6a6b5b63 | ||
|
|
4df3267521 | ||
|
|
d3e4a1a6f9 | ||
|
|
b023699f1b | ||
|
|
f338dbe3f8 | ||
|
|
ab07f7df6c | ||
|
|
a59d73de7b | ||
|
|
1ac7618bb1 | ||
|
|
2a069880cd | ||
|
|
5e5928a8a6 | ||
|
|
d6e87420c3 | ||
|
|
79cfd39fde | ||
|
|
e9831a7507 | ||
|
|
0c802d1f86 | ||
|
|
b7a96c6466 | ||
|
|
9126da6299 | ||
|
|
fea139d8e7 | ||
|
|
ac7a8a8e1e | ||
|
|
bbaa2f4cda | ||
|
|
9d61eecd81 | ||
|
|
21247e10d0 | ||
|
|
c1fc06ae34 | ||
|
|
a0eb3d1079 | ||
|
|
164aea3a3a | ||
|
|
ec83f83017 | ||
|
|
5cd08ab2f5 | ||
|
|
072f6b103e | ||
|
|
b4dcde252b | ||
|
|
0b2c3c1aa7 | ||
|
|
0dc9d0bed7 | ||
|
|
a2a2e37797 | ||
|
|
3d9819f97c | ||
|
|
b2b4a24d7c | ||
|
|
7557d6d619 | ||
|
|
57eeb9b0a3 | ||
|
|
813c7d5902 | ||
|
|
4b645a82c7 | ||
|
|
d599b77b6f | ||
|
|
f200b4183d | ||
|
|
33642c20ec | ||
|
|
26e93dc8c1 | ||
|
|
a4c9a8491b | ||
|
|
2704962277 | ||
|
|
6bcd89acf7 | ||
|
|
433cb9b3b2 | ||
|
|
7f43372dd4 | ||
|
|
b12e2ceada | ||
|
|
bc067e9ad4 | ||
|
|
245294fbc5 | ||
|
|
f38bc75ab4 | ||
|
|
3407900abb | ||
|
|
684c20c4ea | ||
|
|
6ef522df7e | ||
|
|
3b771f2976 | ||
|
|
afc56c12fe | ||
|
|
5eeed03dcd | ||
|
|
0d98b4ce5e | ||
|
|
ae2ec43a82 | ||
|
|
265ed34ffd | ||
|
|
711dcb4a48 | ||
|
|
67bc7007aa | ||
|
|
9e0c931573 | ||
|
|
70ee636d87 | ||
|
|
b35f6dbb03 | ||
|
|
2ea7e10923 | ||
|
|
e7f8188ee5 | ||
|
|
67d9e24d8f | ||
|
|
314d98abc3 | ||
|
|
b2dfc069c5 | ||
|
|
69c50b9b48 | ||
|
|
f101e4f010 | ||
|
|
005f0eb4fc | ||
|
|
7293ad7b24 | ||
|
|
c5b58d8fd2 | ||
|
|
4db2c274e2 | ||
|
|
cbff801aec | ||
|
|
d5d83da766 | ||
|
|
de6d8a811c | ||
|
|
a94844b6b7 | ||
|
|
968e7b8b72 | ||
|
|
5f461c9796 | ||
|
|
b2b8706d05 | ||
|
|
2d345fe454 | ||
|
|
7bec70d429 | ||
|
|
60710e7fb4 | ||
|
|
f10d3a8564 | ||
|
|
b020520366 | ||
|
|
52010e5820 | ||
|
|
75eca03b05 | ||
|
|
b8ebefd803 | ||
|
|
168c66815e | ||
|
|
26e703a1cd | ||
|
|
c8be214ee0 | ||
|
|
2060cdb931 | ||
|
|
574ee820a9 | ||
|
|
7d02f77e67 | ||
|
|
fd50efb503 | ||
|
|
9dbd7fa618 | ||
|
|
8dab31b87a | ||
|
|
e155c52256 | ||
|
|
c76e7c706c | ||
|
|
552943c033 | ||
|
|
4efe3b41da | ||
|
|
218376a41c | ||
|
|
e647ec22b1 | ||
|
|
38fe756725 | ||
|
|
652a67ad65 | ||
|
|
5bd9da6054 | ||
|
|
7c6fe8c4e2 | ||
|
|
689d1eb082 | ||
|
|
06d75e1804 | ||
|
|
0528e98d9c | ||
|
|
3903fda6ca | ||
|
|
441e46ebaa | ||
|
|
f99c8ff99a | ||
|
|
9558f84b97 | ||
|
|
2fd421b115 | ||
|
|
6ff440e677 | ||
|
|
0bda5554dd | ||
|
|
860d4d4be5 | ||
|
|
88f93f76dd | ||
|
|
e5fc6bf5fa | ||
|
|
8990a9817f | ||
|
|
9a97995f18 | ||
|
|
1a1e71cd60 | ||
|
|
34802ff8a6 | ||
|
|
0ff5aad9c0 | ||
|
|
03e5d61798 | ||
|
|
4f231d1bf0 | ||
|
|
1f4260f359 | ||
|
|
dc0bf8ad4e | ||
|
|
75981c2412 | ||
|
|
8b82753218 | ||
|
|
3368fe42d8 | ||
|
|
102e326e6a | ||
|
|
2b25bf6f3b | ||
|
|
f8ae4c335e | ||
|
|
5570e858e5 | ||
|
|
1859a4d356 | ||
|
|
ad4642c2c4 | ||
|
|
92108d710d | ||
|
|
c3ea0d333e | ||
|
|
f93280696d | ||
|
|
1787391b07 | ||
|
|
44347ee353 | ||
|
|
9e704fcae4 | ||
|
|
fdd816b17d | ||
|
|
82e2c523e6 | ||
|
|
a323b0d49c | ||
|
|
4c985aac7e | ||
|
|
87e18b8068 | ||
|
|
607a2c91fc | ||
|
|
d447355a61 | ||
|
|
8e2437498f | ||
|
|
9de85283cd | ||
|
|
b3d4c199ae | ||
|
|
fde970ba59 | ||
|
|
ec7be1b08b | ||
|
|
746a760a23 | ||
|
|
1a09d88891 | ||
|
|
46c01ecba2 | ||
|
|
544ece03a5 | ||
|
|
5fee7c4db1 | ||
|
|
8ed9f75d57 | ||
|
|
a15b179676 | ||
|
|
73844b9eeb | ||
|
|
dcde599709 | ||
|
|
0e0945ef84 | ||
|
|
ad125327c0 | ||
|
|
dfaf20dd83 | ||
|
|
786262db3b | ||
|
|
29a4110d8f | ||
|
|
9b639f715f | ||
|
|
46f3d78c8a | ||
|
|
1338ae2fc3 | ||
|
|
37813a223a | ||
|
|
b488e969bb | ||
|
|
1377296ef4 | ||
|
|
01cb22af37 | ||
|
|
331305333d | ||
|
|
0651eae7ec | ||
|
|
1552417598 | ||
|
|
4e71a5a47b | ||
|
|
a74a8ee483 | ||
|
|
7fa5405cb7 | ||
|
|
cc0ce7c630 | ||
|
|
070d4aeb6c | ||
|
|
668ce26269 | ||
|
|
6725ddcc41 | ||
|
|
bce941db3f | ||
|
|
4499ae84bb | ||
|
|
d4e790d3cf | ||
|
|
9b35aa42a2 | ||
|
|
7163997367 | ||
|
|
2385a6c29b | ||
|
|
36173eb47d | ||
|
|
f7645824d9 | ||
|
|
6d926048ec | ||
|
|
5335c973b4 | ||
|
|
bcafa73faf | ||
|
|
15c3e5c96e | ||
|
|
a5d5904969 | ||
|
|
e3eefba745 | ||
|
|
a90f564980 | ||
|
|
253132afdf | ||
|
|
eded8abded | ||
|
|
0abed1afe5 | ||
|
|
22077d4181 | ||
|
|
b0e849f413 | ||
|
|
af3c0e43a5 | ||
|
|
387047f262 | ||
|
|
4a2a539c08 | ||
|
|
4214fcd2fa | ||
|
|
b2b64fb853 | ||
|
|
a6128a1df1 | ||
|
|
598758b991 | ||
|
|
9926e23bc8 | ||
|
|
6638ee47d3 | ||
|
|
65899a3e91 | ||
|
|
86625a7642 | ||
|
|
ee495450cc | ||
|
|
d369d315a7 | ||
|
|
7c9937e924 | ||
|
|
410e967eb1 | ||
|
|
388f2f40dc | ||
|
|
33a2999a57 | ||
|
|
076693efc9 | ||
|
|
54d1fcde5b | ||
|
|
1c656d6556 | ||
|
|
2431ce9f86 | ||
|
|
5d3264bc63 | ||
|
|
d71f819f95 | ||
|
|
80f844139c | ||
|
|
9eecaea31a | ||
|
|
3ccfa0e7fc | ||
|
|
148350009c | ||
|
|
70991fc1e5 | ||
|
|
e5c4e0ac86 | ||
|
|
6669998c10 | ||
|
|
ee13509760 | ||
|
|
82d7bb1f32 | ||
|
|
492e2f173e | ||
|
|
cdfda508d8 | ||
|
|
da941e584f | ||
|
|
380f0f2042 | ||
|
|
33d4844f17 | ||
|
|
989de47f22 | ||
|
|
8f19231ed5 | ||
|
|
5c60f27a7d | ||
|
|
2d4034f3b7 | ||
|
|
56d58ad8e5 | ||
|
|
a4f6bc63f0 | ||
|
|
26da81a3b0 | ||
|
|
ec9410b510 | ||
|
|
a0035e4de2 | ||
|
|
e4e3b25c22 | ||
|
|
d8c5f72258 | ||
|
|
d67ad47b2c | ||
|
|
b08e49b59d | ||
|
|
d3dc73ca06 | ||
|
|
d5ea15e6dd | ||
|
|
5d45286646 | ||
|
|
dabf149411 | ||
|
|
ee5ded6e1e | ||
|
|
598b07b53d | ||
|
|
e211f31370 | ||
|
|
0bcf608e0b | ||
|
|
969ad8911c | ||
|
|
397db04428 | ||
|
|
581c382f65 | ||
|
|
f7f86ff821 | ||
|
|
212cd710aa | ||
|
|
33c44d3c0f | ||
|
|
0faa130cfd | ||
|
|
af76580b98 | ||
|
|
b526d172d6 | ||
|
|
cd6572b61a | ||
|
|
f2917fc462 | ||
|
|
7c8c4c2a05 | ||
|
|
3595ac2551 | ||
|
|
8453191dfb | ||
|
|
65796cfc7b | ||
|
|
bab27462ab | ||
|
|
241278226f | ||
|
|
7f9de2c8ab | ||
|
|
f91f33c236 | ||
|
|
3f0ef57d31 | ||
|
|
0eb90cb3b6 | ||
|
|
9fe158b78a | ||
|
|
b14222dabd | ||
|
|
a24f3d7d47 | ||
|
|
c9700e38e2 | ||
|
|
05316c90ba | ||
|
|
242dc21876 | ||
|
|
08c4b9ac7c | ||
|
|
f30f4579e9 | ||
|
|
573357a08c | ||
|
|
0775cd09a1 | ||
|
|
96075dee7b | ||
|
|
066adf3cea | ||
|
|
581881d0ca | ||
|
|
d2efc2f47f | ||
|
|
65874d7b96 | ||
|
|
ac9b8f405c | ||
|
|
286f9b0c7d | ||
|
|
c403d05755 | ||
|
|
57dc53ceff | ||
|
|
340d3f833b | ||
|
|
694a9459c1 | ||
|
|
8d1419a12e | ||
|
|
04f7a7d301 | ||
|
|
0fb1fc87c8 | ||
|
|
d3c7aada89 | ||
|
|
e639600ba5 | ||
|
|
d439fb459b | ||
|
|
9d8a176182 | ||
|
|
600055bc73 | ||
|
|
e691b401c8 | ||
|
|
672b15d36c | ||
|
|
ac80723058 | ||
|
|
ab468bac3c | ||
|
|
c10d2a1493 | ||
|
|
97bbf79ffd | ||
|
|
2af0021c2b | ||
|
|
0f2eaccb39 | ||
|
|
b251671e3f | ||
|
|
c4eed37d8e | ||
|
|
8b43b91057 | ||
|
|
91fe7f0bee | ||
|
|
5cfb7b8de4 | ||
|
|
6329a1842a | ||
|
|
a6c6c6e300 | ||
|
|
30458405ce | ||
|
|
91220239e5 | ||
|
|
7ee695d74a | ||
|
|
867fc8be64 | ||
|
|
89509b487a | ||
|
|
ac0b29fb6d | ||
|
|
673c74ddfc | ||
|
|
3b7d223b0c | ||
|
|
b662670efc | ||
|
|
771626b6ec | ||
|
|
f15cc5bdfa | ||
|
|
fff5bbcee4 | ||
|
|
42d8e9e5bd | ||
|
|
4cc73657a1 | ||
|
|
da10c50945 | ||
|
|
24523cf31d | ||
|
|
1d681e53e4 | ||
|
|
e0235fda8b | ||
|
|
9dc9724e15 | ||
|
|
f7b01ae53d | ||
|
|
d704e1dbba | ||
|
|
393f6047f2 | ||
|
|
d94fc5b694 | ||
|
|
f06770d91d | ||
|
|
ef2ff5e093 | ||
|
|
7caed3b0db | ||
|
|
11960d9d3a | ||
|
|
bbd8fff021 | ||
|
|
ec17bd8608 | ||
|
|
0dbb8babee | ||
|
|
b14e9fc619 | ||
|
|
63c6d3478d | ||
|
|
781f0e7887 | ||
|
|
23e014cb25 | ||
|
|
5b64358ef1 | ||
|
|
56df64063d | ||
|
|
3f81eba13b | ||
|
|
7973412c29 | ||
|
|
f83de5f834 | ||
|
|
f2ceba978a | ||
|
|
96c074bb56 | ||
|
|
45641d0754 | ||
|
|
4b1d08ba99 | ||
|
|
f8a299caee | ||
|
|
437dce7756 | ||
|
|
f8ad820281 | ||
|
|
757f16a3b5 | ||
|
|
632ecc668f | ||
|
|
92d393537c | ||
|
|
160fa99ba4 | ||
|
|
d2a5ab49ed | ||
|
|
43d9d2eba7 | ||
|
|
baa260a03d | ||
|
|
b157a9927a | ||
|
|
b246a67e8a | ||
|
|
2d958e88bf | ||
|
|
07c7d5af17 | ||
|
|
42976ca48c | ||
|
|
d05e85efa9 | ||
|
|
547e117760 | ||
|
|
50a5d3c546 | ||
|
|
3fd82200cc | ||
|
|
7215392784 | ||
|
|
c279c6e2af | ||
|
|
8a9a8dfae5 | ||
|
|
c44314def3 | ||
|
|
8b899a9cf0 | ||
|
|
0ebdfa4825 | ||
|
|
32a06f119b | ||
|
|
ec30864ce5 | ||
|
|
6bc72e157a | ||
|
|
9537e2ae95 | ||
|
|
c6404d8917 | ||
|
|
7113807f12 | ||
|
|
10418323ef | ||
|
|
a11d5245ec | ||
|
|
565033e0d4 | ||
|
|
c25ef18900 | ||
|
|
b0a63ba0cc | ||
|
|
7b6c88f17c | ||
|
|
361ba8b244 | ||
|
|
9baa96d41c | ||
|
|
e584b4926f | ||
|
|
bcd9c2044e | ||
|
|
d582e76fed | ||
|
|
991dd8a558 | ||
|
|
bc49784797 | ||
|
|
7f99903fdb | ||
|
|
97d011ac8e | ||
|
|
61596a8e21 | ||
|
|
44e337cef6 | ||
|
|
1bec3eaa1e | ||
|
|
5942d796b5 | ||
|
|
eec9c05518 | ||
|
|
246d1621f5 | ||
|
|
6c1e06bf86 | ||
|
|
3c1e165134 | ||
|
|
80d1c594cc | ||
|
|
947db95d16 | ||
|
|
5b9362ab0b | ||
|
|
f602b088ac | ||
|
|
4acf0c4ee0 | ||
|
|
be711215e8 | ||
|
|
7e3b404240 | ||
|
|
9d3f329bc9 | ||
|
|
bd00a03e7b | ||
|
|
be517de7dc | ||
|
|
23ab1f0c81 | ||
|
|
49621e7b15 | ||
|
|
1ab6f017c9 | ||
|
|
0b364fd72f | ||
|
|
e80ae76856 | ||
|
|
eebad3e2a0 | ||
|
|
db2af47286 | ||
|
|
7ad28aeab4 | ||
|
|
8d80e7311c | ||
|
|
7932069535 | ||
|
|
78564ec61d | ||
|
|
b80184cd93 | ||
|
|
1fa079b466 | ||
|
|
fcfb9470c9 | ||
|
|
c99653f0f2 | ||
|
|
5080b4996e | ||
|
|
1903422113 | ||
|
|
e86901ca20 | ||
|
|
bdfa61c8b2 | ||
|
|
4e8722661f | ||
|
|
6f9d2939e7 | ||
|
|
3234510d31 | ||
|
|
a40af08018 | ||
|
|
223a6b546c | ||
|
|
0d5da2d9d4 | ||
|
|
37337b8b1d | ||
|
|
ffcc058927 | ||
|
|
3a1cda5802 | ||
|
|
5c1015d6fc | ||
|
|
3385ec3f78 | ||
|
|
1b46c19849 | ||
|
|
75a4fc905b | ||
|
|
05666efda9 | ||
|
|
59367b3417 | ||
|
|
9a31b83b2a | ||
|
|
a81a56706e | ||
|
|
9dc1b35e82 | ||
|
|
2dc1180a41 | ||
|
|
ada7c83e96 | ||
|
|
ea287ebf86 | ||
|
|
043cdeafb3 | ||
|
|
8b36ca95a3 | ||
|
|
6e03078de3 |
@@ -3,6 +3,10 @@ APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_KEY=SomeRandomString
|
||||
|
||||
# The below url has to be set if using social auth options
|
||||
# or if you are not using BookStack at the root path of your domain.
|
||||
# APP_URL=http://bookstack.dev
|
||||
|
||||
# Database details
|
||||
DB_HOST=localhost
|
||||
DB_DATABASE=database_database
|
||||
@@ -42,8 +46,6 @@ GITHUB_APP_ID=false
|
||||
GITHUB_APP_SECRET=false
|
||||
GOOGLE_APP_ID=false
|
||||
GOOGLE_APP_SECRET=false
|
||||
# URL used for social login redirects, NO TRAILING SLASH
|
||||
APP_URL=http://bookstack.dev
|
||||
|
||||
# External services such as Gravatar
|
||||
DISABLE_EXTERNAL_SERVICES=false
|
||||
|
||||
21
.github/ISSUE_TEMPLATE.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
### For Feature Requests
|
||||
|
||||
Desired Feature:
|
||||
|
||||
### For Bug Reports
|
||||
|
||||
* BookStack Version *(Found in settings, Please don't put 'latest')*:
|
||||
* PHP Version:
|
||||
* MySQL Version:
|
||||
|
||||
##### Expected Behavior
|
||||
|
||||
|
||||
|
||||
##### Current Behavior
|
||||
|
||||
|
||||
|
||||
##### Steps to Reproduce
|
||||
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -8,6 +8,15 @@ Homestead.yaml
|
||||
/public/css/*.map
|
||||
/public/js/*.map
|
||||
/public/bower
|
||||
/public/build/
|
||||
/storage/images
|
||||
_ide_helper.php
|
||||
/storage/debugbar
|
||||
/storage/debugbar
|
||||
.phpstorm.meta.php
|
||||
yarn.lock
|
||||
/bin
|
||||
nbproject
|
||||
.buildpath
|
||||
.project
|
||||
.settings/org.eclipse.wst.common.project.facet.core.xml
|
||||
.settings/org.eclipse.php.core.prefs
|
||||
|
||||
28
.travis.yml
Normal file
28
.travis.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
dist: trusty
|
||||
sudo: false
|
||||
language: php
|
||||
php:
|
||||
- 7.0.7
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
before_script:
|
||||
- mysql -u root -e 'create database `bookstack-test`;'
|
||||
- mysql -u root -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
|
||||
- mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
||||
- mysql -u root -e "FLUSH PRIVILEGES;"
|
||||
- phpenv config-rm xdebug.ini
|
||||
- composer dump-autoload --no-interaction
|
||||
- composer install --prefer-dist --no-interaction
|
||||
- php artisan clear-compiled -n
|
||||
- php artisan optimize -n
|
||||
- php artisan migrate --force -n --database=mysql_testing
|
||||
- php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
|
||||
|
||||
after_failure:
|
||||
- cat storage/logs/laravel.log
|
||||
|
||||
script:
|
||||
- phpunit
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Dan Brown
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property string key
|
||||
* @property \User user
|
||||
@@ -28,7 +26,7 @@ class Activity extends Model
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User');
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +44,7 @@ class Activity extends Model
|
||||
* @return bool
|
||||
*/
|
||||
public function isSimilarTo($activityB) {
|
||||
return [$this->key, $this->entitiy_type, $this->entitiy_id] === [$activityB->key, $activityB->entitiy_type, $activityB->entitiy_id];
|
||||
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
36
app/Attachment.php
Normal file
36
app/Attachment.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class Attachment extends Ownable
|
||||
{
|
||||
protected $fillable = ['name', 'order'];
|
||||
|
||||
/**
|
||||
* Get the downloadable file name for this upload.
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function getFileName()
|
||||
{
|
||||
if (str_contains($this->name, '.')) return $this->name;
|
||||
return $this->name . '.' . $this->extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page this file was uploaded to.
|
||||
* @return Page
|
||||
*/
|
||||
public function page()
|
||||
{
|
||||
return $this->belongsTo(Page::class, 'uploaded_to');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url of this file.
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return baseUrl('/attachments/' . $this->id);
|
||||
}
|
||||
|
||||
}
|
||||
49
app/Book.php
49
app/Book.php
@@ -1,35 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class Book extends Entity
|
||||
{
|
||||
|
||||
protected $fillable = ['name', 'description'];
|
||||
|
||||
public function getUrl()
|
||||
/**
|
||||
* Get the url for this book.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
return '/books/' . $this->slug;
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||
}
|
||||
return baseUrl('/books/' . urlencode($this->slug));
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the edit url for this book.
|
||||
* @return string
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return $this->getUrl() . '/edit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages within this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function pages()
|
||||
{
|
||||
return $this->hasMany('BookStack\Page');
|
||||
return $this->hasMany(Page::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all chapters within this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function chapters()
|
||||
{
|
||||
return $this->hasMany('BookStack\Chapter');
|
||||
return $this->hasMany(Chapter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description;
|
||||
$description = $this->description;
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,25 +5,59 @@ class Chapter extends Entity
|
||||
{
|
||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||
|
||||
protected $with = ['book'];
|
||||
|
||||
/**
|
||||
* Get the book this chapter is within.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function book()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Book');
|
||||
return $this->belongsTo(Book::class);
|
||||
}
|
||||
|
||||
public function pages()
|
||||
/**
|
||||
* Get the pages that this chapter contains.
|
||||
* @param string $dir
|
||||
* @return mixed
|
||||
*/
|
||||
public function pages($dir = 'ASC')
|
||||
{
|
||||
return $this->hasMany('BookStack\Page')->orderBy('priority', 'ASC');
|
||||
return $this->hasMany(Page::class)->orderBy('priority', $dir);
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
/**
|
||||
* Get the url of this chapter.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
||||
return '/books/' . $bookSlug. '/chapter/' . $this->slug;
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||
}
|
||||
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this chapter's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description;
|
||||
$description = $this->description;
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
43
app/Comment.php
Normal file
43
app/Comment.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
class Comment extends Ownable
|
||||
{
|
||||
protected $fillable = ['text', 'html', 'parent_id'];
|
||||
protected $appends = ['created', 'updated'];
|
||||
|
||||
/**
|
||||
* Get the entity that this comment belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment has been updated since creation.
|
||||
* @return bool
|
||||
*/
|
||||
public function isUpdated()
|
||||
{
|
||||
return $this->updated_at->timestamp > $this->created_at->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get created date as a relative diff.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCreatedAttribute()
|
||||
{
|
||||
return $this->created_at->diffForHumans();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get updated date as a relative diff.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUpdatedAttribute()
|
||||
{
|
||||
return $this->updated_at->diffForHumans();
|
||||
}
|
||||
}
|
||||
47
app/Console/Commands/ClearActivity.php
Normal file
47
app/Console/Commands/ClearActivity.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Activity;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearActivity extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:clear-activity';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clear user activity from the system';
|
||||
|
||||
protected $activity;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param Activity $activity
|
||||
*/
|
||||
public function __construct(Activity $activity)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->activity->newQuery()->truncate();
|
||||
$this->comment('System activity cleared');
|
||||
}
|
||||
}
|
||||
50
app/Console/Commands/ClearRevisions.php
Normal file
50
app/Console/Commands/ClearRevisions.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\PageRevision;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearRevisions extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:clear-revisions
|
||||
{--a|all : Include active update drafts in deletion}
|
||||
';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clear page revisions';
|
||||
|
||||
protected $pageRevision;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param PageRevision $pageRevision
|
||||
*/
|
||||
public function __construct(PageRevision $pageRevision)
|
||||
{
|
||||
$this->pageRevision = $pageRevision;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$deleteTypes = $this->option('all') ? ['version', 'update_draft'] : ['version'];
|
||||
$this->pageRevision->newQuery()->whereIn('type', $deleteTypes)->delete();
|
||||
$this->comment('Revisions deleted');
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,21 @@ namespace BookStack\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ResetViews extends Command
|
||||
class ClearViews extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'views:reset';
|
||||
protected $signature = 'bookstack:clear-views';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Reset all view-counts for all entities.';
|
||||
protected $description = 'Clear all view-counts for all entities.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
@@ -37,5 +37,6 @@ class ResetViews extends Command
|
||||
public function handle()
|
||||
{
|
||||
\Views::resetAll();
|
||||
$this->comment('Views cleared');
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
|
||||
class Inspire extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'inspire';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Display an inspiring quote';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->comment(PHP_EOL.Inspiring::quote().PHP_EOL);
|
||||
}
|
||||
}
|
||||
60
app/Console/Commands/RegeneratePermissions.php
Normal file
60
app/Console/Commands/RegeneratePermissions.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\PermissionService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegeneratePermissions extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:regenerate-permissions {--database= : The database connection to use.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Regenerate all system permissions';
|
||||
|
||||
/**
|
||||
* The service to handle the permission system.
|
||||
*
|
||||
* @var PermissionService
|
||||
*/
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(PermissionService $permissionService)
|
||||
{
|
||||
$this->permissionService = $permissionService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = \DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
\DB::setDefaultConnection($this->option('database'));
|
||||
$this->permissionService->setConnection(\DB::connection($this->option('database')));
|
||||
}
|
||||
|
||||
$this->permissionService->buildJointPermissions();
|
||||
|
||||
\DB::setDefaultConnection($connection);
|
||||
$this->comment('Permissions regenerated');
|
||||
}
|
||||
}
|
||||
54
app/Console/Commands/RegenerateSearch.php
Normal file
54
app/Console/Commands/RegenerateSearch.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\SearchService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegenerateSearch extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:regenerate-search {--database= : The database connection to use.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Re-index all content for searching';
|
||||
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(SearchService $searchService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->searchService = $searchService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = \DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
\DB::setDefaultConnection($this->option('database'));
|
||||
$this->searchService->setConnection(\DB::connection($this->option('database')));
|
||||
}
|
||||
|
||||
$this->searchService->indexAllEntities();
|
||||
\DB::setDefaultConnection($connection);
|
||||
$this->comment('Search index regenerated');
|
||||
}
|
||||
}
|
||||
57
app/Console/Commands/UpgradeDatabaseEncoding.php
Normal file
57
app/Console/Commands/UpgradeDatabaseEncoding.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpgradeDatabaseEncoding extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:db-utf8mb4 {--database= : The database connection to use.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate SQL commands to upgrade the database to UTF8mb4';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
DB::setDefaultConnection($this->option('database'));
|
||||
}
|
||||
|
||||
$database = DB::getDatabaseName();
|
||||
$tables = DB::select('SHOW TABLES');
|
||||
$this->line('ALTER DATABASE `'.$database.'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
|
||||
$this->line('USE `'.$database.'`;');
|
||||
$key = 'Tables_in_' . $database;
|
||||
foreach ($tables as $table) {
|
||||
$tableName = $table->$key;
|
||||
$this->line('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
|
||||
}
|
||||
|
||||
DB::setDefaultConnection($connection);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console;
|
||||
<?php namespace BookStack\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
@@ -13,8 +11,12 @@ class Kernel extends ConsoleKernel
|
||||
* @var array
|
||||
*/
|
||||
protected $commands = [
|
||||
\BookStack\Console\Commands\Inspire::class,
|
||||
\BookStack\Console\Commands\ResetViews::class,
|
||||
Commands\ClearViews::class,
|
||||
Commands\ClearActivity::class,
|
||||
Commands\ClearRevisions::class,
|
||||
Commands\RegeneratePermissions::class,
|
||||
Commands\RegenerateSearch::class,
|
||||
Commands\UpgradeDatabaseEncoding::class
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -25,7 +27,6 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->command('inspire')
|
||||
->hourly();
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EmailConfirmation extends Model
|
||||
{
|
||||
protected $fillable = ['user_id', 'token'];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User');
|
||||
}
|
||||
}
|
||||
141
app/Entity.php
141
app/Entity.php
@@ -1,9 +1,13 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
abstract class Entity extends Ownable
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
class Entity extends Ownable
|
||||
{
|
||||
|
||||
public $textField = 'description';
|
||||
|
||||
/**
|
||||
* Compares this entity to another given entity.
|
||||
* Matches by comparing class and id.
|
||||
@@ -43,7 +47,7 @@ abstract class Entity extends Ownable
|
||||
*/
|
||||
public function activity()
|
||||
{
|
||||
return $this->morphMany('BookStack\Activity', 'entity')->orderBy('created_at', 'desc');
|
||||
return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,15 +55,44 @@ abstract class Entity extends Ownable
|
||||
*/
|
||||
public function views()
|
||||
{
|
||||
return $this->morphMany('BookStack\View', 'viewable');
|
||||
return $this->morphMany(View::class, 'viewable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Tag models that have been user assigned to this entity.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comments for an entity
|
||||
* @param bool $orderByCreated
|
||||
* @return MorphMany
|
||||
*/
|
||||
public function comments($orderByCreated = true)
|
||||
{
|
||||
$query = $this->morphMany(Comment::class, 'entity');
|
||||
return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related search terms.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function searchTerms()
|
||||
{
|
||||
return $this->morphMany(SearchTerm::class, 'entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this entities restrictions.
|
||||
*/
|
||||
public function restrictions()
|
||||
public function permissions()
|
||||
{
|
||||
return $this->morphMany('BookStack\Restriction', 'restrictable');
|
||||
return $this->morphMany(EntityPermission::class, 'restrictable');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +103,17 @@ abstract class Entity extends Ownable
|
||||
*/
|
||||
public function hasRestriction($role_id, $action)
|
||||
{
|
||||
return $this->restrictions->where('role_id', $role_id)->where('action', $action)->count() > 0;
|
||||
return $this->permissions()->where('role_id', '=', $role_id)
|
||||
->where('action', '=', $action)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity jointPermissions this is connected to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function jointPermissions()
|
||||
{
|
||||
return $this->morphMany(JointPermission::class, 'entity');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +124,32 @@ abstract class Entity extends Ownable
|
||||
*/
|
||||
public static function isA($type)
|
||||
{
|
||||
return static::getClassName() === strtolower($type);
|
||||
return static::getType() === strtolower($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entity type.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getType()
|
||||
{
|
||||
return strtolower(static::getClassName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of an entity of the given type.
|
||||
* @param $type
|
||||
* @return Entity
|
||||
*/
|
||||
public static function getEntityInstance($type)
|
||||
{
|
||||
$types = ['Page', 'Book', 'Chapter'];
|
||||
$className = str_replace([' ', '-', '_'], '', ucwords($type));
|
||||
if (!in_array($className, $types)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return app('BookStack\\' . $className);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,60 +164,25 @@ abstract class Entity extends Ownable
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a full-text search on this entity.
|
||||
* @param string[] $fieldsToSearch
|
||||
* @param string[] $terms
|
||||
* @param string[] array $wheres
|
||||
* Get the body text of this entity.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
||||
public function getText()
|
||||
{
|
||||
$exactTerms = [];
|
||||
foreach ($terms as $key => $term) {
|
||||
$term = htmlentities($term, ENT_QUOTES);
|
||||
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
||||
if (preg_match('/\s/', $term)) {
|
||||
$exactTerms[] = '%' . $term . '%';
|
||||
$term = '"' . $term . '"';
|
||||
} else {
|
||||
$term = '' . $term . '*';
|
||||
}
|
||||
if ($term !== '*') $terms[$key] = $term;
|
||||
}
|
||||
$termString = implode(' ', $terms);
|
||||
$fields = implode(',', $fieldsToSearch);
|
||||
$search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
|
||||
|
||||
// Ensure at least one exact term matches if in search
|
||||
if (count($exactTerms) > 0) {
|
||||
$search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
|
||||
foreach ($exactTerms as $exactTerm) {
|
||||
foreach ($fieldsToSearch as $field) {
|
||||
$query->orWhere($field, 'like', $exactTerm);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add additional where terms
|
||||
foreach ($wheres as $whereTerm) {
|
||||
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
|
||||
}
|
||||
// Load in relations
|
||||
if (static::isA('page')) {
|
||||
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
|
||||
} else if (static::isA('chapter')) {
|
||||
$search = $search->with('book');
|
||||
}
|
||||
|
||||
return $search->orderBy('title_relevance', 'desc');
|
||||
return $this->{$this->textField};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this item.
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getUrl();
|
||||
public function entityRawQuery(){return '';}
|
||||
|
||||
/**
|
||||
* Get the url of this entity
|
||||
* @param $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path){return '/';}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Restriction extends Model
|
||||
class EntityPermission extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['role_id', 'action'];
|
||||
@@ -16,6 +13,6 @@ class Restriction extends Model
|
||||
*/
|
||||
public function restrictable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
return $this->morphTo('restrictable');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Events;
|
||||
|
||||
abstract class Event
|
||||
{
|
||||
//
|
||||
}
|
||||
4
app/Exceptions/FileUploadException.php
Normal file
4
app/Exceptions/FileUploadException.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class FileUploadException extends PrettyException {}
|
||||
@@ -3,9 +3,9 @@
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Validation\ValidationException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use PhpSpec\Exception\Example\ErrorException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
@@ -47,19 +47,60 @@ class Handler extends ExceptionHandler
|
||||
{
|
||||
// Handle notify exceptions which will redirect to the
|
||||
// specified location then show a notification message.
|
||||
if ($e instanceof NotifyException) {
|
||||
\Session::flash('error', $e->message);
|
||||
return response()->redirectTo($e->redirectLocation);
|
||||
if ($this->isExceptionType($e, NotifyException::class)) {
|
||||
session()->flash('error', $this->getOriginalMessage($e));
|
||||
return redirect($e->redirectLocation);
|
||||
}
|
||||
|
||||
// Handle pretty exceptions which will show a friendly application-fitting page
|
||||
// Which will include the basic message to point the user roughly to the cause.
|
||||
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
|
||||
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
|
||||
if ($this->isExceptionType($e, PrettyException::class) && !config('app.debug')) {
|
||||
$message = $this->getOriginalMessage($e);
|
||||
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
|
||||
return response()->view('errors/' . $code, ['message' => $message], $code);
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the exception chain to compare against the original exception type.
|
||||
* @param Exception $e
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
protected function isExceptionType(Exception $e, $type) {
|
||||
do {
|
||||
if (is_a($e, $type)) return true;
|
||||
} while ($e = $e->getPrevious());
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get original exception message.
|
||||
* @param Exception $e
|
||||
* @return string
|
||||
*/
|
||||
protected function getOriginalMessage(Exception $e) {
|
||||
do {
|
||||
$message = $e->getMessage();
|
||||
} while ($e = $e->getPrevious());
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an authentication exception into an unauthenticated response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Auth\AuthenticationException $exception
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
protected function unauthenticated($request, AuthenticationException $exception)
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['error' => 'Unauthenticated.'], 401);
|
||||
}
|
||||
|
||||
return redirect()->guest('login');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PrettyException extends Exception {}
|
||||
class PrettyException extends \Exception {}
|
||||
215
app/Http/Controllers/AttachmentController.php
Normal file
215
app/Http/Controllers/AttachmentController.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
use BookStack\Attachment;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Services\AttachmentService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AttachmentController extends Controller
|
||||
{
|
||||
protected $attachmentService;
|
||||
protected $attachment;
|
||||
protected $entityRepo;
|
||||
|
||||
/**
|
||||
* AttachmentController constructor.
|
||||
* @param AttachmentService $attachmentService
|
||||
* @param Attachment $attachment
|
||||
* @param EntityRepo $entityRepo
|
||||
*/
|
||||
public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo)
|
||||
{
|
||||
$this->attachmentService = $attachmentService;
|
||||
$this->attachment = $attachment;
|
||||
$this->entityRepo = $entityRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Endpoint at which attachments are uploaded to.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function upload(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||
'file' => 'required|file'
|
||||
]);
|
||||
|
||||
$pageId = $request->get('uploaded_to');
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
|
||||
$this->checkPermission('attachment-create-all');
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$uploadedFile = $request->file('file');
|
||||
|
||||
try {
|
||||
$attachment = $this->attachmentService->saveNewUpload($uploadedFile, $pageId);
|
||||
} catch (FileUploadException $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return response()->json($attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an uploaded attachment.
|
||||
* @param int $attachmentId
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function uploadUpdate($attachmentId, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||
'file' => 'required|file'
|
||||
]);
|
||||
|
||||
$pageId = $request->get('uploaded_to');
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||
|
||||
if (intval($pageId) !== intval($attachment->uploaded_to)) {
|
||||
return $this->jsonError(trans('errors.attachment_page_mismatch'));
|
||||
}
|
||||
|
||||
$uploadedFile = $request->file('file');
|
||||
|
||||
try {
|
||||
$attachment = $this->attachmentService->saveUpdatedUpload($uploadedFile, $attachment);
|
||||
} catch (FileUploadException $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return response()->json($attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the details of an existing file.
|
||||
* @param $attachmentId
|
||||
* @param Request $request
|
||||
* @return Attachment|mixed
|
||||
*/
|
||||
public function update($attachmentId, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||
'name' => 'required|string|min:1|max:255',
|
||||
'link' => 'url|min:1|max:255'
|
||||
]);
|
||||
|
||||
$pageId = $request->get('uploaded_to');
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||
|
||||
if (intval($pageId) !== intval($attachment->uploaded_to)) {
|
||||
return $this->jsonError(trans('errors.attachment_page_mismatch'));
|
||||
}
|
||||
|
||||
$attachment = $this->attachmentService->updateFile($attachment, $request->all());
|
||||
return response()->json($attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a link to a page.
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function attachLink(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||
'name' => 'required|string|min:1|max:255',
|
||||
'link' => 'required|url|min:1|max:255'
|
||||
]);
|
||||
|
||||
$pageId = $request->get('uploaded_to');
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
|
||||
$this->checkPermission('attachment-create-all');
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$attachmentName = $request->get('name');
|
||||
$link = $request->get('link');
|
||||
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
|
||||
|
||||
return response()->json($attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for a specific page.
|
||||
* @param $pageId
|
||||
* @return mixed
|
||||
*/
|
||||
public function listForPage($pageId)
|
||||
{
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
return response()->json($page->attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the attachment sorting.
|
||||
* @param $pageId
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function sortForPage($pageId, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'files' => 'required|array',
|
||||
'files.*.id' => 'required|integer',
|
||||
]);
|
||||
$page = $this->entityRepo->getById('page', $pageId);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$attachments = $request->get('files');
|
||||
$this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
|
||||
return response()->json(['message' => trans('entities.attachments_order_updated')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an attachment from storage.
|
||||
* @param $attachmentId
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function get($attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
$page = $this->entityRepo->getById('page', $attachment->uploaded_to);
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
if ($attachment->external) {
|
||||
return redirect($attachment->path);
|
||||
}
|
||||
|
||||
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
|
||||
return response($attachmentContents, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific attachment in the system.
|
||||
* @param $attachmentId
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete($attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
$this->checkOwnablePermission('attachment-delete', $attachment);
|
||||
$this->attachmentService->deleteFile($attachment);
|
||||
return response()->json(['message' => trans('entities.attachments_deleted')]);
|
||||
}
|
||||
}
|
||||
68
app/Http/Controllers/Auth/ForgotPasswordController.php
Normal file
68
app/Http/Controllers/Auth/ForgotPasswordController.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
||||
use Illuminate\Http\Request;
|
||||
use Password;
|
||||
|
||||
class ForgotPasswordController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller is responsible for handling password reset emails and
|
||||
| includes a trait which assists in sending these notifications from
|
||||
| your application to your users. Feel free to explore this trait.
|
||||
|
|
||||
*/
|
||||
|
||||
use SendsPasswordResetEmails;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send a reset link to the given user.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function sendResetLinkEmail(Request $request)
|
||||
{
|
||||
$this->validate($request, ['email' => 'required|email']);
|
||||
|
||||
// We will send the password reset link to this user. Once we have attempted
|
||||
// to send the link, we will examine the response then see the message we
|
||||
// need to show to the user. Finally, we'll send out a proper response.
|
||||
$response = $this->broker()->sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
if ($response === Password::RESET_LINK_SENT) {
|
||||
$message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
|
||||
session()->flash('success', $message);
|
||||
return back()->with('status', trans($response));
|
||||
}
|
||||
|
||||
// If an error was returned by the password broker, we will get this message
|
||||
// translated so we can notify a user of the problem. We'll redirect back
|
||||
// to where the users came from so they can attempt this process again.
|
||||
return back()->withErrors(
|
||||
['email' => trans($response)]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
124
app/Http/Controllers/Auth/LoginController.php
Normal file
124
app/Http/Controllers/Auth/LoginController.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Exceptions\AuthException;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Login Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller handles authenticating users for the application and
|
||||
| redirecting them to your home screen. The controller uses a trait
|
||||
| to conveniently provide its functionality to your applications.
|
||||
|
|
||||
*/
|
||||
|
||||
use AuthenticatesUsers;
|
||||
|
||||
/**
|
||||
* Where to redirect users after login.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/';
|
||||
|
||||
protected $redirectPath = '/';
|
||||
protected $redirectAfterLogout = '/login';
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
|
||||
{
|
||||
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->redirectPath = baseUrl('/');
|
||||
$this->redirectAfterLogout = baseUrl('/login');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function username()
|
||||
{
|
||||
return config('auth.method') === 'standard' ? 'email' : 'username';
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the action when a user is authenticated.
|
||||
* If the user authenticated but does not exist in the user table we create them.
|
||||
* @param Request $request
|
||||
* @param Authenticatable $user
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws AuthException
|
||||
*/
|
||||
protected function authenticated(Request $request, Authenticatable $user)
|
||||
{
|
||||
// Explicitly log them out for now if they do no exist.
|
||||
if (!$user->exists) auth()->logout($user);
|
||||
|
||||
if (!$user->exists && $user->email === null && !$request->has('email')) {
|
||||
$request->flash();
|
||||
session()->flash('request-email', true);
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
if (!$user->exists && $user->email === null && $request->has('email')) {
|
||||
$user->email = $request->get('email');
|
||||
}
|
||||
|
||||
if (!$user->exists) {
|
||||
|
||||
// Check for users with same email already
|
||||
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
||||
if ($alreadyUser) {
|
||||
throw new AuthException(trans('errors.error_user_exists_different_creds', ['email' => $user->email]));
|
||||
}
|
||||
|
||||
$user->save();
|
||||
$this->userRepo->attachDefaultRole($user);
|
||||
auth()->login($user);
|
||||
}
|
||||
|
||||
$path = session()->pull('url.intended', '/');
|
||||
$path = baseUrl($path, true);
|
||||
return redirect($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the application login form.
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function getLogin()
|
||||
{
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
$authMethod = config('auth.method');
|
||||
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the relevant social site.
|
||||
* @param $socialDriver
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
public function getSocialLogin($socialDriver)
|
||||
{
|
||||
session()->put('social-callback', 'login');
|
||||
return $this->socialAuthService->startLogIn($socialDriver);
|
||||
}
|
||||
}
|
||||
@@ -2,63 +2,69 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Exceptions\AuthException;
|
||||
use BookStack\Exceptions\PrettyException;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\EmailConfirmationService;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use BookStack\SocialAccount;
|
||||
use BookStack\User;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Validator;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ThrottlesLogins;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
|
||||
class AuthController extends Controller
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Registration & Login Controller
|
||||
| Register Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller handles the registration of new users, as well as the
|
||||
| authentication of existing users. By default, this controller uses
|
||||
| a simple trait to add these behaviors. Why don't you explore it?
|
||||
| This controller handles the registration of new users as well as their
|
||||
| validation and creation. By default this controller uses a trait to
|
||||
| provide this functionality without requiring any additional code.
|
||||
|
|
||||
*/
|
||||
|
||||
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
|
||||
|
||||
protected $redirectPath = '/';
|
||||
protected $redirectAfterLogout = '/login';
|
||||
protected $username = 'email';
|
||||
|
||||
use RegistersUsers;
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $emailConfirmationService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new authentication controller instance.
|
||||
* Where to redirect users after login / registration.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/';
|
||||
protected $redirectPath = '/';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* @param EmailConfirmationService $emailConfirmationService
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
||||
{
|
||||
$this->middleware('guest', ['only' => ['getLogin', 'postLogin', 'getRegister', 'postRegister']]);
|
||||
$this->middleware('guest')->except(['socialCallback', 'detachSocialAccount']);
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->emailConfirmationService = $emailConfirmationService;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->username = config('auth.method') === 'standard' ? 'email' : 'username';
|
||||
$this->redirectTo = baseUrl('/');
|
||||
$this->redirectPath = baseUrl('/');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a validator for an incoming registration request.
|
||||
*
|
||||
* @param array $data
|
||||
* @return \Illuminate\Contracts\Validation\Validator
|
||||
*/
|
||||
@@ -71,16 +77,20 @@ class AuthController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not registrations are allowed in the app settings.
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function checkRegistrationAllowed()
|
||||
{
|
||||
if (!setting('registration-enabled')) {
|
||||
throw new UserRegistrationException('Registrations are currently disabled.', '/login');
|
||||
throw new UserRegistrationException(trans('auth.registrations_disabled'), '/login');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the application registration form.
|
||||
* @return \Illuminate\Http\Response
|
||||
* @return Response
|
||||
*/
|
||||
public function getRegister()
|
||||
{
|
||||
@@ -91,9 +101,10 @@ class AuthController extends Controller
|
||||
|
||||
/**
|
||||
* Handle a registration request for the application.
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
* @param Request|\Illuminate\Http\Request $request
|
||||
* @return Response
|
||||
* @throws UserRegistrationException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function postRegister(Request $request)
|
||||
{
|
||||
@@ -110,64 +121,18 @@ class AuthController extends Controller
|
||||
return $this->registerUser($userData);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overrides the action when a user is authenticated.
|
||||
* If the user authenticated but does not exist in the user table we create them.
|
||||
* @param Request $request
|
||||
* @param Authenticatable $user
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws AuthException
|
||||
* Create a new user instance after a valid registration.
|
||||
* @param array $data
|
||||
* @return User
|
||||
*/
|
||||
protected function authenticated(Request $request, Authenticatable $user)
|
||||
protected function create(array $data)
|
||||
{
|
||||
// Explicitly log them out for now if they do no exist.
|
||||
if (!$user->exists) auth()->logout($user);
|
||||
|
||||
if (!$user->exists && $user->email === null && !$request->has('email')) {
|
||||
$request->flash();
|
||||
session()->flash('request-email', true);
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
if (!$user->exists && $user->email === null && $request->has('email')) {
|
||||
$user->email = $request->get('email');
|
||||
}
|
||||
|
||||
if (!$user->exists) {
|
||||
|
||||
// Check for users with same email already
|
||||
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
||||
if ($alreadyUser) {
|
||||
throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
|
||||
}
|
||||
|
||||
$user->save();
|
||||
$this->userRepo->attachDefaultRole($user);
|
||||
auth()->login($user);
|
||||
}
|
||||
|
||||
return redirect()->intended($this->redirectPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user after a registration callback.
|
||||
* @param $socialDriver
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function socialRegisterCallback($socialDriver)
|
||||
{
|
||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
|
||||
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
|
||||
|
||||
// Create an array of the user data to create a new user instance
|
||||
$userData = [
|
||||
'name' => $socialUser->getName(),
|
||||
'email' => $socialUser->getEmail(),
|
||||
'password' => str_random(30)
|
||||
];
|
||||
return $this->registerUser($userData, $socialAccount);
|
||||
return User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password']),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,7 +141,7 @@ class AuthController extends Controller
|
||||
* @param bool|false|SocialAccount $socialAccount
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
* @throws \BookStack\Exceptions\ConfirmationEmailException
|
||||
* @throws ConfirmationEmailException
|
||||
*/
|
||||
protected function registerUser(array $userData, $socialAccount = false)
|
||||
{
|
||||
@@ -184,7 +149,7 @@ class AuthController extends Controller
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
|
||||
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
|
||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||
throw new UserRegistrationException('That email domain does not have access to this application', '/register');
|
||||
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,12 +160,18 @@ class AuthController extends Controller
|
||||
|
||||
if (setting('registration-confirmation') || setting('registration-restrict')) {
|
||||
$newUser->save();
|
||||
$this->emailConfirmationService->sendConfirmation($newUser);
|
||||
|
||||
try {
|
||||
$this->emailConfirmationService->sendConfirmation($newUser);
|
||||
} catch (Exception $e) {
|
||||
session()->flash('error', trans('auth.email_confirm_send_error'));
|
||||
}
|
||||
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
auth()->login($newUser);
|
||||
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
|
||||
session()->flash('success', trans('auth.register_success'));
|
||||
return redirect($this->redirectPath());
|
||||
}
|
||||
|
||||
@@ -213,18 +184,6 @@ class AuthController extends Controller
|
||||
return view('auth/register-confirm');
|
||||
}
|
||||
|
||||
/**
|
||||
* View the confirmation email as a standard web page.
|
||||
* @param $token
|
||||
* @return \Illuminate\View\View
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function viewConfirmEmail($token)
|
||||
{
|
||||
$confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
|
||||
return view('emails/email-confirmation', ['token' => $confirmation->token]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms an email via a token and logs the user into the system.
|
||||
* @param $token
|
||||
@@ -237,8 +196,8 @@ class AuthController extends Controller
|
||||
$user = $confirmation->user;
|
||||
$user->email_confirmed = true;
|
||||
$user->save();
|
||||
auth()->login($confirmation->user);
|
||||
session()->flash('success', 'Your email has been confirmed!');
|
||||
auth()->login($user);
|
||||
session()->flash('success', trans('auth.email_confirm_success'));
|
||||
$this->emailConfirmationService->deleteConfirmationsByUser($user);
|
||||
return redirect($this->redirectPath);
|
||||
}
|
||||
@@ -264,33 +223,18 @@ class AuthController extends Controller
|
||||
'email' => 'required|email|exists:users,email'
|
||||
]);
|
||||
$user = $this->userRepo->getByEmail($request->get('email'));
|
||||
$this->emailConfirmationService->sendConfirmation($user);
|
||||
session()->flash('success', 'Confirmation email resent, Please check your inbox.');
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the application login form.
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function getLogin()
|
||||
{
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
$authMethod = config('auth.method');
|
||||
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the relevant social site.
|
||||
* @param $socialDriver
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
public function getSocialLogin($socialDriver)
|
||||
{
|
||||
session()->put('social-callback', 'login');
|
||||
return $this->socialAuthService->startLogIn($socialDriver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the social site for authentication intended to register.
|
||||
* @param $socialDriver
|
||||
@@ -311,16 +255,13 @@ class AuthController extends Controller
|
||||
*/
|
||||
public function socialCallback($socialDriver)
|
||||
{
|
||||
if (session()->has('social-callback')) {
|
||||
$action = session()->pull('social-callback');
|
||||
if ($action == 'login') {
|
||||
return $this->socialAuthService->handleLoginCallback($socialDriver);
|
||||
} elseif ($action == 'register') {
|
||||
return $this->socialRegisterCallback($socialDriver);
|
||||
}
|
||||
} else {
|
||||
throw new SocialSignInException('No action defined', '/login');
|
||||
if (!session()->has('social-callback')) {
|
||||
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
|
||||
}
|
||||
|
||||
$action = session()->pull('social-callback');
|
||||
if ($action == 'login') return $this->socialAuthService->handleLoginCallback($socialDriver);
|
||||
if ($action == 'register') return $this->socialRegisterCallback($socialDriver);
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
@@ -334,4 +275,24 @@ class AuthController extends Controller
|
||||
return $this->socialAuthService->detachSocialAccount($socialDriver);
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Register a new user after a registration callback.
|
||||
* @param $socialDriver
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function socialRegisterCallback($socialDriver)
|
||||
{
|
||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
|
||||
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
|
||||
|
||||
// Create an array of the user data to create a new user instance
|
||||
$userData = [
|
||||
'name' => $socialUser->getName(),
|
||||
'email' => $socialUser->getEmail(),
|
||||
'password' => str_random(30)
|
||||
];
|
||||
return $this->registerUser($userData, $socialAccount);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace BookStack\Http\Controllers\Auth;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
|
||||
class PasswordController extends Controller
|
||||
class ResetPasswordController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -23,10 +23,27 @@ class PasswordController extends Controller
|
||||
protected $redirectTo = '/';
|
||||
|
||||
/**
|
||||
* Create a new password controller instance.
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response for a successful password reset.
|
||||
*
|
||||
* @param string $response
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
protected function sendResetResponse($response)
|
||||
{
|
||||
$message = trans('auth.reset_password_success');
|
||||
session()->flash('success', $message);
|
||||
return redirect($this->redirectPath())
|
||||
->with('status', trans($response));
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,32 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use BookStack\Repos\PageRepo;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
||||
class BookController extends Controller
|
||||
{
|
||||
|
||||
protected $bookRepo;
|
||||
protected $pageRepo;
|
||||
protected $chapterRepo;
|
||||
protected $entityRepo;
|
||||
protected $userRepo;
|
||||
protected $exportService;
|
||||
|
||||
/**
|
||||
* BookController constructor.
|
||||
* @param BookRepo $bookRepo
|
||||
* @param PageRepo $pageRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param UserRepo $userRepo
|
||||
* @param ExportService $exportService
|
||||
*/
|
||||
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
|
||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->exportService = $exportService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -40,11 +36,17 @@ class BookController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$books = $this->bookRepo->getAllPaginated(10);
|
||||
$recents = $this->signedIn ? $this->bookRepo->getRecentlyViewed(4, 0) : false;
|
||||
$popular = $this->bookRepo->getPopular(4, 0);
|
||||
$books = $this->entityRepo->getAllPaginated('book', 20);
|
||||
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
|
||||
$popular = $this->entityRepo->getPopular('book', 4, 0);
|
||||
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
|
||||
$this->setPageTitle('Books');
|
||||
return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]);
|
||||
return view('books/index', [
|
||||
'books' => $books,
|
||||
'recents' => $recents,
|
||||
'popular' => $popular,
|
||||
'new' => $new
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +56,7 @@ class BookController extends Controller
|
||||
public function create()
|
||||
{
|
||||
$this->checkPermission('book-create-all');
|
||||
$this->setPageTitle('Create New Book');
|
||||
$this->setPageTitle(trans('entities.books_create'));
|
||||
return view('books/create');
|
||||
}
|
||||
|
||||
@@ -71,11 +73,7 @@ class BookController extends Controller
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
]);
|
||||
$book = $this->bookRepo->newFromInput($request->all());
|
||||
$book->slug = $this->bookRepo->findSuitableSlug($book->name);
|
||||
$book->created_by = Auth::user()->id;
|
||||
$book->updated_by = Auth::user()->id;
|
||||
$book->save();
|
||||
$book = $this->entityRepo->createFromInput('book', $request->all());
|
||||
Activity::add($book, 'book_create', $book->id);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
@@ -87,11 +85,17 @@ class BookController extends Controller
|
||||
*/
|
||||
public function show($slug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$bookChildren = $this->bookRepo->getChildren($book);
|
||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||
$this->checkOwnablePermission('book-view', $book);
|
||||
$bookChildren = $this->entityRepo->getBookChildren($book);
|
||||
Views::add($book);
|
||||
$this->setPageTitle($book->getShortName());
|
||||
return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
|
||||
return view('books/show', [
|
||||
'book' => $book,
|
||||
'current' => $book,
|
||||
'bookChildren' => $bookChildren,
|
||||
'activity' => Activity::entityActivity($book, 20, 0)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,9 +105,9 @@ class BookController extends Controller
|
||||
*/
|
||||
public function edit($slug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->setPageTitle('Edit Book ' . $book->getShortName());
|
||||
$this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()]));
|
||||
return view('books/edit', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
|
||||
@@ -115,16 +119,13 @@ class BookController extends Controller
|
||||
*/
|
||||
public function update(Request $request, $slug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
]);
|
||||
$book->fill($request->all());
|
||||
$book->slug = $this->bookRepo->findSuitableSlug($book->name, $book->id);
|
||||
$book->updated_by = Auth::user()->id;
|
||||
$book->save();
|
||||
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
|
||||
Activity::add($book, 'book_update', $book->id);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
@@ -136,9 +137,9 @@ class BookController extends Controller
|
||||
*/
|
||||
public function showDelete($bookSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
$this->setPageTitle('Delete Book ' . $book->getShortName());
|
||||
$this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
|
||||
return view('books/delete', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
|
||||
@@ -149,11 +150,11 @@ class BookController extends Controller
|
||||
*/
|
||||
public function sort($bookSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$bookChildren = $this->bookRepo->getChildren($book, true);
|
||||
$books = $this->bookRepo->getAll(false);
|
||||
$this->setPageTitle('Sort Book ' . $book->getShortName());
|
||||
$bookChildren = $this->entityRepo->getBookChildren($book, true);
|
||||
$books = $this->entityRepo->getAll('book', false);
|
||||
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
|
||||
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
|
||||
}
|
||||
|
||||
@@ -165,8 +166,8 @@ class BookController extends Controller
|
||||
*/
|
||||
public function getSortItem($bookSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$bookChildren = $this->bookRepo->getChildren($book);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$bookChildren = $this->entityRepo->getBookChildren($book);
|
||||
return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
|
||||
}
|
||||
|
||||
@@ -178,7 +179,7 @@ class BookController extends Controller
|
||||
*/
|
||||
public function saveSort($bookSlug, Request $request)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
|
||||
// Return if no map sent
|
||||
@@ -186,21 +187,31 @@ class BookController extends Controller
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
$sortedBooks = [];
|
||||
// Sort pages and chapters
|
||||
$sortedBooks = [];
|
||||
$updatedModels = collect();
|
||||
$sortMap = json_decode($request->get('sort-tree'));
|
||||
$defaultBookId = $book->id;
|
||||
foreach ($sortMap as $index => $bookChild) {
|
||||
$id = $bookChild->id;
|
||||
|
||||
// Loop through contents of provided map and update entities accordingly
|
||||
foreach ($sortMap as $bookChild) {
|
||||
$priority = $bookChild->sort;
|
||||
$id = intval($bookChild->id);
|
||||
$isPage = $bookChild->type == 'page';
|
||||
$bookId = $this->bookRepo->exists($bookChild->book) ? $bookChild->book : $defaultBookId;
|
||||
$model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
|
||||
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
|
||||
$model->priority = $index;
|
||||
if ($isPage) {
|
||||
$model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter;
|
||||
$bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId;
|
||||
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
|
||||
$model = $this->entityRepo->getById($isPage?'page':'chapter', $id);
|
||||
|
||||
// Update models only if there's a change in parent chain or ordering.
|
||||
if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
|
||||
$this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model);
|
||||
$model->priority = $priority;
|
||||
if ($isPage) $model->chapter_id = $chapterId;
|
||||
$model->save();
|
||||
$updatedModels->push($model);
|
||||
}
|
||||
$model->save();
|
||||
|
||||
// Store involved books to be sorted later
|
||||
if (!in_array($bookId, $sortedBooks)) {
|
||||
$sortedBooks[] = $bookId;
|
||||
}
|
||||
@@ -208,7 +219,9 @@ class BookController extends Controller
|
||||
|
||||
// Add activity for books
|
||||
foreach ($sortedBooks as $bookId) {
|
||||
$updatedBook = $this->bookRepo->getById($bookId);
|
||||
/** @var Book $updatedBook */
|
||||
$updatedBook = $this->entityRepo->getById('book', $bookId);
|
||||
$this->entityRepo->buildJointPermissionsForBook($updatedBook);
|
||||
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
|
||||
}
|
||||
|
||||
@@ -222,11 +235,10 @@ class BookController extends Controller
|
||||
*/
|
||||
public function destroy($bookSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
Activity::addMessage('book_delete', 0, $book->name);
|
||||
Activity::removeEntity($book);
|
||||
$this->bookRepo->destroyBySlug($bookSlug);
|
||||
$this->entityRepo->destroyBook($book);
|
||||
return redirect('/books');
|
||||
}
|
||||
|
||||
@@ -237,7 +249,7 @@ class BookController extends Controller
|
||||
*/
|
||||
public function showRestrict($bookSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('books/restrictions', [
|
||||
@@ -255,10 +267,55 @@ class BookController extends Controller
|
||||
*/
|
||||
public function restrict($bookSlug, Request $request)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||
$this->bookRepo->updateRestrictionsFromRequest($request, $book);
|
||||
session()->flash('success', 'Book Restrictions Updated');
|
||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $book);
|
||||
session()->flash('success', trans('entities.books_permissions_updated'));
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a book as a PDF file.
|
||||
* @param string $bookSlug
|
||||
* @return mixed
|
||||
*/
|
||||
public function exportPdf($bookSlug)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$pdfContent = $this->exportService->bookToPdf($book);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a book as a contained HTML file.
|
||||
* @param string $bookSlug
|
||||
* @return mixed
|
||||
*/
|
||||
public function exportHtml($bookSlug)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$htmlContent = $this->exportService->bookToContainedHtml($book);
|
||||
return response()->make($htmlContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a book as a plain text file.
|
||||
* @param $bookSlug
|
||||
* @return mixed
|
||||
*/
|
||||
public function exportPlainText($bookSlug)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$htmlContent = $this->exportService->bookToPlainText($book);
|
||||
return response()->make($htmlContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
||||
class ChapterController extends Controller
|
||||
{
|
||||
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $userRepo;
|
||||
protected $entityRepo;
|
||||
protected $exportService;
|
||||
|
||||
/**
|
||||
* ChapterController constructor.
|
||||
* @param BookRepo $bookRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param UserRepo $userRepo
|
||||
* @param ExportService $exportService
|
||||
*/
|
||||
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
|
||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->exportService = $exportService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function create($bookSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-create', $book);
|
||||
$this->setPageTitle('Create New Chapter');
|
||||
$this->setPageTitle(trans('entities.chapters_create'));
|
||||
return view('chapters/create', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
|
||||
@@ -54,15 +54,12 @@ class ChapterController extends Controller
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-create', $book);
|
||||
|
||||
$chapter = $this->chapterRepo->newFromInput($request->all());
|
||||
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id);
|
||||
$chapter->priority = $this->bookRepo->getNewPriority($book);
|
||||
$chapter->created_by = auth()->user()->id;
|
||||
$chapter->updated_by = auth()->user()->id;
|
||||
$book->chapters()->save($chapter);
|
||||
$input = $request->all();
|
||||
$input['priority'] = $this->entityRepo->getNewBookPriority($book);
|
||||
$chapter = $this->entityRepo->createFromInput('chapter', $input, $book);
|
||||
Activity::add($chapter, 'chapter_create', $book->id);
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
@@ -75,14 +72,14 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function show($bookSlug, $chapterSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-view', $chapter);
|
||||
$sidebarTree = $this->entityRepo->getBookChildren($chapter->book);
|
||||
Views::add($chapter);
|
||||
$this->setPageTitle($chapter->getShortName());
|
||||
$pages = $this->chapterRepo->getChildren($chapter);
|
||||
$pages = $this->entityRepo->getChapterChildren($chapter);
|
||||
return view('chapters/show', [
|
||||
'book' => $book,
|
||||
'book' => $chapter->book,
|
||||
'chapter' => $chapter,
|
||||
'current' => $chapter,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
@@ -98,11 +95,10 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function edit($bookSlug, $chapterSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
$this->setPageTitle('Edit Chapter' . $chapter->getShortName());
|
||||
return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
|
||||
return view('chapters/edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,14 +110,15 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function update(Request $request, $bookSlug, $chapterSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
if ($chapter->name !== $request->get('name')) {
|
||||
$chapter->slug = $this->entityRepo->findSuitableSlug('chapter', $request->get('name'), $chapter->id, $chapter->book->id);
|
||||
}
|
||||
$chapter->fill($request->all());
|
||||
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
|
||||
$chapter->updated_by = auth()->user()->id;
|
||||
$chapter->updated_by = user()->id;
|
||||
$chapter->save();
|
||||
Activity::add($chapter, 'chapter_update', $book->id);
|
||||
Activity::add($chapter, 'chapter_update', $chapter->book->id);
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
@@ -133,11 +130,10 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function showDelete($bookSlug, $chapterSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||
$this->setPageTitle('Delete Chapter' . $chapter->getShortName());
|
||||
return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
|
||||
return view('chapters/delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,14 +144,70 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function destroy($bookSlug, $chapterSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$book = $chapter->book;
|
||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||
Activity::addMessage('chapter_delete', $book->id, $chapter->name);
|
||||
$this->chapterRepo->destroy($chapter);
|
||||
$this->entityRepo->destroyChapter($chapter);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page for moving a chapter.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function showMove($bookSlug, $chapterSlug) {
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
return view('chapters/move', [
|
||||
'chapter' => $chapter,
|
||||
'book' => $chapter->book
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the move action for a chapter.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function move($bookSlug, $chapterSlug, Request $request) {
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
|
||||
$entitySelection = $request->get('entity_selection', null);
|
||||
if ($entitySelection === null || $entitySelection === '') {
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
$stringExploded = explode(':', $entitySelection);
|
||||
$entityType = $stringExploded[0];
|
||||
$entityId = intval($stringExploded[1]);
|
||||
|
||||
$parent = false;
|
||||
|
||||
if ($entityType == 'book') {
|
||||
$parent = $this->entityRepo->getById('book', $entityId);
|
||||
}
|
||||
|
||||
if ($parent === false || $parent === null) {
|
||||
session()->flash('error', trans('errors.selected_book_not_found'));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
$this->entityRepo->changeBook('chapter', $parent->id, $chapter, true);
|
||||
Activity::add($chapter, 'chapter_move', $chapter->book->id);
|
||||
session()->flash('success', trans('entities.chapter_move_success', ['bookName' => $parent->name]));
|
||||
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $bookSlug
|
||||
@@ -164,8 +216,7 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function showRestrict($bookSlug, $chapterSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('chapters/restrictions', [
|
||||
@@ -183,11 +234,58 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function restrict($bookSlug, $chapterSlug, Request $request)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||
$this->chapterRepo->updateRestrictionsFromRequest($request, $chapter);
|
||||
session()->flash('success', 'Chapter Restrictions Updated');
|
||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter);
|
||||
session()->flash('success', trans('entities.chapters_permissions_success'));
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a chapter to pdf .
|
||||
* @param string $bookSlug
|
||||
* @param string $chapterSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportPdf($bookSlug, $chapterSlug)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$pdfContent = $this->exportService->chapterToPdf($chapter);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a chapter to a self-contained HTML file.
|
||||
* @param string $bookSlug
|
||||
* @param string $chapterSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportHtml($bookSlug, $chapterSlug)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$containedHtml = $this->exportService->chapterToContainedHtml($chapter);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a chapter to a simple plaintext .txt file.
|
||||
* @param string $bookSlug
|
||||
* @param string $chapterSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportPlainText($bookSlug, $chapterSlug)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$containedHtml = $this->exportService->chapterToPlainText($chapter);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
93
app/Http/Controllers/CommentController.php
Normal file
93
app/Http/Controllers/CommentController.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\CommentRepo;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
protected $entityRepo;
|
||||
protected $commentRepo;
|
||||
|
||||
/**
|
||||
* CommentController constructor.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param CommentRepo $commentRepo
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->commentRepo = $commentRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new comment for a Page
|
||||
* @param Request $request
|
||||
* @param integer $pageId
|
||||
* @param null|integer $commentId
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function savePageComment(Request $request, $pageId, $commentId = null)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'text' => 'required|string',
|
||||
'html' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return response('Not found', 404);
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
// Prevent adding comments to draft pages
|
||||
if ($page->draft) {
|
||||
return $this->jsonError(trans('errors.cannot_add_comment_to_draft'), 400);
|
||||
}
|
||||
|
||||
// Create a new comment.
|
||||
$this->checkPermission('comment-create-all');
|
||||
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
|
||||
Activity::add($page, 'commented_on', $page->book->id);
|
||||
return view('comments/comment', ['comment' => $comment]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing comment.
|
||||
* @param Request $request
|
||||
* @param integer $commentId
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function update(Request $request, $commentId)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'text' => 'required|string',
|
||||
'html' => 'required|string',
|
||||
]);
|
||||
|
||||
$comment = $this->commentRepo->getById($commentId);
|
||||
$this->checkOwnablePermission('page-view', $comment->entity);
|
||||
$this->checkOwnablePermission('comment-update', $comment);
|
||||
|
||||
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
|
||||
return view('comments/comment', ['comment' => $comment]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a comment from the system.
|
||||
* @param integer $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$comment = $this->commentRepo->getById($id);
|
||||
$this->checkOwnablePermission('comment-delete', $comment);
|
||||
$this->commentRepo->delete($comment);
|
||||
return response()->json(['message' => trans('entities.comment_deleted')]);
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,11 @@
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Ownable;
|
||||
use HttpRequestException;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Http\Exception\HttpResponseException;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use BookStack\User;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
@@ -30,17 +28,21 @@ abstract class Controller extends BaseController
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// Get a user instance for the current user
|
||||
$user = auth()->user();
|
||||
if (!$user) $user = User::getDefault();
|
||||
$this->middleware(function ($request, $next) {
|
||||
|
||||
// Share variables with views
|
||||
view()->share('signedIn', auth()->check());
|
||||
view()->share('currentUser', $user);
|
||||
// Get a user instance for the current user
|
||||
$user = user();
|
||||
|
||||
// Share variables with controllers
|
||||
$this->currentUser = $user;
|
||||
$this->signedIn = auth()->check();
|
||||
// Share variables with controllers
|
||||
$this->currentUser = $user;
|
||||
$this->signedIn = auth()->check();
|
||||
|
||||
// Share variables with views
|
||||
view()->share('signedIn', $this->signedIn);
|
||||
view()->share('currentUser', $user);
|
||||
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,8 +69,13 @@ abstract class Controller extends BaseController
|
||||
*/
|
||||
protected function showPermissionError()
|
||||
{
|
||||
Session::flash('error', trans('errors.permission'));
|
||||
$response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/');
|
||||
if (request()->wantsJson()) {
|
||||
$response = response()->json(['error' => trans('errors.permissionJson')], 403);
|
||||
} else {
|
||||
$response = redirect('/');
|
||||
session()->flash('error', trans('errors.permission'));
|
||||
}
|
||||
|
||||
throw new HttpResponseException($response);
|
||||
}
|
||||
|
||||
@@ -79,7 +86,7 @@ abstract class Controller extends BaseController
|
||||
*/
|
||||
protected function checkPermission($permissionName)
|
||||
{
|
||||
if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
|
||||
if (!user() || !user()->can($permissionName)) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
return true;
|
||||
@@ -110,4 +117,33 @@ abstract class Controller extends BaseController
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send back a json error message.
|
||||
* @param string $messageText
|
||||
* @param int $statusCode
|
||||
* @return mixed
|
||||
*/
|
||||
protected function jsonError($messageText = "", $statusCode = 500)
|
||||
{
|
||||
return response()->json(['message' => $messageText], $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the response for when a request fails validation.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param array $errors
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function buildFailedValidationResponse(Request $request, array $errors)
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['validation' => $errors], 422);
|
||||
}
|
||||
|
||||
return redirect()->to($this->getRedirectUrl())
|
||||
->withInput($request->input())
|
||||
->withErrors($errors, $this->errorBag());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Http\Requests;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
||||
class HomeController extends Controller
|
||||
@@ -31,15 +29,60 @@ class HomeController extends Controller
|
||||
$activity = Activity::latest(10);
|
||||
$draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
|
||||
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreatedBooks(10*$recentFactor);
|
||||
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreatedPages(5);
|
||||
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdatedPages(5);
|
||||
return view('home', [
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
|
||||
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
|
||||
|
||||
// Custom homepage
|
||||
$customHomepage = false;
|
||||
$homepageSetting = setting('app-homepage');
|
||||
if ($homepageSetting) {
|
||||
$id = intval(explode(':', $homepageSetting)[0]);
|
||||
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
|
||||
$this->entityRepo->renderPage($customHomepage, true);
|
||||
}
|
||||
|
||||
$view = $customHomepage ? 'home-custom' : 'home';
|
||||
return view($view, [
|
||||
'activity' => $activity,
|
||||
'recents' => $recents,
|
||||
'recentlyCreatedPages' => $recentlyCreatedPages,
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||
'draftPages' => $draftPages
|
||||
'draftPages' => $draftPages,
|
||||
'customHomepage' => $customHomepage
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a js representation of the current translations
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function getTranslations() {
|
||||
$locale = app()->getLocale();
|
||||
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
|
||||
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
|
||||
$resp = cache($cacheKey);
|
||||
} else {
|
||||
$translations = [
|
||||
// Get only translations which might be used in JS
|
||||
'common' => trans('common'),
|
||||
'components' => trans('components'),
|
||||
'entities' => trans('entities'),
|
||||
'errors' => trans('errors')
|
||||
];
|
||||
if ($locale !== 'en') {
|
||||
$enTrans = [
|
||||
'common' => trans('common', [], 'en'),
|
||||
'components' => trans('components', [], 'en'),
|
||||
'entities' => trans('entities', [], 'en'),
|
||||
'errors' => trans('errors', [], 'en')
|
||||
];
|
||||
$translations = array_replace_recursive($enTrans, $translations);
|
||||
}
|
||||
$resp = 'window.translations = ' . json_encode($translations);
|
||||
cache()->put($cacheKey, $resp, 120);
|
||||
}
|
||||
|
||||
return response($resp, 200, [
|
||||
'Content-Type' => 'application/javascript'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\ImageRepo;
|
||||
use Illuminate\Filesystem\Filesystem as File;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -51,9 +52,9 @@ class ImageController extends Controller
|
||||
$this->validate($request, [
|
||||
'term' => 'required|string'
|
||||
]);
|
||||
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$imgData = $this->imageRepo->searchPaginatedByType($type, $page,24, $searchTerm);
|
||||
$imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm);
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
@@ -73,6 +74,7 @@ class ImageController extends Controller
|
||||
* @param $filter
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function getGalleryFiltered($filter, $page = 0, Request $request)
|
||||
{
|
||||
@@ -99,7 +101,7 @@ class ImageController extends Controller
|
||||
{
|
||||
$this->checkPermission('image-create-all');
|
||||
$this->validate($request, [
|
||||
'file' => 'image|mimes:jpeg,gif,png'
|
||||
'file' => 'is_image'
|
||||
]);
|
||||
|
||||
$imageUpload = $request->file('file');
|
||||
@@ -149,12 +151,12 @@ class ImageController extends Controller
|
||||
|
||||
/**
|
||||
* Deletes an image and all thumbnail/image files
|
||||
* @param PageRepo $pageRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function destroy(PageRepo $pageRepo, Request $request, $id)
|
||||
public function destroy(EntityRepo $entityRepo, Request $request, $id)
|
||||
{
|
||||
$image = $this->imageRepo->getById($id);
|
||||
$this->checkOwnablePermission('image-delete', $image);
|
||||
@@ -162,14 +164,14 @@ class ImageController extends Controller
|
||||
// Check if this image is used on any pages
|
||||
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
|
||||
if (!$isForced) {
|
||||
$pageSearch = $pageRepo->searchForImage($image->url);
|
||||
$pageSearch = $entityRepo->searchForImage($image->url);
|
||||
if ($pageSearch !== false) {
|
||||
return response()->json($pageSearch, 400);
|
||||
}
|
||||
}
|
||||
|
||||
$this->imageRepo->destroyImage($image);
|
||||
return response()->json('Image Deleted');
|
||||
return response()->json(trans('components.images_deleted'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,39 +2,31 @@
|
||||
|
||||
use Activity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use BookStack\Repos\PageRepo;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
use GatherContent\Htmldiff\Htmldiff;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
|
||||
protected $pageRepo;
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $entityRepo;
|
||||
protected $exportService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* PageController constructor.
|
||||
* @param PageRepo $pageRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param ExportService $exportService
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo)
|
||||
public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->exportService = $exportService;
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
@@ -42,43 +34,82 @@ class PageController extends Controller
|
||||
|
||||
/**
|
||||
* Show the form for creating a new page.
|
||||
* @param $bookSlug
|
||||
* @param bool $chapterSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $chapterSlug
|
||||
* @return Response
|
||||
* @internal param bool $pageSlug
|
||||
*/
|
||||
public function create($bookSlug, $chapterSlug = false)
|
||||
public function create($bookSlug, $chapterSlug = null)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
|
||||
$parent = $chapter ? $chapter : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
$this->setPageTitle('Create New Page');
|
||||
|
||||
$draft = $this->pageRepo->getDraftPage($book, $chapter);
|
||||
return redirect($draft->getUrl());
|
||||
// Redirect to draft edit screen if signed in
|
||||
if ($this->signedIn) {
|
||||
$draft = $this->entityRepo->getDraftPage($book, $chapter);
|
||||
return redirect($draft->getUrl());
|
||||
}
|
||||
|
||||
// Otherwise show edit view
|
||||
$this->setPageTitle(trans('entities.pages_new'));
|
||||
return view('pages/guest-create', ['parent' => $parent]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new page as a guest user.
|
||||
* @param Request $request
|
||||
* @param string $bookSlug
|
||||
* @param string|null $chapterSlug
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
|
||||
$parent = $chapter ? $chapter : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
$page = $this->entityRepo->getDraftPage($book, $chapter);
|
||||
$this->entityRepo->publishPageDraft($page, [
|
||||
'name' => $request->get('name'),
|
||||
'html' => ''
|
||||
]);
|
||||
return redirect($page->getUrl('/edit'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show form to continue editing a draft page.
|
||||
* @param $bookSlug
|
||||
* @param $pageId
|
||||
* @param string $bookSlug
|
||||
* @param int $pageId
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function editDraft($bookSlug, $pageId)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$draft = $this->pageRepo->getById($pageId, true);
|
||||
$this->checkOwnablePermission('page-create', $draft);
|
||||
$this->setPageTitle('Edit Page Draft');
|
||||
$draft = $this->entityRepo->getById('page', $pageId, true);
|
||||
$this->checkOwnablePermission('page-create', $draft->book);
|
||||
$this->setPageTitle(trans('entities.pages_edit_draft'));
|
||||
|
||||
return view('pages/create', ['draft' => $draft, 'book' => $book]);
|
||||
$draftsEnabled = $this->signedIn;
|
||||
return view('pages/edit', [
|
||||
'page' => $draft,
|
||||
'book' => $draft->book,
|
||||
'isDraft' => true,
|
||||
'draftsEnabled' => $draftsEnabled
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new page by changing a draft into a page.
|
||||
* @param Request $request
|
||||
* @param string $bookSlug
|
||||
* @param int $pageId
|
||||
* @return Response
|
||||
*/
|
||||
public function store(Request $request, $bookSlug, $pageId)
|
||||
@@ -88,21 +119,21 @@ class PageController extends Controller
|
||||
]);
|
||||
|
||||
$input = $request->all();
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
|
||||
$draftPage = $this->pageRepo->getById($pageId, true);
|
||||
$draftPage = $this->entityRepo->getById('page', $pageId, true);
|
||||
|
||||
$chapterId = $draftPage->chapter_id;
|
||||
$parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
|
||||
$chapterId = intval($draftPage->chapter_id);
|
||||
$parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
if ($parent->isA('chapter')) {
|
||||
$input['priority'] = $this->chapterRepo->getNewPriority($parent);
|
||||
$input['priority'] = $this->entityRepo->getNewChapterPriority($parent);
|
||||
} else {
|
||||
$input['priority'] = $this->bookRepo->getNewPriority($parent);
|
||||
$input['priority'] = $this->entityRepo->getNewBookPriority($parent);
|
||||
}
|
||||
|
||||
$page = $this->pageRepo->publishDraft($draftPage, $input);
|
||||
$page = $this->entityRepo->publishPageDraft($draftPage, $input);
|
||||
|
||||
Activity::add($page, 'page_create', $book->id);
|
||||
return redirect($page->getUrl());
|
||||
@@ -110,81 +141,92 @@ class PageController extends Controller
|
||||
|
||||
/**
|
||||
* Display the specified page.
|
||||
* If the page is not found via the slug the
|
||||
* revisions are searched for a match.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* If the page is not found via the slug the revisions are searched for a match.
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function show($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
|
||||
try {
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
} catch (NotFoundException $e) {
|
||||
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
|
||||
$page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
|
||||
if ($page === null) abort(404);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
|
||||
$pageNav = $this->entityRepo->getPageNav($page->html);
|
||||
$page->load(['comments.createdBy']);
|
||||
|
||||
Views::add($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]);
|
||||
return view('pages/show', [
|
||||
'page' => $page,'book' => $page->book,
|
||||
'current' => $page, 'sidebarTree' => $sidebarTree,
|
||||
'pageNav' => $pageNav]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page from an ajax request.
|
||||
* @param $pageId
|
||||
* @param int $pageId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getPageAjax($pageId)
|
||||
{
|
||||
$page = $this->pageRepo->getById($pageId);
|
||||
$page = $this->entityRepo->getById('page', $pageId);
|
||||
return response()->json($page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified page.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function edit($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->setPageTitle('Editing Page ' . $page->getShortName());
|
||||
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
|
||||
$page->isDraft = false;
|
||||
|
||||
// Check for active editing
|
||||
$warnings = [];
|
||||
if ($this->pageRepo->isPageEditingActive($page, 60)) {
|
||||
$warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
|
||||
if ($this->entityRepo->isPageEditingActive($page, 60)) {
|
||||
$warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60);
|
||||
}
|
||||
|
||||
// Check for a current draft version for this user
|
||||
if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
|
||||
$draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
|
||||
if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
|
||||
$draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id);
|
||||
$page->name = $draft->name;
|
||||
$page->html = $draft->html;
|
||||
$page->markdown = $draft->markdown;
|
||||
$page->isDraft = true;
|
||||
$warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
|
||||
$warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
|
||||
}
|
||||
|
||||
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
|
||||
|
||||
return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
|
||||
$draftsEnabled = $this->signedIn;
|
||||
return view('pages/edit', [
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'current' => $page,
|
||||
'draftsEnabled' => $draftsEnabled
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified page in storage.
|
||||
* @param Request $request
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function update(Request $request, $bookSlug, $pageSlug)
|
||||
@@ -192,35 +234,38 @@ class PageController extends Controller
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->pageRepo->updatePage($page, $book->id, $request->all());
|
||||
Activity::add($page, 'page_update', $book->id);
|
||||
$this->entityRepo->updatePage($page, $page->book->id, $request->all());
|
||||
Activity::add($page, 'page_update', $page->book->id);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a draft update as a revision.
|
||||
* @param Request $request
|
||||
* @param $pageId
|
||||
* @param int $pageId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function saveDraft(Request $request, $pageId)
|
||||
{
|
||||
$page = $this->pageRepo->getById($pageId, true);
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
if ($page->draft) {
|
||||
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
|
||||
} else {
|
||||
$draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
|
||||
|
||||
if (!$this->signedIn) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => trans('errors.guests_cannot_save_drafts'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
|
||||
|
||||
$updateTime = $draft->updated_at->timestamp;
|
||||
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Draft saved at ',
|
||||
'status' => 'success',
|
||||
'message' => trans('entities.pages_edit_draft_save_at'),
|
||||
'timestamp' => $utcUpdateTimestamp
|
||||
]);
|
||||
}
|
||||
@@ -228,141 +273,175 @@ class PageController extends Controller
|
||||
/**
|
||||
* Redirect from a special link url which
|
||||
* uses the page id rather than the name.
|
||||
* @param $pageId
|
||||
* @param int $pageId
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function redirectFromLink($pageId)
|
||||
{
|
||||
$page = $this->pageRepo->getById($pageId);
|
||||
$page = $this->entityRepo->getById('page', $pageId);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the deletion page for the specified page.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function showDelete($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
$this->setPageTitle('Delete Page ' . $page->getShortName());
|
||||
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
|
||||
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
|
||||
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the deletion page for the specified page.
|
||||
* @param $bookSlug
|
||||
* @param $pageId
|
||||
* @param string $bookSlug
|
||||
* @param int $pageId
|
||||
* @return \Illuminate\View\View
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function showDeleteDraft($bookSlug, $pageId)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getById($pageId, true);
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->setPageTitle('Delete Draft Page ' . $page->getShortName());
|
||||
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
|
||||
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
|
||||
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified page from storage.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return Response
|
||||
* @internal param int $id
|
||||
*/
|
||||
public function destroy($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$book = $page->book;
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
Activity::addMessage('page_delete', $book->id, $page->name);
|
||||
session()->flash('success', 'Page deleted');
|
||||
$this->pageRepo->destroy($page);
|
||||
session()->flash('success', trans('entities.pages_delete_success'));
|
||||
$this->entityRepo->destroyPage($page);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified draft page from storage.
|
||||
* @param $bookSlug
|
||||
* @param $pageId
|
||||
* @param string $bookSlug
|
||||
* @param int $pageId
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function destroyDraft($bookSlug, $pageId)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getById($pageId, true);
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$book = $page->book;
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
session()->flash('success', 'Draft deleted');
|
||||
$this->pageRepo->destroy($page);
|
||||
session()->flash('success', trans('entities.pages_delete_draft_success'));
|
||||
$this->entityRepo->destroyPage($page);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the last revisions for this page.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function showRevisions($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->setPageTitle('Revisions For ' . $page->getShortName());
|
||||
return view('pages/revisions', ['page' => $page, 'book' => $book, 'current' => $page]);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
|
||||
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a preview of a single revision
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param $revisionId
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param int $revisionId
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function showRevision($bookSlug, $pageSlug, $revisionId)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$revision = $this->pageRepo->getRevisionById($revisionId);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||
if ($revision === null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$page->fill($revision->toArray());
|
||||
$this->setPageTitle('Page Revision For ' . $page->getShortName());
|
||||
return view('pages/revision', ['page' => $page, 'book' => $book]);
|
||||
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
|
||||
|
||||
return view('pages/revision', [
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'revision' => $revision
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the changes of a single revision
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param int $revisionId
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||
if ($revision === null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$prev = $revision->getPrevious();
|
||||
$prevContent = ($prev === null) ? '' : $prev->html;
|
||||
$diff = (new Htmldiff)->diff($prevContent, $revision->html);
|
||||
|
||||
$page->fill($revision->toArray());
|
||||
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
|
||||
|
||||
return view('pages/revision', [
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'diff' => $diff,
|
||||
'revision' => $revision
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a page using the content of the specified revision.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param $revisionId
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param int $revisionId
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
|
||||
Activity::add($page, 'page_restore', $book->id);
|
||||
$page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId);
|
||||
Activity::add($page, 'page_restore', $page->book->id);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
|
||||
* Exports a page to a PDF.
|
||||
* https://github.com/barryvdh/laravel-dompdf
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportPdf($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
$pdfContent = $this->exportService->pageToPdf($page);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
@@ -372,14 +451,14 @@ class PageController extends Controller
|
||||
|
||||
/**
|
||||
* Export a page to a self-contained HTML file.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportHtml($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
$containedHtml = $this->exportService->pageToContainedHtml($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
@@ -389,14 +468,13 @@ class PageController extends Controller
|
||||
|
||||
/**
|
||||
* Export a page to a simple plaintext .txt file.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportPlainText($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$containedHtml = $this->exportService->pageToPlainText($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
@@ -410,9 +488,9 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRecentlyCreated()
|
||||
{
|
||||
$pages = $this->pageRepo->getRecentlyCreatedPaginated(20);
|
||||
$pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => 'Recently Created Pages',
|
||||
'title' => trans('entities.recently_created_pages'),
|
||||
'pages' => $pages
|
||||
]);
|
||||
}
|
||||
@@ -423,23 +501,22 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRecentlyUpdated()
|
||||
{
|
||||
$pages = $this->pageRepo->getRecentlyUpdatedPaginated(20);
|
||||
$pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => 'Recently Updated Pages',
|
||||
'title' => trans('entities.recently_updated_pages'),
|
||||
'pages' => $pages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRestrict($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('pages/restrictions', [
|
||||
@@ -449,19 +526,72 @@ class PageController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the restrictions for this page.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* Show the view to choose a new parent to move a page into.
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function showMove($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
return view('pages/move', [
|
||||
'book' => $page->book,
|
||||
'page' => $page
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the action of moving the location of a page
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function move($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$entitySelection = $request->get('entity_selection', null);
|
||||
if ($entitySelection === null || $entitySelection === '') {
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
$stringExploded = explode(':', $entitySelection);
|
||||
$entityType = $stringExploded[0];
|
||||
$entityId = intval($stringExploded[1]);
|
||||
|
||||
|
||||
try {
|
||||
$parent = $this->entityRepo->getById($entityType, $entityId);
|
||||
} catch (\Exception $e) {
|
||||
session()->flash(trans('entities.selected_book_chapter_not_found'));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
$this->entityRepo->changePageParent($page, $parent);
|
||||
Activity::add($page, 'page_move', $page->book->id);
|
||||
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
|
||||
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the permissions for this page.
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function restrict($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$this->pageRepo->updateRestrictionsFromRequest($request, $page);
|
||||
session()->flash('success', 'Page Restrictions Updated');
|
||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $page);
|
||||
session()->flash('success', trans('entities.pages_permissions_success'));
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Repos\PermissionsRepo;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Http\Requests;
|
||||
|
||||
class PermissionController extends Controller
|
||||
{
|
||||
@@ -54,7 +53,7 @@ class PermissionController extends Controller
|
||||
]);
|
||||
|
||||
$this->permissionsRepo->saveNewRole($request->all());
|
||||
session()->flash('success', 'Role successfully created');
|
||||
session()->flash('success', trans('settings.role_create_success'));
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
|
||||
@@ -62,11 +61,13 @@ class PermissionController extends Controller
|
||||
* Show the form for editing a user role.
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function editRole($id)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
if ($role->hidden) throw new PermissionsException(trans('errors.role_cannot_be_edited'));
|
||||
return view('settings/roles/edit', ['role' => $role]);
|
||||
}
|
||||
|
||||
@@ -85,7 +86,7 @@ class PermissionController extends Controller
|
||||
]);
|
||||
|
||||
$this->permissionsRepo->updateRole($id, $request->all());
|
||||
session()->flash('success', 'Role successfully updated');
|
||||
session()->flash('success', trans('settings.role_update_success'));
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
|
||||
@@ -100,7 +101,7 @@ class PermissionController extends Controller
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
$roles = $this->permissionsRepo->getAllRolesExcept($role);
|
||||
$blankRole = $role->newInstance(['display_name' => 'Don\'t migrate users']);
|
||||
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
|
||||
$roles->prepend($blankRole);
|
||||
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
|
||||
}
|
||||
@@ -123,7 +124,7 @@ class PermissionController extends Controller
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
session()->flash('success', 'Role successfully deleted');
|
||||
session()->flash('success', trans('settings.role_delete_success'));
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Services\SearchService;
|
||||
use BookStack\Services\ViewService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use BookStack\Repos\PageRepo;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
protected $pageRepo;
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $entityRepo;
|
||||
protected $viewService;
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* SearchController constructor.
|
||||
* @param $pageRepo
|
||||
* @param $bookRepo
|
||||
* @param $chapterRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param ViewService $viewService
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->viewService = $viewService;
|
||||
$this->searchService = $searchService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -36,84 +31,26 @@ class SearchController extends Controller
|
||||
* @return \Illuminate\View\View
|
||||
* @internal param string $searchTerm
|
||||
*/
|
||||
public function searchAll(Request $request)
|
||||
public function search(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) {
|
||||
return redirect()->back();
|
||||
}
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
|
||||
$books = $this->bookRepo->getBySearch($searchTerm, 10, $paginationAppends);
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
|
||||
$this->setPageTitle('Search For ' . $searchTerm);
|
||||
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
|
||||
|
||||
$page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1;
|
||||
$nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
|
||||
|
||||
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
|
||||
$hasNextPage = $this->searchService->searchEntities($searchTerm, 'all', $page+1, 20)['count'] > 0;
|
||||
|
||||
return view('search/all', [
|
||||
'pages' => $pages,
|
||||
'books' => $books,
|
||||
'chapters' => $chapters,
|
||||
'searchTerm' => $searchTerm
|
||||
'entities' => $results['results'],
|
||||
'totalResults' => $results['total'],
|
||||
'searchTerm' => $searchTerm,
|
||||
'hasNextPage' => $hasNextPage,
|
||||
'nextPageLink' => $nextPageLink
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the pages in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchPages(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle('Page Search For ' . $searchTerm);
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $pages,
|
||||
'title' => 'Page Search Results',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the chapters in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchChapters(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle('Chapter Search For ' . $searchTerm);
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $chapters,
|
||||
'title' => 'Chapter Search Results',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the books in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchBooks(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends);
|
||||
$this->setPageTitle('Book Search For ' . $searchTerm);
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $books,
|
||||
'title' => 'Book Search Results',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches all entities within a book.
|
||||
@@ -124,14 +61,50 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function searchBook(Request $request, $bookId)
|
||||
{
|
||||
if (!$request->has('term')) {
|
||||
return redirect()->back();
|
||||
$term = $request->get('term', '');
|
||||
$results = $this->searchService->searchBook($bookId, $term);
|
||||
return view('partials/entity-list', ['entities' => $results]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches all entities within a chapter.
|
||||
* @param Request $request
|
||||
* @param integer $chapterId
|
||||
* @return \Illuminate\View\View
|
||||
* @internal param string $searchTerm
|
||||
*/
|
||||
public function searchChapter(Request $request, $chapterId)
|
||||
{
|
||||
$term = $request->get('term', '');
|
||||
$results = $this->searchService->searchChapter($chapterId, $term);
|
||||
return view('partials/entity-list', ['entities' => $results]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a list of entities and return a partial HTML response of matching entities.
|
||||
* Returns the most popular entities if no search is provided.
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchEntitiesAjax(Request $request)
|
||||
{
|
||||
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
|
||||
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
|
||||
|
||||
// Search for entities otherwise show most popular
|
||||
if ($searchTerm !== false) {
|
||||
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
|
||||
$entities = $this->searchService->searchEntities($searchTerm)['results'];
|
||||
} else {
|
||||
$entityNames = $entityTypes->map(function ($type) {
|
||||
return 'BookStack\\' . ucfirst($type);
|
||||
})->toArray();
|
||||
$entities = $this->viewService->getPopular(20, 0, $entityNames);
|
||||
}
|
||||
$searchTerm = $request->get('term');
|
||||
$searchWhereTerms = [['book_id', '=', $bookId]];
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm, $searchWhereTerms);
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, $searchWhereTerms);
|
||||
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
|
||||
|
||||
return view('search/entity-ajax-list', ['entities' => $entities]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use BookStack\Http\Requests;
|
||||
use Illuminate\Http\Response;
|
||||
use Setting;
|
||||
|
||||
class SettingController extends Controller
|
||||
@@ -15,7 +14,11 @@ class SettingController extends Controller
|
||||
{
|
||||
$this->checkPermission('settings-manage');
|
||||
$this->setPageTitle('Settings');
|
||||
return view('settings/index');
|
||||
|
||||
// Get application version
|
||||
$version = trim(file_get_contents(base_path('version')));
|
||||
|
||||
return view('settings/index', ['version' => $version]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,7 +38,7 @@ class SettingController extends Controller
|
||||
Setting::put($key, $value);
|
||||
}
|
||||
|
||||
session()->flash('success', 'Settings Saved');
|
||||
session()->flash('success', trans('settings.settings_save_success'));
|
||||
return redirect('/settings');
|
||||
}
|
||||
|
||||
|
||||
58
app/Http/Controllers/TagController.php
Normal file
58
app/Http/Controllers/TagController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Repos\TagRepo;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
|
||||
protected $tagRepo;
|
||||
|
||||
/**
|
||||
* TagController constructor.
|
||||
* @param $tagRepo
|
||||
*/
|
||||
public function __construct(TagRepo $tagRepo)
|
||||
{
|
||||
$this->tagRepo = $tagRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the Tags for a particular entity
|
||||
* @param $entityType
|
||||
* @param $entityId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getForEntity($entityType, $entityId)
|
||||
{
|
||||
$tags = $this->tagRepo->getForEntity($entityType, $entityId);
|
||||
return response()->json($tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag name suggestions from a given search term.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getNameSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->has('search') ? $request->get('search') : false;
|
||||
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
|
||||
return response()->json($suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag value suggestions from a given search term.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getValueSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->has('search') ? $request->get('search') : false;
|
||||
$tagName = $request->has('name') ? $request->get('name') : false;
|
||||
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
|
||||
return response()->json($suggestions);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
<?php
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Activity;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use BookStack\User;
|
||||
@@ -31,14 +27,21 @@ class UserController extends Controller
|
||||
|
||||
/**
|
||||
* Display a listing of the users.
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->checkPermission('users-manage');
|
||||
$users = $this->userRepo->getAllUsers();
|
||||
$this->setPageTitle('Users');
|
||||
return view('users/index', ['users' => $users]);
|
||||
$listDetails = [
|
||||
'order' => $request->has('order') ? $request->get('order') : 'asc',
|
||||
'search' => $request->has('search') ? $request->get('search') : '',
|
||||
'sort' => $request->has('sort') ? $request->get('sort') : 'name',
|
||||
];
|
||||
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
|
||||
$this->setPageTitle(trans('settings.users'));
|
||||
$users->appends($listDetails);
|
||||
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +52,8 @@ class UserController extends Controller
|
||||
{
|
||||
$this->checkPermission('users-manage');
|
||||
$authMethod = config('auth.method');
|
||||
return view('users/create', ['authMethod' => $authMethod]);
|
||||
$roles = $this->userRepo->getAllRoles();
|
||||
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +78,6 @@ class UserController extends Controller
|
||||
}
|
||||
$this->validate($request, $validationRules);
|
||||
|
||||
|
||||
$user = $this->user->fill($request->all());
|
||||
|
||||
if ($authMethod === 'standard') {
|
||||
@@ -92,9 +95,14 @@ class UserController extends Controller
|
||||
|
||||
// Get avatar from gravatar and save
|
||||
if (!config('services.disable_services')) {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
try {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
} catch (Exception $e) {
|
||||
\Log::error('Failed to save user gravatar image');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return redirect('/settings/users');
|
||||
@@ -112,12 +120,14 @@ class UserController extends Controller
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$authMethod = config('auth.method');
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
|
||||
$authMethod = ($user->system_name) ? 'system' : config('auth.method');
|
||||
|
||||
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
||||
$this->setPageTitle('User Profile');
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod]);
|
||||
$this->setPageTitle(trans('settings.user_profile'));
|
||||
$roles = $this->userRepo->getAllRoles();
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,9 +147,8 @@ class UserController extends Controller
|
||||
'name' => 'min:2',
|
||||
'email' => 'min:2|email|unique:users,email,' . $id,
|
||||
'password' => 'min:5|required_with:password_confirm',
|
||||
'password-confirm' => 'same:password|required_with:password'
|
||||
], [
|
||||
'password-confirm.required_with' => 'Password confirmation required'
|
||||
'password-confirm' => 'same:password|required_with:password',
|
||||
'setting' => 'array'
|
||||
]);
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
@@ -162,8 +171,15 @@ class UserController extends Controller
|
||||
$user->external_auth_id = $request->get('external_auth_id');
|
||||
}
|
||||
|
||||
// Save an user-specific settings
|
||||
if ($request->has('setting')) {
|
||||
foreach ($request->get('setting') as $key => $value) {
|
||||
setting()->putUser($user, $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
$user->save();
|
||||
session()->flash('success', 'User successfully updated');
|
||||
session()->flash('success', trans('settings.users_edit_success'));
|
||||
|
||||
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
|
||||
return redirect($redirectUrl);
|
||||
@@ -171,7 +187,7 @@ class UserController extends Controller
|
||||
|
||||
/**
|
||||
* Show the user delete page.
|
||||
* @param $id
|
||||
* @param int $id
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function delete($id)
|
||||
@@ -181,7 +197,7 @@ class UserController extends Controller
|
||||
});
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
$this->setPageTitle('Delete User ' . $user->name);
|
||||
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
|
||||
return view('users/delete', ['user' => $user]);
|
||||
}
|
||||
|
||||
@@ -198,11 +214,19 @@ class UserController extends Controller
|
||||
});
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
|
||||
if ($this->userRepo->isOnlyAdmin($user)) {
|
||||
session()->flash('error', 'You cannot delete the only admin');
|
||||
session()->flash('error', trans('errors.users_cannot_delete_only_admin'));
|
||||
return redirect($user->getEditUrl());
|
||||
}
|
||||
|
||||
if ($user->system_name === 'public') {
|
||||
session()->flash('error', trans('errors.users_cannot_delete_guest'));
|
||||
return redirect($user->getEditUrl());
|
||||
}
|
||||
|
||||
$this->userRepo->destroy($user);
|
||||
session()->flash('success', trans('settings.users_delete_success'));
|
||||
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http;
|
||||
<?php namespace BookStack\Http;
|
||||
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
@@ -9,15 +7,35 @@ class Kernel extends HttpKernel
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*
|
||||
* These middleware are run during every request to your application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middleware = [
|
||||
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
||||
\BookStack\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\BookStack\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\BookStack\Http\Middleware\TrimStrings::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\BookStack\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\BookStack\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\BookStack\Http\Middleware\Localization::class
|
||||
],
|
||||
'api' => [
|
||||
'throttle:60,1',
|
||||
'bindings',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -26,6 +44,7 @@ class Kernel extends HttpKernel
|
||||
* @var array
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'auth' => \BookStack\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
|
||||
@@ -4,8 +4,6 @@ namespace BookStack\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use Setting;
|
||||
|
||||
class Authenticate
|
||||
{
|
||||
@@ -33,14 +31,14 @@ class Authenticate
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
|
||||
return redirect()->guest('/register/confirm/awaiting');
|
||||
return redirect(baseUrl('/register/confirm/awaiting'));
|
||||
}
|
||||
|
||||
if ($this->auth->guest() && !setting('app-public')) {
|
||||
if ($request->ajax()) {
|
||||
return response('Unauthorized.', 401);
|
||||
} else {
|
||||
return redirect()->guest('/login');
|
||||
return redirect()->guest(baseUrl('/login'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
app/Http/Middleware/Localization.php
Normal file
33
app/Http/Middleware/Localization.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php namespace BookStack\Http\Middleware;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Closure;
|
||||
|
||||
class Localization
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$defaultLang = config('app.locale');
|
||||
if (user()->isDefault()) {
|
||||
$locale = $defaultLang;
|
||||
$availableLocales = config('app.locales');
|
||||
foreach ($request->getLanguages() as $lang) {
|
||||
if (!in_array($lang, $availableLocales)) continue;
|
||||
$locale = $lang;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$locale = setting()->getUser(user(), 'language', $defaultLang);
|
||||
}
|
||||
app()->setLocale($locale);
|
||||
Carbon::setLocale($locale);
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
<?php namespace BookStack\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
@@ -34,7 +32,8 @@ class RedirectIfAuthenticated
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if ($this->auth->check()) {
|
||||
$requireConfirmation = setting('registration-confirmation');
|
||||
if ($this->auth->check() && (!$requireConfirmation || ($requireConfirmation && $this->auth->user()->email_confirmed))) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
|
||||
19
app/Http/Middleware/TrimStrings.php
Normal file
19
app/Http/Middleware/TrimStrings.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\TrimStrings as BaseTrimmer;
|
||||
|
||||
class TrimStrings extends BaseTrimmer
|
||||
{
|
||||
/**
|
||||
* The names of the attributes that should not be trimmed.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
'password',
|
||||
'password_confirmation',
|
||||
'password-confirm',
|
||||
];
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
||||
abstract class Job
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queueable Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This job base class provides a central location to place any logic that
|
||||
| is shared across all of your jobs. The trait included with the class
|
||||
| provides access to the "queueOn" and "delay" queue helper methods.
|
||||
|
|
||||
*/
|
||||
|
||||
use Queueable;
|
||||
}
|
||||
24
app/JointPermission.php
Normal file
24
app/JointPermission.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
class JointPermission extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get the role that this points to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function role()
|
||||
{
|
||||
return $this->belongsTo(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity this points to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphOne(Entity::class, 'entity');
|
||||
}
|
||||
}
|
||||
19
app/Model.php
Normal file
19
app/Model.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
||||
|
||||
class Model extends EloquentModel
|
||||
{
|
||||
|
||||
/**
|
||||
* Provides public access to get the raw attribute value from the model.
|
||||
* Used in areas where no mutations are required but performance is critical.
|
||||
* @param $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRawAttribute($key)
|
||||
{
|
||||
return parent::getAttributeFromArray($key);
|
||||
}
|
||||
|
||||
}
|
||||
53
app/Notifications/ConfirmEmail.php
Normal file
53
app/Notifications/ConfirmEmail.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class ConfirmEmail extends Notification implements ShouldQueue
|
||||
{
|
||||
|
||||
use Queueable;
|
||||
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
* @param string $token
|
||||
*/
|
||||
public function __construct($token)
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
$appName = ['appName' => setting('app-name')];
|
||||
return (new MailMessage)
|
||||
->subject(trans('auth.email_confirm_subject', $appName))
|
||||
->greeting(trans('auth.email_confirm_greeting', $appName))
|
||||
->line(trans('auth.email_confirm_text'))
|
||||
->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
|
||||
}
|
||||
|
||||
}
|
||||
51
app/Notifications/ResetPassword.php
Normal file
51
app/Notifications/ResetPassword.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Notifications;
|
||||
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class ResetPassword extends Notification
|
||||
{
|
||||
/**
|
||||
* The password reset token.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* Create a notification instance.
|
||||
*
|
||||
* @param string $token
|
||||
*/
|
||||
public function __construct($token)
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array|string
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the mail representation of the notification.
|
||||
*
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
public function toMail()
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')]))
|
||||
->line(trans('auth.email_reset_text'))
|
||||
->action(trans('auth.reset_password'), baseUrl('password/reset/' . $this->token))
|
||||
->line(trans('auth.email_reset_not_requested'));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
abstract class Ownable extends Model
|
||||
{
|
||||
@@ -10,7 +9,7 @@ abstract class Ownable extends Model
|
||||
*/
|
||||
public function createdBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'created_by');
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,7 +18,7 @@ abstract class Ownable extends Model
|
||||
*/
|
||||
public function updatedBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'updated_by');
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
74
app/Page.php
74
app/Page.php
@@ -1,8 +1,5 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Page extends Entity
|
||||
{
|
||||
@@ -10,6 +7,13 @@ class Page extends Entity
|
||||
|
||||
protected $simpleAttributes = ['name', 'id', 'slug'];
|
||||
|
||||
protected $with = ['book'];
|
||||
public $textField = 'text';
|
||||
|
||||
/**
|
||||
* Converts this page into a simplified array.
|
||||
* @return mixed
|
||||
*/
|
||||
public function toSimpleArray()
|
||||
{
|
||||
$array = array_intersect_key($this->toArray(), array_flip($this->simpleAttributes));
|
||||
@@ -17,38 +21,88 @@ class Page extends Entity
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the book this page sits in.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function book()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Book');
|
||||
return $this->belongsTo(Book::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chapter that this page is in, If applicable.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function chapter()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Chapter');
|
||||
return $this->belongsTo(Chapter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this page has a chapter.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChapter()
|
||||
{
|
||||
return $this->chapter()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated page revisions, ordered by created date.
|
||||
* @return mixed
|
||||
*/
|
||||
public function revisions()
|
||||
{
|
||||
return $this->hasMany('BookStack\PageRevision')->where('type', '=', 'version')->orderBy('created_at', 'desc');
|
||||
return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
/**
|
||||
* Get the attachments assigned to this page.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function attachments()
|
||||
{
|
||||
return $this->hasMany(Attachment::class, 'uploaded_to')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this page.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
||||
$midText = $this->draft ? '/draft/' : '/page/';
|
||||
$idComponent = $this->draft ? $this->id : $this->slug;
|
||||
return '/books/' . $bookSlug . $midText . $idComponent;
|
||||
$idComponent = $this->draft ? $this->id : urlencode($this->slug);
|
||||
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/'));
|
||||
}
|
||||
|
||||
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this page's content to the specified length.
|
||||
* @param int $length
|
||||
* @return mixed
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
$text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
|
||||
return mb_convert_encoding($text, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @param bool $withContent
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery($withContent = false)
|
||||
{ $htmlQuery = $withContent ? 'html' : "'' as html";
|
||||
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PageRevision extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'text', 'markdown'];
|
||||
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
|
||||
|
||||
/**
|
||||
* Get the user that created the page revision
|
||||
@@ -12,7 +11,7 @@ class PageRevision extends Model
|
||||
*/
|
||||
public function createdBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'created_by');
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,16 +20,43 @@ class PageRevision extends Model
|
||||
*/
|
||||
public function page()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Page');
|
||||
return $this->belongsTo(Page::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this revision.
|
||||
* @param null|string $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
public function getUrl($path = null)
|
||||
{
|
||||
return $this->page->getUrl() . '/revisions/' . $this->id;
|
||||
$url = $this->page->getUrl() . '/revisions/' . $this->id;
|
||||
if ($path) return $url . '/' . trim($path, '/');
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous revision for the same page if existing
|
||||
* @return \BookStack\PageRevision|null
|
||||
*/
|
||||
public function getPrevious()
|
||||
{
|
||||
if ($id = static::where('page_id', '=', $this->page_id)->where('id', '<', $this->id)->max('id')) {
|
||||
return static::find($id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows checking of the exact class, Used to check entity type.
|
||||
* Included here to align with entities in similar use cases.
|
||||
* (Yup, Bit of an awkward hack)
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
public static function isA($type)
|
||||
{
|
||||
return $type === 'revision';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<?php
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Services\SettingService;
|
||||
use BookStack\Setting;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use BookStack\User;
|
||||
use Validator;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -15,7 +14,18 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
// Custom validation methods
|
||||
Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
|
||||
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
|
||||
return in_array($value->getMimeType(), $imageMimes);
|
||||
});
|
||||
|
||||
\Blade::directive('icon', function($expression) {
|
||||
return "<?php echo icon($expression); ?>";
|
||||
});
|
||||
|
||||
// Allow longer string lengths after upgrade to utf8mb4
|
||||
\Schema::defaultStringLength(191);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,6 +35,8 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
$this->app->singleton(SettingService::class, function($app) {
|
||||
return new SettingService($app->make(Setting::class), $app->make('Illuminate\Contracts\Cache\Repository'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Auth;
|
||||
use BookStack\Services\LdapService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
@@ -25,7 +26,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
public function register()
|
||||
{
|
||||
Auth::provider('ldap', function($app, array $config) {
|
||||
return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']);
|
||||
return new LdapUserProvider($config['model'], $app[LdapService::class]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
26
app/Providers/BroadcastServiceProvider.php
Normal file
26
app/Providers/BroadcastServiceProvider.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
|
||||
class BroadcastServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
// Broadcast::routes();
|
||||
//
|
||||
// /*
|
||||
// * Authenticate the user's personal channel...
|
||||
// */
|
||||
// Broadcast::channel('BookStack.User.*', function ($user, $userId) {
|
||||
// return (int) $user->id === (int) $userId;
|
||||
// });
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,18 @@
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use BookStack\Activity;
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Services\ViewService;
|
||||
use BookStack\Setting;
|
||||
use BookStack\View;
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Illuminate\Contracts\Filesystem\Factory;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use BookStack\Services\ActivityService;
|
||||
use BookStack\Services\SettingService;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class CustomFacadeProvider extends ServiceProvider
|
||||
{
|
||||
@@ -29,30 +36,30 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
{
|
||||
$this->app->bind('activity', function() {
|
||||
return new ActivityService(
|
||||
$this->app->make('BookStack\Activity'),
|
||||
$this->app->make('BookStack\Services\RestrictionService')
|
||||
$this->app->make(Activity::class),
|
||||
$this->app->make(PermissionService::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('views', function() {
|
||||
return new ViewService(
|
||||
$this->app->make('BookStack\View'),
|
||||
$this->app->make('BookStack\Services\RestrictionService')
|
||||
$this->app->make(View::class),
|
||||
$this->app->make(PermissionService::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('setting', function() {
|
||||
return new SettingService(
|
||||
$this->app->make('BookStack\Setting'),
|
||||
$this->app->make('Illuminate\Contracts\Cache\Repository')
|
||||
$this->app->make(Setting::class),
|
||||
$this->app->make(Repository::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('images', function() {
|
||||
return new ImageService(
|
||||
$this->app->make('Intervention\Image\ImageManager'),
|
||||
$this->app->make('Illuminate\Contracts\Filesystem\Factory'),
|
||||
$this->app->make('Illuminate\Contracts\Cache\Repository')
|
||||
$this->app->make(ImageManager::class),
|
||||
$this->app->make(Factory::class),
|
||||
$this->app->make(Repository::class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -13,21 +14,19 @@ class EventServiceProvider extends ServiceProvider
|
||||
* @var array
|
||||
*/
|
||||
protected $listen = [
|
||||
'BookStack\Events\SomeEvent' => [
|
||||
'BookStack\Listeners\EventListener',
|
||||
SocialiteWasCalled::class => [
|
||||
'SocialiteProviders\Slack\SlackExtendSocialite@handle',
|
||||
'SocialiteProviders\Azure\AzureExtendSocialite@handle',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any other events for your application.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Events\Dispatcher $events
|
||||
* @return void
|
||||
*/
|
||||
public function boot(DispatcherContract $events)
|
||||
public function boot()
|
||||
{
|
||||
parent::boot($events);
|
||||
|
||||
//
|
||||
parent::boot();
|
||||
}
|
||||
}
|
||||
|
||||
35
app/Providers/PaginationServiceProvider.php
Normal file
35
app/Providers/PaginationServiceProvider.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
|
||||
use Illuminate\Pagination\PaginationServiceProvider as IlluminatePaginationServiceProvider;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
|
||||
class PaginationServiceProvider extends IlluminatePaginationServiceProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
Paginator::viewFactoryResolver(function () {
|
||||
return $this->app['view'];
|
||||
});
|
||||
|
||||
Paginator::currentPathResolver(function () {
|
||||
return baseUrl($this->app['request']->path());
|
||||
});
|
||||
|
||||
Paginator::currentPageResolver(function ($pageName = 'page') {
|
||||
$page = $this->app['request']->input($pageName);
|
||||
|
||||
if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -19,26 +20,54 @@ class RouteServiceProvider extends ServiceProvider
|
||||
/**
|
||||
* Define your route model bindings, pattern filters, etc.
|
||||
*
|
||||
* @param \Illuminate\Routing\Router $router
|
||||
* @return void
|
||||
*/
|
||||
public function boot(Router $router)
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
|
||||
parent::boot($router);
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
*
|
||||
* @param \Illuminate\Routing\Router $router
|
||||
* @return void
|
||||
*/
|
||||
public function map(Router $router)
|
||||
public function map()
|
||||
{
|
||||
$router->group(['namespace' => $this->namespace], function ($router) {
|
||||
require app_path('Http/routes.php');
|
||||
$this->mapWebRoutes();
|
||||
// $this->mapApiRoutes();
|
||||
}
|
||||
/**
|
||||
* Define the "web" routes for the application.
|
||||
*
|
||||
* These routes all receive session state, CSRF protection, etc.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function mapWebRoutes()
|
||||
{
|
||||
Route::group([
|
||||
'middleware' => 'web',
|
||||
'namespace' => $this->namespace,
|
||||
], function ($router) {
|
||||
require base_path('routes/web.php');
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Define the "api" routes for the application.
|
||||
*
|
||||
* These routes are typically stateless.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function mapApiRoutes()
|
||||
{
|
||||
Route::group([
|
||||
'middleware' => 'api',
|
||||
'namespace' => $this->namespace,
|
||||
'prefix' => 'api',
|
||||
], function ($router) {
|
||||
require base_path('routes/api.php');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class SocialiteServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Indicates if loading of the provider is deferred.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $defer = true;
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->bindShared('Laravel\Socialite\Contracts\Factory', function ($app) {
|
||||
return new SocialiteManager($app);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides()
|
||||
{
|
||||
return ['Laravel\Socialite\Contracts\Factory'];
|
||||
}
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Book;
|
||||
use Views;
|
||||
|
||||
class BookRepo extends EntityRepo
|
||||
{
|
||||
protected $pageRepo;
|
||||
protected $chapterRepo;
|
||||
|
||||
/**
|
||||
* BookRepo constructor.
|
||||
* @param PageRepo $pageRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Base query for getting books.
|
||||
* Takes into account any restrictions.
|
||||
* @return mixed
|
||||
*/
|
||||
private function bookQuery()
|
||||
{
|
||||
return $this->restrictionService->enforceBookRestrictions($this->book, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the book that has the given id.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->bookQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all books, Limited by count.
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAll($count = 10)
|
||||
{
|
||||
$bookQuery = $this->bookQuery()->orderBy('name', 'asc');
|
||||
if (!$count) return $bookQuery->get();
|
||||
return $bookQuery->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all books paginated.
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAllPaginated($count = 10)
|
||||
{
|
||||
return $this->bookQuery()
|
||||
->orderBy('name', 'asc')->paginate($count);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the latest books.
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLatest($count = 10)
|
||||
{
|
||||
return $this->bookQuery()->orderBy('created_at', 'desc')->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most recently viewed for a user.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyViewed($count = 10, $page = 0)
|
||||
{
|
||||
return Views::getUserRecentlyViewed($count, $page, $this->book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most viewed books.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPopular($count = 10, $page = 0)
|
||||
{
|
||||
return Views::getPopular($count, $page, $this->book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a book by slug
|
||||
* @param $slug
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug)
|
||||
{
|
||||
$book = $this->bookQuery()->where('slug', '=', $slug)->first();
|
||||
if ($book === null) throw new NotFoundException('Book not found');
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a book exists.
|
||||
* @param $id
|
||||
* @return bool
|
||||
*/
|
||||
public function exists($id)
|
||||
{
|
||||
return $this->bookQuery()->where('id', '=', $id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new book instance from request input.
|
||||
* @param $input
|
||||
* @return Book
|
||||
*/
|
||||
public function newFromInput($input)
|
||||
{
|
||||
return $this->book->newInstance($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a book identified by the given slug.
|
||||
* @param $bookSlug
|
||||
*/
|
||||
public function destroyBySlug($bookSlug)
|
||||
{
|
||||
$book = $this->getBySlug($bookSlug);
|
||||
foreach ($book->pages as $page) {
|
||||
$this->pageRepo->destroy($page);
|
||||
}
|
||||
foreach ($book->chapters as $chapter) {
|
||||
$this->chapterRepo->destroy($chapter);
|
||||
}
|
||||
$book->views()->delete();
|
||||
$book->restrictions()->delete();
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next child element priority.
|
||||
* @param Book $book
|
||||
* @return int
|
||||
*/
|
||||
public function getNewPriority($book)
|
||||
{
|
||||
$lastElem = $this->getChildren($book)->pop();
|
||||
return $lastElem ? $lastElem->priority + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slug
|
||||
* @param bool|false $currentId
|
||||
* @return bool
|
||||
*/
|
||||
public function doesSlugExist($slug, $currentId = false)
|
||||
{
|
||||
$query = $this->book->where('slug', '=', $slug);
|
||||
if ($currentId) {
|
||||
$query = $query->where('id', '!=', $currentId);
|
||||
}
|
||||
return $query->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a suitable slug for the given book name.
|
||||
* Ensures the returned slug is unique in the system.
|
||||
* @param string $name
|
||||
* @param bool|false $currentId
|
||||
* @return string
|
||||
*/
|
||||
public function findSuitableSlug($name, $currentId = false)
|
||||
{
|
||||
$originalSlug = Str::slug($name);
|
||||
$slug = $originalSlug;
|
||||
$count = 2;
|
||||
while ($this->doesSlugExist($slug, $currentId)) {
|
||||
$slug = $originalSlug . '-' . $count;
|
||||
$count++;
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all child objects of a book.
|
||||
* Returns a sorted collection of Pages and Chapters.
|
||||
* Loads the bookslug onto child elements to prevent access database access for getting the slug.
|
||||
* @param Book $book
|
||||
* @param bool $filterDrafts
|
||||
* @return mixed
|
||||
*/
|
||||
public function getChildren(Book $book, $filterDrafts = false)
|
||||
{
|
||||
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
|
||||
$pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
|
||||
|
||||
if ($filterDrafts) {
|
||||
$pageQuery = $pageQuery->where('draft', '=', false);
|
||||
}
|
||||
|
||||
$pages = $pageQuery->get();
|
||||
|
||||
$chapterQuery = $book->chapters()->with(['pages' => function($query) use ($filterDrafts) {
|
||||
$this->restrictionService->enforcePageRestrictions($query, 'view');
|
||||
if ($filterDrafts) $query->where('draft', '=', false);
|
||||
}]);
|
||||
$chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
|
||||
$chapters = $chapterQuery->get();
|
||||
$children = $pages->merge($chapters);
|
||||
$bookSlug = $book->slug;
|
||||
|
||||
$children->each(function ($child) use ($bookSlug) {
|
||||
$child->setAttribute('bookSlug', $bookSlug);
|
||||
if ($child->isA('chapter')) {
|
||||
$child->pages->each(function ($page) use ($bookSlug) {
|
||||
$page->setAttribute('bookSlug', $bookSlug);
|
||||
});
|
||||
$child->pages = $child->pages->sortBy(function($child, $key) {
|
||||
$score = $child->priority;
|
||||
if ($child->draft) $score -= 100;
|
||||
return $score;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sort items with drafts first then by priority.
|
||||
return $children->sortBy(function($child, $key) {
|
||||
$score = $child->priority;
|
||||
if ($child->isA('page') && $child->draft) $score -= 100;
|
||||
return $score;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get books by search term.
|
||||
* @param $term
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term, $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$books = $this->restrictionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms))
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
foreach ($books as $book) {
|
||||
//highlight
|
||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $book->getExcerpt(100));
|
||||
$book->searchSnippet = $result;
|
||||
}
|
||||
return $books;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use Activity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Chapter;
|
||||
|
||||
class ChapterRepo extends EntityRepo
|
||||
{
|
||||
/**
|
||||
* Base query for getting chapters, Takes restrictions into account.
|
||||
* @return mixed
|
||||
*/
|
||||
private function chapterQuery()
|
||||
{
|
||||
return $this->restrictionService->enforceChapterRestrictions($this->chapter, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an id exists.
|
||||
* @param $id
|
||||
* @return bool
|
||||
*/
|
||||
public function idExists($id)
|
||||
{
|
||||
return $this->chapterQuery()->where('id', '=', $id)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chapter by a specific id.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->chapterQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all chapters.
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
return $this->chapterQuery()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chapter that has the given slug within the given book.
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug, $bookId)
|
||||
{
|
||||
$chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
if ($chapter === null) throw new NotFoundException('Chapter not found');
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the child items for a chapter
|
||||
* @param Chapter $chapter
|
||||
*/
|
||||
public function getChildren(Chapter $chapter)
|
||||
{
|
||||
$pages = $this->restrictionService->enforcePageRestrictions($chapter->pages())->get();
|
||||
// Sort items with drafts first then by priority.
|
||||
return $pages->sortBy(function($child, $key) {
|
||||
$score = $child->priority;
|
||||
if ($child->draft) $score -= 100;
|
||||
return $score;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new chapter from request input.
|
||||
* @param $input
|
||||
* @return $this
|
||||
*/
|
||||
public function newFromInput($input)
|
||||
{
|
||||
return $this->chapter->fill($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a chapter and its relations by providing its slug.
|
||||
* @param Chapter $chapter
|
||||
*/
|
||||
public function destroy(Chapter $chapter)
|
||||
{
|
||||
if (count($chapter->pages) > 0) {
|
||||
foreach ($chapter->pages as $page) {
|
||||
$page->chapter_id = 0;
|
||||
$page->save();
|
||||
}
|
||||
}
|
||||
Activity::removeEntity($chapter);
|
||||
$chapter->views()->delete();
|
||||
$chapter->restrictions()->delete();
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a chapter's slug exists.
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @param bool|false $currentId
|
||||
* @return bool
|
||||
*/
|
||||
public function doesSlugExist($slug, $bookId, $currentId = false)
|
||||
{
|
||||
$query = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId);
|
||||
if ($currentId) {
|
||||
$query = $query->where('id', '!=', $currentId);
|
||||
}
|
||||
return $query->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a suitable slug for the provided name.
|
||||
* Checks database to prevent duplicate slugs.
|
||||
* @param $name
|
||||
* @param $bookId
|
||||
* @param bool|false $currentId
|
||||
* @return string
|
||||
*/
|
||||
public function findSuitableSlug($name, $bookId, $currentId = false)
|
||||
{
|
||||
$slug = Str::slug($name);
|
||||
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new priority value for a new page to be added
|
||||
* to the given chapter.
|
||||
* @param Chapter $chapter
|
||||
* @return int
|
||||
*/
|
||||
public function getNewPriority(Chapter $chapter)
|
||||
{
|
||||
$lastPage = $chapter->pages->last();
|
||||
return $lastPage !== null ? $lastPage->priority + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chapters by the given search term.
|
||||
* @param string $term
|
||||
* @param array $whereTerms
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$chapters = $this->restrictionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms))
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
foreach ($chapters as $chapter) {
|
||||
//highlight
|
||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $chapter->getExcerpt(100));
|
||||
$chapter->searchSnippet = $result;
|
||||
}
|
||||
return $chapters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the book relation of this chapter.
|
||||
* @param $bookId
|
||||
* @param Chapter $chapter
|
||||
* @return Chapter
|
||||
*/
|
||||
public function changeBook($bookId, Chapter $chapter)
|
||||
{
|
||||
$chapter->book_id = $bookId;
|
||||
foreach ($chapter->activity as $activity) {
|
||||
$activity->book_id = $bookId;
|
||||
$activity->save();
|
||||
}
|
||||
$chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id);
|
||||
$chapter->save();
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
}
|
||||
87
app/Repos/CommentRepo.php
Normal file
87
app/Repos/CommentRepo.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use BookStack\Comment;
|
||||
use BookStack\Entity;
|
||||
|
||||
/**
|
||||
* Class CommentRepo
|
||||
* @package BookStack\Repos
|
||||
*/
|
||||
class CommentRepo {
|
||||
|
||||
/**
|
||||
* @var Comment $comment
|
||||
*/
|
||||
protected $comment;
|
||||
|
||||
/**
|
||||
* CommentRepo constructor.
|
||||
* @param Comment $comment
|
||||
*/
|
||||
public function __construct(Comment $comment)
|
||||
{
|
||||
$this->comment = $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a comment by ID.
|
||||
* @param $id
|
||||
* @return Comment|\Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->comment->newQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new comment on an entity.
|
||||
* @param Entity $entity
|
||||
* @param array $data
|
||||
* @return Comment
|
||||
*/
|
||||
public function create (Entity $entity, $data = [])
|
||||
{
|
||||
$userId = user()->id;
|
||||
$comment = $this->comment->newInstance($data);
|
||||
$comment->created_by = $userId;
|
||||
$comment->updated_by = $userId;
|
||||
$comment->local_id = $this->getNextLocalId($entity);
|
||||
$entity->comments()->save($comment);
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing comment.
|
||||
* @param Comment $comment
|
||||
* @param array $input
|
||||
* @return mixed
|
||||
*/
|
||||
public function update($comment, $input)
|
||||
{
|
||||
$comment->updated_by = user()->id;
|
||||
$comment->update($input);
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a comment from the system.
|
||||
* @param Comment $comment
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete($comment)
|
||||
{
|
||||
return $comment->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next local ID relative to the linked entity.
|
||||
* @param Entity $entity
|
||||
* @return int
|
||||
*/
|
||||
protected function getNextLocalId(Entity $entity)
|
||||
{
|
||||
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
|
||||
if ($comments === null) return 1;
|
||||
return $comments->local_id + 1;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,8 @@
|
||||
use BookStack\Image;
|
||||
use BookStack\Page;
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Services\RestrictionService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Setting;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
@@ -13,21 +14,21 @@ class ImageRepo
|
||||
|
||||
protected $image;
|
||||
protected $imageService;
|
||||
protected $restictionService;
|
||||
protected $restrictionService;
|
||||
protected $page;
|
||||
|
||||
/**
|
||||
* ImageRepo constructor.
|
||||
* @param Image $image
|
||||
* @param ImageService $imageService
|
||||
* @param RestrictionService $restrictionService
|
||||
* @param PermissionService $permissionService
|
||||
* @param Page $page
|
||||
*/
|
||||
public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService, Page $page)
|
||||
public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page)
|
||||
{
|
||||
$this->image = $image;
|
||||
$this->imageService = $imageService;
|
||||
$this->restictionService = $restrictionService;
|
||||
$this->restrictionService = $permissionService;
|
||||
$this->page = $page;
|
||||
}
|
||||
|
||||
@@ -52,7 +53,7 @@ class ImageRepo
|
||||
*/
|
||||
private function returnPaginated($query, $page = 0, $pageSize = 24)
|
||||
{
|
||||
$images = $this->restictionService->filterRelatedPages($query, 'images', 'uploaded_to');
|
||||
$images = $this->restrictionService->filterRelatedPages($query, 'images', 'uploaded_to');
|
||||
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
|
||||
$hasMore = count($images) > $pageSize;
|
||||
|
||||
@@ -191,7 +192,12 @@ class ImageRepo
|
||||
*/
|
||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||
{
|
||||
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
||||
try {
|
||||
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
||||
} catch (FileNotFoundException $exception) {
|
||||
$image->delete();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,607 +0,0 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Carbon\Carbon;
|
||||
use DOMDocument;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Page;
|
||||
use BookStack\PageRevision;
|
||||
|
||||
class PageRepo extends EntityRepo
|
||||
{
|
||||
|
||||
protected $pageRevision;
|
||||
|
||||
/**
|
||||
* PageRepo constructor.
|
||||
* @param PageRevision $pageRevision
|
||||
*/
|
||||
public function __construct(PageRevision $pageRevision)
|
||||
{
|
||||
$this->pageRevision = $pageRevision;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Base query for getting pages, Takes restrictions into account.
|
||||
* @param bool $allowDrafts
|
||||
* @return mixed
|
||||
*/
|
||||
private function pageQuery($allowDrafts = false)
|
||||
{
|
||||
$query = $this->restrictionService->enforcePageRestrictions($this->page, 'view');
|
||||
if (!$allowDrafts) {
|
||||
$query = $query->where('draft', '=', false);
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page via a specific ID.
|
||||
* @param $id
|
||||
* @param bool $allowDrafts
|
||||
* @return mixed
|
||||
*/
|
||||
public function getById($id, $allowDrafts = false)
|
||||
{
|
||||
return $this->pageQuery($allowDrafts)->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page identified by the given slug.
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug, $bookId)
|
||||
{
|
||||
$page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
if ($page === null) throw new NotFoundException('Page not found');
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through page revisions and retrieve
|
||||
* the last page in the current book that
|
||||
* has a slug equal to the one given.
|
||||
* @param $pageSlug
|
||||
* @param $bookSlug
|
||||
* @return null | Page
|
||||
*/
|
||||
public function findPageUsingOldSlug($pageSlug, $bookSlug)
|
||||
{
|
||||
$revision = $this->pageRevision->where('slug', '=', $pageSlug)
|
||||
->whereHas('page', function ($query) {
|
||||
$this->restrictionService->enforcePageRestrictions($query);
|
||||
})
|
||||
->where('type', '=', 'version')
|
||||
->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
|
||||
->with('page')->first();
|
||||
return $revision !== null ? $revision->page : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new Page instance from the given input.
|
||||
* @param $input
|
||||
* @return Page
|
||||
*/
|
||||
public function newFromInput($input)
|
||||
{
|
||||
$page = $this->page->fill($input);
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the pages with a particular slug within a book.
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
*/
|
||||
public function countBySlug($slug, $bookId)
|
||||
{
|
||||
return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new page into the system.
|
||||
* Input validation must be done beforehand.
|
||||
* @param array $input
|
||||
* @param Book $book
|
||||
* @param int $chapterId
|
||||
* @return Page
|
||||
*/
|
||||
public function saveNew(array $input, Book $book, $chapterId = null)
|
||||
{
|
||||
$page = $this->newFromInput($input);
|
||||
$page->slug = $this->findSuitableSlug($page->name, $book->id);
|
||||
|
||||
if ($chapterId) $page->chapter_id = $chapterId;
|
||||
|
||||
$page->html = $this->formatHtml($input['html']);
|
||||
$page->text = strip_tags($page->html);
|
||||
$page->created_by = auth()->user()->id;
|
||||
$page->updated_by = auth()->user()->id;
|
||||
|
||||
$book->pages()->save($page);
|
||||
return $page;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function publishDraft(Page $draftPage, array $input)
|
||||
{
|
||||
$draftPage->fill($input);
|
||||
|
||||
$draftPage->slug = $this->findSuitableSlug($draftPage->name, $draftPage->book->id);
|
||||
$draftPage->html = $this->formatHtml($input['html']);
|
||||
$draftPage->text = strip_tags($draftPage->html);
|
||||
$draftPage->draft = false;
|
||||
|
||||
$draftPage->save();
|
||||
return $draftPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new draft page instance.
|
||||
* @param Book $book
|
||||
* @param Chapter|bool $chapter
|
||||
* @return static
|
||||
*/
|
||||
public function getDraftPage(Book $book, $chapter = false)
|
||||
{
|
||||
$page = $this->page->newInstance();
|
||||
$page->name = 'New Page';
|
||||
$page->created_by = auth()->user()->id;
|
||||
$page->updated_by = auth()->user()->id;
|
||||
$page->draft = true;
|
||||
|
||||
if ($chapter) $page->chapter_id = $chapter->id;
|
||||
|
||||
$book->pages()->save($page);
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a page's html to be tagged correctly
|
||||
* within the system.
|
||||
* @param string $htmlText
|
||||
* @return string
|
||||
*/
|
||||
protected function formatHtml($htmlText)
|
||||
{
|
||||
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;
|
||||
|
||||
// Ensure no duplicate ids are used
|
||||
$idArray = [];
|
||||
|
||||
foreach ($childNodes as $index => $childNode) {
|
||||
/** @var \DOMElement $childNode */
|
||||
if (get_class($childNode) !== 'DOMElement') continue;
|
||||
|
||||
// Overwrite id if not a BookStack custom id
|
||||
if ($childNode->hasAttribute('id')) {
|
||||
$id = $childNode->getAttribute('id');
|
||||
if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
|
||||
$idArray[] = $id;
|
||||
continue;
|
||||
};
|
||||
}
|
||||
|
||||
// Create an unique id for the element
|
||||
// Uses the content as a basis to ensure output is the same every time
|
||||
// the same content is passed through.
|
||||
$contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
|
||||
$newId = urlencode($contentId);
|
||||
$loopIndex = 0;
|
||||
while (in_array($newId, $idArray)) {
|
||||
$newId = urlencode($contentId . '-' . $loopIndex);
|
||||
$loopIndex++;
|
||||
}
|
||||
|
||||
$childNode->setAttribute('id', $newId);
|
||||
$idArray[] = $newId;
|
||||
}
|
||||
|
||||
// Generate inner html as a string
|
||||
$html = '';
|
||||
foreach ($childNodes as $childNode) {
|
||||
$html .= $doc->saveHTML($childNode);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets pages by a search term.
|
||||
* Highlights page content for showing in results.
|
||||
* @param string $term
|
||||
* @param array $whereTerms
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$pages = $this->restrictionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms))
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
|
||||
// Add highlights to page text.
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
//lookahead/behind assertions ensures cut between words
|
||||
$s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
|
||||
|
||||
foreach ($pages as $page) {
|
||||
preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
|
||||
//delimiter between occurrences
|
||||
$results = [];
|
||||
foreach ($matches as $line) {
|
||||
$results[] = htmlspecialchars($line[0], 0, 'UTF-8');
|
||||
}
|
||||
$matchLimit = 6;
|
||||
if (count($results) > $matchLimit) {
|
||||
$results = array_slice($results, 0, $matchLimit);
|
||||
}
|
||||
$result = join('... ', $results);
|
||||
|
||||
//highlight
|
||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
|
||||
if (strlen($result) < 5) {
|
||||
$result = $page->getExcerpt(80);
|
||||
}
|
||||
$page->searchSnippet = $result;
|
||||
}
|
||||
return $pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for image usage.
|
||||
* @param $imageString
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchForImage($imageString)
|
||||
{
|
||||
$pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get();
|
||||
foreach ($pages as $page) {
|
||||
$page->url = $page->getUrl();
|
||||
$page->html = '';
|
||||
$page->text = '';
|
||||
}
|
||||
return count($pages) > 0 ? $pages : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a page with any fillable data and saves it into the database.
|
||||
* @param Page $page
|
||||
* @param int $book_id
|
||||
* @param string $input
|
||||
* @return Page
|
||||
*/
|
||||
public function updatePage(Page $page, $book_id, $input)
|
||||
{
|
||||
// Save a revision before updating
|
||||
if ($page->html !== $input['html'] || $page->name !== $input['name']) {
|
||||
$this->saveRevision($page);
|
||||
}
|
||||
|
||||
// Prevent slug being updated if no name change
|
||||
if ($page->name !== $input['name']) {
|
||||
$page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
|
||||
}
|
||||
|
||||
// Update with new details
|
||||
$userId = auth()->user()->id;
|
||||
$page->fill($input);
|
||||
$page->html = $this->formatHtml($input['html']);
|
||||
$page->text = strip_tags($page->html);
|
||||
if (setting('app-editor') !== 'markdown') $page->markdown = '';
|
||||
$page->updated_by = $userId;
|
||||
$page->save();
|
||||
|
||||
// Remove all update drafts for this user & page.
|
||||
$this->userUpdateDraftsQuery($page, $userId)->delete();
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a revision's content back into a page.
|
||||
* @param Page $page
|
||||
* @param Book $book
|
||||
* @param int $revisionId
|
||||
* @return Page
|
||||
*/
|
||||
public function restoreRevision(Page $page, Book $book, $revisionId)
|
||||
{
|
||||
$this->saveRevision($page);
|
||||
$revision = $this->getRevisionById($revisionId);
|
||||
$page->fill($revision->toArray());
|
||||
$page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id);
|
||||
$page->text = strip_tags($page->html);
|
||||
$page->updated_by = auth()->user()->id;
|
||||
$page->save();
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a page revision into the system.
|
||||
* @param Page $page
|
||||
* @return $this
|
||||
*/
|
||||
public function saveRevision(Page $page)
|
||||
{
|
||||
$revision = $this->pageRevision->fill($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 = auth()->user()->id;
|
||||
$revision->created_at = $page->updated_at;
|
||||
$revision->type = 'version';
|
||||
$revision->save();
|
||||
// Clear old revisions
|
||||
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
|
||||
$this->pageRevision->where('page_id', '=', $page->id)
|
||||
->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
|
||||
}
|
||||
return $revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a page update draft.
|
||||
* @param Page $page
|
||||
* @param array $data
|
||||
* @return PageRevision
|
||||
*/
|
||||
public function saveUpdateDraft(Page $page, $data = [])
|
||||
{
|
||||
$userId = auth()->user()->id;
|
||||
$drafts = $this->userUpdateDraftsQuery($page, $userId)->get();
|
||||
|
||||
if ($drafts->count() > 0) {
|
||||
$draft = $drafts->first();
|
||||
} else {
|
||||
$draft = $this->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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a draft page.
|
||||
* @param Page $page
|
||||
* @param array $data
|
||||
* @return Page
|
||||
*/
|
||||
public function updateDraftPage(Page $page, $data = [])
|
||||
{
|
||||
$page->fill($data);
|
||||
|
||||
if (isset($data['html'])) {
|
||||
$page->text = strip_tags($data['html']);
|
||||
}
|
||||
|
||||
$page->save();
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for getting user update drafts.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return mixed
|
||||
*/
|
||||
private function userUpdateDraftsQuery(Page $page, $userId)
|
||||
{
|
||||
return $this->pageRevision->where('created_by', '=', $userId)
|
||||
->where('type', 'update_draft')
|
||||
->where('page_id', '=', $page->id)
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user has a draft version of a particular page or not.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return bool
|
||||
*/
|
||||
public function hasUserGotPageDraft(Page $page, $userId)
|
||||
{
|
||||
return $this->userUpdateDraftsQuery($page, $userId)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest updated draft revision for a particular page and user.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUserPageDraft(Page $page, $userId)
|
||||
{
|
||||
return $this->userUpdateDraftsQuery($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 = 'You are currently editing a draft that was last saved ' . $draft->updated_at->diffForHumans() . '.';
|
||||
if ($draft->page->updated_at->timestamp > $draft->updated_at->timestamp) {
|
||||
$message .= "\n This page has been updated by since that time. It is recommended that you discard this draft.";
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 null $minRange
|
||||
* @return bool
|
||||
*/
|
||||
public function isPageEditingActive(Page $page, $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 null $minRange
|
||||
* @return string
|
||||
*/
|
||||
public function getPageEditingActiveMessage(Page $page, $minRange = null)
|
||||
{
|
||||
$pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
|
||||
$userMessage = $pageDraftEdits->count() > 1 ? $pageDraftEdits->count() . ' users have' : $pageDraftEdits->first()->createdBy->name . ' has';
|
||||
$timeMessage = $minRange === null ? 'since the page was last updated' : 'in the last ' . $minRange . ' minutes';
|
||||
$message = '%s started editing this page %s. Take care not to overwrite each other\'s updates!';
|
||||
return sprintf($message, $userMessage, $timeMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* A query to check for active update drafts on a particular page.
|
||||
* @param Page $page
|
||||
* @param null $minRange
|
||||
* @return mixed
|
||||
*/
|
||||
private function activePageEditingQuery(Page $page, $minRange = null)
|
||||
{
|
||||
$query = $this->pageRevision->where('type', '=', 'update_draft')
|
||||
->where('page_id', '=', $page->id)
|
||||
->where('updated_at', '>', $page->updated_at)
|
||||
->where('created_by', '!=', auth()->user()->id)
|
||||
->with('createdBy');
|
||||
|
||||
if ($minRange !== null) {
|
||||
$query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single revision via it's id.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRevisionById($id)
|
||||
{
|
||||
return $this->pageRevision->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a slug exists within a book already.
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @param bool|false $currentId
|
||||
* @return bool
|
||||
*/
|
||||
public function doesSlugExist($slug, $bookId, $currentId = false)
|
||||
{
|
||||
$query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId);
|
||||
if ($currentId) $query = $query->where('id', '!=', $currentId);
|
||||
return $query->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the related book for the specified page.
|
||||
* Changes the book id of any relations to the page that store the book id.
|
||||
* @param int $bookId
|
||||
* @param Page $page
|
||||
* @return Page
|
||||
*/
|
||||
public function changeBook($bookId, Page $page)
|
||||
{
|
||||
$page->book_id = $bookId;
|
||||
foreach ($page->activity as $activity) {
|
||||
$activity->book_id = $bookId;
|
||||
$activity->save();
|
||||
}
|
||||
$page->slug = $this->findSuitableSlug($page->name, $bookId, $page->id);
|
||||
$page->save();
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a suitable slug for the resource
|
||||
* @param $name
|
||||
* @param $bookId
|
||||
* @param bool|false $currentId
|
||||
* @return string
|
||||
*/
|
||||
public function findSuitableSlug($name, $bookId, $currentId = false)
|
||||
{
|
||||
$slug = Str::slug($name);
|
||||
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a given page along with its dependencies.
|
||||
* @param $page
|
||||
*/
|
||||
public function destroy($page)
|
||||
{
|
||||
Activity::removeEntity($page);
|
||||
$page->views()->delete();
|
||||
$page->revisions()->delete();
|
||||
$page->restrictions()->delete();
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest pages added to the system.
|
||||
* @param $count
|
||||
*/
|
||||
public function getRecentlyCreatedPaginated($count = 20)
|
||||
{
|
||||
return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest pages added to the system.
|
||||
* @param $count
|
||||
*/
|
||||
public function getRecentlyUpdatedPaginated($count = 20)
|
||||
{
|
||||
return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Permission;
|
||||
use BookStack\RolePermission;
|
||||
use BookStack\Role;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Setting;
|
||||
|
||||
class PermissionsRepo
|
||||
@@ -11,16 +12,21 @@ class PermissionsRepo
|
||||
|
||||
protected $permission;
|
||||
protected $role;
|
||||
protected $permissionService;
|
||||
|
||||
protected $systemRoles = ['admin', 'public'];
|
||||
|
||||
/**
|
||||
* PermissionsRepo constructor.
|
||||
* @param $permission
|
||||
* @param $role
|
||||
* @param RolePermission $permission
|
||||
* @param Role $role
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(Permission $permission, Role $role)
|
||||
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
|
||||
{
|
||||
$this->permission = $permission;
|
||||
$this->role = $role;
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +75,7 @@ class PermissionsRepo
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
return $role;
|
||||
}
|
||||
|
||||
@@ -77,20 +84,23 @@ class PermissionsRepo
|
||||
* Ensure Admin role always has all permissions.
|
||||
* @param $roleId
|
||||
* @param $roleData
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function updateRole($roleId, $roleData)
|
||||
{
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
if ($role->name === 'admin') {
|
||||
if ($role->system_name === 'admin') {
|
||||
$permissions = $this->permission->all()->pluck('id')->toArray();
|
||||
$role->permissions()->sync($permissions);
|
||||
}
|
||||
|
||||
$role->fill($roleData);
|
||||
$role->save();
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,10 +132,10 @@ class PermissionsRepo
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
// Prevent deleting admin role or default registration role.
|
||||
if ($role->name === 'admin') {
|
||||
throw new PermissionsException('The admin role cannot be deleted');
|
||||
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
|
||||
throw new PermissionsException(trans('errors.role_system_cannot_be_deleted'));
|
||||
} else if ($role->id == setting('registration-role')) {
|
||||
throw new PermissionsException('This role cannot be deleted while set as the default registration role.');
|
||||
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
|
||||
}
|
||||
|
||||
if ($migrateRoleId) {
|
||||
@@ -136,6 +146,7 @@ class PermissionsRepo
|
||||
}
|
||||
}
|
||||
|
||||
$this->permissionService->deleteJointPermissionsForRole($role);
|
||||
$role->delete();
|
||||
}
|
||||
|
||||
|
||||
136
app/Repos/TagRepo.php
Normal file
136
app/Repos/TagRepo.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use BookStack\Tag;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Services\PermissionService;
|
||||
|
||||
/**
|
||||
* Class TagRepo
|
||||
* @package BookStack\Repos
|
||||
*/
|
||||
class TagRepo
|
||||
{
|
||||
|
||||
protected $tag;
|
||||
protected $entity;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* TagRepo constructor.
|
||||
* @param Tag $attr
|
||||
* @param Entity $ent
|
||||
* @param PermissionService $ps
|
||||
*/
|
||||
public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
|
||||
{
|
||||
$this->tag = $attr;
|
||||
$this->entity = $ent;
|
||||
$this->permissionService = $ps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance of its particular type.
|
||||
* @param $entityType
|
||||
* @param $entityId
|
||||
* @param string $action
|
||||
* @return \Illuminate\Database\Eloquent\Model|null|static
|
||||
*/
|
||||
public function getEntity($entityType, $entityId, $action = 'view')
|
||||
{
|
||||
$entityInstance = $this->entity->getEntityInstance($entityType);
|
||||
$searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
|
||||
$searchQuery = $this->permissionService->enforceEntityRestrictions($entityType, $searchQuery, $action);
|
||||
return $searchQuery->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags for a particular entity.
|
||||
* @param string $entityType
|
||||
* @param int $entityId
|
||||
* @return mixed
|
||||
*/
|
||||
public function getForEntity($entityType, $entityId)
|
||||
{
|
||||
$entity = $this->getEntity($entityType, $entityId);
|
||||
if ($entity === null) return collect();
|
||||
|
||||
return $entity->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag name suggestions from scanning existing tag names.
|
||||
* If no search term is given the 50 most popular tag names are provided.
|
||||
* @param $searchTerm
|
||||
* @return array
|
||||
*/
|
||||
public function getNameSuggestions($searchTerm = false)
|
||||
{
|
||||
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
|
||||
} else {
|
||||
$query = $query->orderBy('count', 'desc')->take(50);
|
||||
}
|
||||
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
return $query->get(['name'])->pluck('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag value suggestions from scanning existing tag values.
|
||||
* If no search is given the 50 most popular values are provided.
|
||||
* Passing a tagName will only find values for a tags with a particular name.
|
||||
* @param $searchTerm
|
||||
* @param $tagName
|
||||
* @return array
|
||||
*/
|
||||
public function getValueSuggestions($searchTerm = false, $tagName = false)
|
||||
{
|
||||
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
|
||||
} else {
|
||||
$query = $query->orderBy('count', 'desc')->take(50);
|
||||
}
|
||||
|
||||
if ($tagName !== false) $query = $query->where('name', '=', $tagName);
|
||||
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
return $query->get(['value'])->pluck('value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an array of tags to an entity
|
||||
* @param Entity $entity
|
||||
* @param array $tags
|
||||
* @return array|\Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function saveTagsToEntity(Entity $entity, $tags = [])
|
||||
{
|
||||
$entity->tags()->delete();
|
||||
$newTags = [];
|
||||
foreach ($tags as $tag) {
|
||||
if (trim($tag['name']) === '') continue;
|
||||
$newTags[] = $this->newInstanceFromInput($tag);
|
||||
}
|
||||
|
||||
return $entity->tags()->saveMany($newTags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Tag instance from user input.
|
||||
* @param $input
|
||||
* @return Tag
|
||||
*/
|
||||
protected function newInstanceFromInput($input)
|
||||
{
|
||||
$name = trim($input['name']);
|
||||
$value = isset($input['value']) ? trim($input['value']) : '';
|
||||
// Any other modification or cleanup required can go here
|
||||
$values = ['name' => $name, 'value' => $value];
|
||||
return $this->tag->newInstance($values);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use Setting;
|
||||
use Exception;
|
||||
|
||||
class UserRepo
|
||||
{
|
||||
@@ -51,6 +51,27 @@ class UserRepo
|
||||
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions in a paginated format.
|
||||
* @param int $count
|
||||
* @param $sortData
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function getAllUsersPaginatedAndSorted($count = 20, $sortData)
|
||||
{
|
||||
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
|
||||
|
||||
if ($sortData['search']) {
|
||||
$term = '%' . $sortData['search'] . '%';
|
||||
$query->where(function($query) use ($term) {
|
||||
$query->where('name', 'like', $term)
|
||||
->orWhere('email', 'like', $term);
|
||||
});
|
||||
}
|
||||
|
||||
return $query->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user and attaches a role to them.
|
||||
* @param array $data
|
||||
@@ -63,9 +84,14 @@ class UserRepo
|
||||
|
||||
// Get avatar from gravatar and save
|
||||
if (!config('services.disable_services')) {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
try {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
} catch (Exception $e) {
|
||||
$user->save();
|
||||
\Log::error('Failed to save user gravatar image');
|
||||
}
|
||||
}
|
||||
|
||||
return $user;
|
||||
@@ -142,13 +168,13 @@ class UserRepo
|
||||
public function getRecentlyCreated(User $user, $count = 20)
|
||||
{
|
||||
return [
|
||||
'pages' => $this->entityRepo->getRecentlyCreatedPages($count, 0, function ($query) use ($user) {
|
||||
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, function ($query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
}),
|
||||
'chapters' => $this->entityRepo->getRecentlyCreatedChapters($count, 0, function ($query) use ($user) {
|
||||
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, function ($query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
}),
|
||||
'books' => $this->entityRepo->getRecentlyCreatedBooks($count, 0, function ($query) use ($user) {
|
||||
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, function ($query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
})
|
||||
];
|
||||
@@ -168,6 +194,15 @@ class UserRepo
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles in the system that are assignable to a user.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAllRoles()
|
||||
{
|
||||
return $this->role->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the roles which can be given restricted access to
|
||||
* other entities in the system.
|
||||
@@ -175,7 +210,7 @@ class UserRepo
|
||||
*/
|
||||
public function getRestrictableRoles()
|
||||
{
|
||||
return $this->role->where('name', '!=', 'admin')->get();
|
||||
return $this->role->where('system_name', '!=', 'admin')->get();
|
||||
}
|
||||
|
||||
}
|
||||
61
app/Role.php
61
app/Role.php
@@ -1,8 +1,5 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
@@ -14,40 +11,54 @@ class Role extends Model
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\User');
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The permissions that belong to the role.
|
||||
* Get all related JointPermissions.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function jointPermissions()
|
||||
{
|
||||
return $this->hasMany(JointPermission::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The RolePermissions that belong to the role.
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Permission');
|
||||
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this role has a permission.
|
||||
* @param $permission
|
||||
* @param $permissionName
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPermission($permission)
|
||||
public function hasPermission($permissionName)
|
||||
{
|
||||
return $this->permissions->pluck('name')->contains($permission);
|
||||
$permissions = $this->getRelationValue('permissions');
|
||||
foreach ($permissions as $permission) {
|
||||
if ($permission->getRawAttribute('name') === $permissionName) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a permission to this role.
|
||||
* @param Permission $permission
|
||||
* @param RolePermission $permission
|
||||
*/
|
||||
public function attachPermission(Permission $permission)
|
||||
public function attachPermission(RolePermission $permission)
|
||||
{
|
||||
$this->permissions()->attach($permission->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach a single permission from this role.
|
||||
* @param Permission $permission
|
||||
* @param RolePermission $permission
|
||||
*/
|
||||
public function detachPermission(Permission $permission)
|
||||
public function detachPermission(RolePermission $permission)
|
||||
{
|
||||
$this->permissions()->detach($permission->id);
|
||||
}
|
||||
@@ -55,10 +66,30 @@ class Role extends Model
|
||||
/**
|
||||
* Get the role object for the specified role.
|
||||
* @param $roleName
|
||||
* @return mixed
|
||||
* @return Role
|
||||
*/
|
||||
public static function getRole($roleName)
|
||||
{
|
||||
return static::where('name', '=', $roleName)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role object for the specified system role.
|
||||
* @param $roleName
|
||||
* @return Role
|
||||
*/
|
||||
public static function getSystemRole($roleName)
|
||||
{
|
||||
return static::where('system_name', '=', $roleName)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible roles
|
||||
* @return mixed
|
||||
*/
|
||||
public static function visible()
|
||||
{
|
||||
return static::where('hidden', '=', false)->orderBy('name')->get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Permission extends Model
|
||||
class RolePermission extends Model
|
||||
{
|
||||
/**
|
||||
* The roles that belong to the permission.
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Role');
|
||||
return $this->belongsToMany(Role::class, 'permission_role','permission_id', 'role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permission object by name.
|
||||
* @param $roleName
|
||||
* @param $name
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getByName($name)
|
||||
18
app/SearchTerm.php
Normal file
18
app/SearchTerm.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
class SearchTerm extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get the entity that this term belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,18 +8,18 @@ class ActivityService
|
||||
{
|
||||
protected $activity;
|
||||
protected $user;
|
||||
protected $restrictionService;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* ActivityService constructor.
|
||||
* @param Activity $activity
|
||||
* @param RestrictionService $restrictionService
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(Activity $activity, RestrictionService $restrictionService)
|
||||
public function __construct(Activity $activity, PermissionService $permissionService)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
$this->restrictionService = $restrictionService;
|
||||
$this->user = auth()->user();
|
||||
$this->permissionService = $permissionService;
|
||||
$this->user = user();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,9 +88,9 @@ class ActivityService
|
||||
*/
|
||||
public function latest($count = 20, $page = 0)
|
||||
{
|
||||
$activityList = $this->restrictionService
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
|
||||
->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get();
|
||||
|
||||
return $this->filterSimilar($activityList);
|
||||
}
|
||||
@@ -105,8 +105,16 @@ class ActivityService
|
||||
*/
|
||||
public function entityActivity($entity, $count = 20, $page = 0)
|
||||
{
|
||||
$activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
if ($entity->isA('book')) {
|
||||
$query = $this->activity->where('book_id', '=', $entity->id);
|
||||
} else {
|
||||
$query = $this->activity->where('entity_type', '=', get_class($entity))
|
||||
->where('entity_id', '=', $entity->id);
|
||||
}
|
||||
|
||||
$activity = $this->permissionService
|
||||
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->with(['entity', 'user.avatar'])->skip($count * $page)->take($count)->get();
|
||||
|
||||
return $this->filterSimilar($activity);
|
||||
}
|
||||
@@ -121,7 +129,7 @@ class ActivityService
|
||||
*/
|
||||
public function userActivity($user, $count = 20, $page = 0)
|
||||
{
|
||||
$activityList = $this->restrictionService
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get();
|
||||
return $this->filterSimilar($activityList);
|
||||
|
||||
201
app/Services/AttachmentService.php
Normal file
201
app/Services/AttachmentService.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
use BookStack\Attachment;
|
||||
use Exception;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class AttachmentService extends UploadService
|
||||
{
|
||||
|
||||
/**
|
||||
* Get an attachment from storage.
|
||||
* @param Attachment $attachment
|
||||
* @return string
|
||||
*/
|
||||
public function getAttachmentFromStorage(Attachment $attachment)
|
||||
{
|
||||
$attachmentPath = $this->getStorageBasePath() . $attachment->path;
|
||||
return $this->getStorage()->get($attachmentPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new attachment upon user upload.
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @param int $page_id
|
||||
* @return Attachment
|
||||
* @throws FileUploadException
|
||||
*/
|
||||
public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
|
||||
{
|
||||
$attachmentName = $uploadedFile->getClientOriginalName();
|
||||
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
|
||||
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
|
||||
|
||||
$attachment = Attachment::forceCreate([
|
||||
'name' => $attachmentName,
|
||||
'path' => $attachmentPath,
|
||||
'extension' => $uploadedFile->getClientOriginalExtension(),
|
||||
'uploaded_to' => $page_id,
|
||||
'created_by' => user()->id,
|
||||
'updated_by' => user()->id,
|
||||
'order' => $largestExistingOrder + 1
|
||||
]);
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a upload, saving to a file and deleting any existing uploads
|
||||
* attached to that file.
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @param Attachment $attachment
|
||||
* @return Attachment
|
||||
* @throws FileUploadException
|
||||
*/
|
||||
public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment)
|
||||
{
|
||||
if (!$attachment->external) {
|
||||
$this->deleteFileInStorage($attachment);
|
||||
}
|
||||
|
||||
$attachmentName = $uploadedFile->getClientOriginalName();
|
||||
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
|
||||
|
||||
$attachment->name = $attachmentName;
|
||||
$attachment->path = $attachmentPath;
|
||||
$attachment->external = false;
|
||||
$attachment->extension = $uploadedFile->getClientOriginalExtension();
|
||||
$attachment->save();
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new File attachment from a given link and name.
|
||||
* @param string $name
|
||||
* @param string $link
|
||||
* @param int $page_id
|
||||
* @return Attachment
|
||||
*/
|
||||
public function saveNewFromLink($name, $link, $page_id)
|
||||
{
|
||||
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
|
||||
return Attachment::forceCreate([
|
||||
'name' => $name,
|
||||
'path' => $link,
|
||||
'external' => true,
|
||||
'extension' => '',
|
||||
'uploaded_to' => $page_id,
|
||||
'created_by' => user()->id,
|
||||
'updated_by' => user()->id,
|
||||
'order' => $largestExistingOrder + 1
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file storage base path, amended for storage type.
|
||||
* This allows us to keep a generic path in the database.
|
||||
* @return string
|
||||
*/
|
||||
private function getStorageBasePath()
|
||||
{
|
||||
return $this->isLocal() ? 'storage/' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the file ordering for a listing of attached files.
|
||||
* @param array $attachmentList
|
||||
* @param $pageId
|
||||
*/
|
||||
public function updateFileOrderWithinPage($attachmentList, $pageId)
|
||||
{
|
||||
foreach ($attachmentList as $index => $attachment) {
|
||||
Attachment::where('uploaded_to', '=', $pageId)->where('id', '=', $attachment['id'])->update(['order' => $index]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the details of a file.
|
||||
* @param Attachment $attachment
|
||||
* @param $requestData
|
||||
* @return Attachment
|
||||
*/
|
||||
public function updateFile(Attachment $attachment, $requestData)
|
||||
{
|
||||
$attachment->name = $requestData['name'];
|
||||
if (isset($requestData['link']) && trim($requestData['link']) !== '') {
|
||||
$attachment->path = $requestData['link'];
|
||||
if (!$attachment->external) {
|
||||
$this->deleteFileInStorage($attachment);
|
||||
$attachment->external = true;
|
||||
}
|
||||
}
|
||||
$attachment->save();
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a File from the database and storage.
|
||||
* @param Attachment $attachment
|
||||
*/
|
||||
public function deleteFile(Attachment $attachment)
|
||||
{
|
||||
if ($attachment->external) {
|
||||
$attachment->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->deleteFileInStorage($attachment);
|
||||
$attachment->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file from the filesystem it sits on.
|
||||
* Cleans any empty leftover folders.
|
||||
* @param Attachment $attachment
|
||||
*/
|
||||
protected function deleteFileInStorage(Attachment $attachment)
|
||||
{
|
||||
$storedFilePath = $this->getStorageBasePath() . $attachment->path;
|
||||
$storage = $this->getStorage();
|
||||
$dirPath = dirname($storedFilePath);
|
||||
|
||||
$storage->delete($storedFilePath);
|
||||
if (count($storage->allFiles($dirPath)) === 0) {
|
||||
$storage->deleteDirectory($dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a file in storage with the given filename
|
||||
* @param $attachmentName
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @return string
|
||||
* @throws FileUploadException
|
||||
*/
|
||||
protected function putFileInStorage($attachmentName, UploadedFile $uploadedFile)
|
||||
{
|
||||
$attachmentData = file_get_contents($uploadedFile->getRealPath());
|
||||
|
||||
$storage = $this->getStorage();
|
||||
$attachmentBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
|
||||
$storageBasePath = $this->getStorageBasePath() . $attachmentBasePath;
|
||||
|
||||
$uploadFileName = $attachmentName;
|
||||
while ($storage->exists($storageBasePath . $uploadFileName)) {
|
||||
$uploadFileName = str_random(3) . $uploadFileName;
|
||||
}
|
||||
|
||||
$attachmentPath = $attachmentBasePath . $uploadFileName;
|
||||
$attachmentStoragePath = $this->getStorageBasePath() . $attachmentPath;
|
||||
|
||||
try {
|
||||
$storage->put($attachmentStoragePath, $attachmentData);
|
||||
} catch (Exception $e) {
|
||||
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentStoragePath]));
|
||||
}
|
||||
return $attachmentPath;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,30 +1,27 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Notifications\ConfirmEmail;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Mail\Mailer;
|
||||
use Illuminate\Mail\Message;
|
||||
use BookStack\EmailConfirmation;
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Setting;
|
||||
use BookStack\User;
|
||||
use Illuminate\Database\Connection as Database;
|
||||
|
||||
class EmailConfirmationService
|
||||
{
|
||||
protected $mailer;
|
||||
protected $emailConfirmation;
|
||||
protected $db;
|
||||
protected $users;
|
||||
|
||||
/**
|
||||
* EmailConfirmationService constructor.
|
||||
* @param Mailer $mailer
|
||||
* @param EmailConfirmation $emailConfirmation
|
||||
* @param Database $db
|
||||
* @param UserRepo $users
|
||||
*/
|
||||
public function __construct(Mailer $mailer, EmailConfirmation $emailConfirmation)
|
||||
public function __construct(Database $db, UserRepo $users)
|
||||
{
|
||||
$this->mailer = $mailer;
|
||||
$this->emailConfirmation = $emailConfirmation;
|
||||
$this->db = $db;
|
||||
$this->users = $users;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,45 +33,59 @@ class EmailConfirmationService
|
||||
public function sendConfirmation(User $user)
|
||||
{
|
||||
if ($user->email_confirmed) {
|
||||
throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login');
|
||||
throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
|
||||
}
|
||||
|
||||
$this->deleteConfirmationsByUser($user);
|
||||
$token = $this->createEmailConfirmation($user);
|
||||
|
||||
$user->notify(new ConfirmEmail($token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new email confirmation in the database and returns the token.
|
||||
* @param User $user
|
||||
* @return string
|
||||
*/
|
||||
public function createEmailConfirmation(User $user)
|
||||
{
|
||||
$token = $this->getToken();
|
||||
$this->emailConfirmation->create([
|
||||
$this->db->table('email_confirmations')->insert([
|
||||
'user_id' => $user->id,
|
||||
'token' => $token,
|
||||
'token' => $token,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now()
|
||||
]);
|
||||
$this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) {
|
||||
$appName = setting('app-name', 'BookStack');
|
||||
$message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.');
|
||||
});
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an email confirmation by looking up the token,
|
||||
* Ensures the token has not expired.
|
||||
* @param string $token
|
||||
* @return EmailConfirmation
|
||||
* @return array|null|\stdClass
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function getEmailConfirmationFromToken($token)
|
||||
{
|
||||
$emailConfirmation = $this->emailConfirmation->where('token', '=', $token)->first();
|
||||
// If not found
|
||||
$emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first();
|
||||
|
||||
// If not found show error
|
||||
if ($emailConfirmation === null) {
|
||||
throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register');
|
||||
throw new UserRegistrationException(trans('errors.email_confirmation_invalid'), '/register');
|
||||
}
|
||||
|
||||
// If more than a day old
|
||||
if (Carbon::now()->subDay()->gt($emailConfirmation->created_at)) {
|
||||
$this->sendConfirmation($emailConfirmation->user);
|
||||
throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm');
|
||||
if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
|
||||
$user = $this->users->getById($emailConfirmation->user_id);
|
||||
$this->sendConfirmation($user);
|
||||
throw new UserRegistrationException(trans('errors.email_confirmation_expired'), '/register/confirm');
|
||||
}
|
||||
|
||||
$emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
|
||||
return $emailConfirmation;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete all email confirmations that belong to a user.
|
||||
* @param User $user
|
||||
@@ -82,7 +93,7 @@ class EmailConfirmationService
|
||||
*/
|
||||
public function deleteConfirmationsByUser(User $user)
|
||||
{
|
||||
return $this->emailConfirmation->where('user_id', '=', $user->id)->delete();
|
||||
return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +103,7 @@ class EmailConfirmationService
|
||||
protected function getToken()
|
||||
{
|
||||
$token = str_random(24);
|
||||
while ($this->emailConfirmation->where('token', '=', $token)->exists()) {
|
||||
while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) {
|
||||
$token = str_random(25);
|
||||
}
|
||||
return $token;
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Page;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
|
||||
class ExportService
|
||||
{
|
||||
|
||||
protected $entityRepo;
|
||||
|
||||
/**
|
||||
* ExportService constructor.
|
||||
* @param $entityRepo
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page to a self-contained HTML file.
|
||||
* Includes required CSS & image content. Images are base64 encoded into the HTML.
|
||||
@@ -14,22 +27,108 @@ class ExportService
|
||||
*/
|
||||
public function pageToContainedHtml(Page $page)
|
||||
{
|
||||
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
|
||||
$pageHtml = view('pages/export', ['page' => $page, 'css' => $cssContent])->render();
|
||||
$this->entityRepo->renderPage($page);
|
||||
$pageHtml = view('pages/export', [
|
||||
'page' => $page
|
||||
])->render();
|
||||
return $this->containHtml($pageHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page to a pdf file.
|
||||
* Convert a chapter to a self-contained HTML file.
|
||||
* @param Chapter $chapter
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function chapterToContainedHtml(Chapter $chapter)
|
||||
{
|
||||
$pages = $this->entityRepo->getChapterChildren($chapter);
|
||||
$pages->each(function($page) {
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
});
|
||||
$html = view('chapters/export', [
|
||||
'chapter' => $chapter,
|
||||
'pages' => $pages
|
||||
])->render();
|
||||
return $this->containHtml($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a book to a self-contained HTML file.
|
||||
* @param Book $book
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function bookToContainedHtml(Book $book)
|
||||
{
|
||||
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
|
||||
$html = view('books/export', [
|
||||
'book' => $book,
|
||||
'bookChildren' => $bookTree
|
||||
])->render();
|
||||
return $this->containHtml($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page to a PDF file.
|
||||
* @param Page $page
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function pageToPdf(Page $page)
|
||||
{
|
||||
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
|
||||
$pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render();
|
||||
$containedHtml = $this->containHtml($pageHtml);
|
||||
$pdf = \PDF::loadHTML($containedHtml);
|
||||
$this->entityRepo->renderPage($page);
|
||||
$html = view('pages/pdf', [
|
||||
'page' => $page
|
||||
])->render();
|
||||
return $this->htmlToPdf($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a chapter to a PDF file.
|
||||
* @param Chapter $chapter
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function chapterToPdf(Chapter $chapter)
|
||||
{
|
||||
$pages = $this->entityRepo->getChapterChildren($chapter);
|
||||
$pages->each(function($page) {
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
});
|
||||
$html = view('chapters/export', [
|
||||
'chapter' => $chapter,
|
||||
'pages' => $pages
|
||||
])->render();
|
||||
return $this->htmlToPdf($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a book to a PDF file
|
||||
* @param Book $book
|
||||
* @return string
|
||||
*/
|
||||
public function bookToPdf(Book $book)
|
||||
{
|
||||
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
|
||||
$html = view('books/export', [
|
||||
'book' => $book,
|
||||
'bookChildren' => $bookTree
|
||||
])->render();
|
||||
return $this->htmlToPdf($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert normal webpage HTML to a PDF.
|
||||
* @param $html
|
||||
* @return string
|
||||
*/
|
||||
protected function htmlToPdf($html)
|
||||
{
|
||||
$containedHtml = $this->containHtml($html);
|
||||
$useWKHTML = config('snappy.pdf.binary') !== false;
|
||||
if ($useWKHTML) {
|
||||
$pdf = \SnappyPDF::loadHTML($containedHtml);
|
||||
$pdf->setOption('print-media-type', true);
|
||||
} else {
|
||||
$pdf = \PDF::loadHTML($containedHtml);
|
||||
}
|
||||
return $pdf->output();
|
||||
}
|
||||
|
||||
@@ -37,6 +136,7 @@ class ExportService
|
||||
* Bundle of the contents of a html file to be self-contained.
|
||||
* @param $htmlContent
|
||||
* @return mixed|string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function containHtml($htmlContent)
|
||||
{
|
||||
@@ -48,14 +148,38 @@ class ExportService
|
||||
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
||||
$oldImgString = $imgMatch;
|
||||
$srcString = $imageTagsOutput[2][$index];
|
||||
if (strpos(trim($srcString), 'http') !== 0) {
|
||||
$pathString = public_path($srcString);
|
||||
$isLocal = strpos(trim($srcString), 'http') !== 0;
|
||||
if ($isLocal) {
|
||||
$pathString = public_path(trim($srcString, '/'));
|
||||
} else {
|
||||
$pathString = $srcString;
|
||||
}
|
||||
$imageContent = file_get_contents($pathString);
|
||||
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
||||
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
||||
|
||||
// Attempt to find local files even if url not absolute
|
||||
$base = baseUrl('/');
|
||||
if (strpos($srcString, $base) === 0) {
|
||||
$isLocal = true;
|
||||
$relString = str_replace($base, '', $srcString);
|
||||
$pathString = public_path(trim($relString, '/'));
|
||||
}
|
||||
|
||||
if ($isLocal && !file_exists($pathString)) continue;
|
||||
try {
|
||||
if ($isLocal) {
|
||||
$imageContent = file_get_contents($pathString);
|
||||
} else {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [CURLOPT_URL => $pathString, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
|
||||
$imageContent = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
if ($err) throw new \Exception("Image fetch failed, Received error: " . $err);
|
||||
}
|
||||
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
||||
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
||||
} catch (\ErrorException $e) {
|
||||
$newImageString = '';
|
||||
}
|
||||
$htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
|
||||
}
|
||||
}
|
||||
@@ -82,14 +206,14 @@ class ExportService
|
||||
|
||||
/**
|
||||
* Converts the page contents into simple plain text.
|
||||
* This method filters any bad looking content to
|
||||
* provide a nice final output.
|
||||
* This method filters any bad looking content to provide a nice final output.
|
||||
* @param Page $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function pageToPlainText(Page $page)
|
||||
{
|
||||
$text = $page->text;
|
||||
$html = $this->entityRepo->renderPage($page);
|
||||
$text = strip_tags($html);
|
||||
// Replace multiple spaces with single spaces
|
||||
$text = preg_replace('/\ {2,}/', ' ', $text);
|
||||
// Reduce multiple horrid whitespace characters.
|
||||
@@ -100,6 +224,40 @@ class ExportService
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a chapter into a plain text string.
|
||||
* @param Chapter $chapter
|
||||
* @return string
|
||||
*/
|
||||
public function chapterToPlainText(Chapter $chapter)
|
||||
{
|
||||
$text = $chapter->name . "\n\n";
|
||||
$text .= $chapter->description . "\n\n";
|
||||
foreach ($chapter->pages as $page) {
|
||||
$text .= $this->pageToPlainText($page);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a book into a plain text string.
|
||||
* @param Book $book
|
||||
* @return string
|
||||
*/
|
||||
public function bookToPlainText(Book $book)
|
||||
{
|
||||
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
|
||||
$text = $book->name . "\n\n";
|
||||
foreach ($bookTree as $bookChild) {
|
||||
if ($bookChild->isA('chapter')) {
|
||||
$text .= $this->chapterToPlainText($bookChild);
|
||||
} else {
|
||||
$text .= $this->pageToPlainText($bookChild);
|
||||
}
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,20 +9,13 @@ use Intervention\Image\ImageManager;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Setting;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImageService
|
||||
class ImageService extends UploadService
|
||||
{
|
||||
|
||||
protected $imageTool;
|
||||
protected $fileSystem;
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* @var FileSystemInstance
|
||||
*/
|
||||
protected $storageInstance;
|
||||
protected $storageUrl;
|
||||
|
||||
/**
|
||||
@@ -34,8 +27,8 @@ class ImageService
|
||||
public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
|
||||
{
|
||||
$this->imageTool = $imageTool;
|
||||
$this->fileSystem = $fileSystem;
|
||||
$this->cache = $cache;
|
||||
parent::__construct($fileSystem);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +59,7 @@ class ImageService
|
||||
{
|
||||
$imageName = $imageName ? $imageName : basename($url);
|
||||
$imageData = file_get_contents($url);
|
||||
if($imageData === false) throw new \Exception('Cannot get image from ' . $url);
|
||||
if($imageData === false) throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
|
||||
return $this->saveNew($imageName, $imageData, $type);
|
||||
}
|
||||
|
||||
@@ -88,6 +81,9 @@ class ImageService
|
||||
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
|
||||
|
||||
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
|
||||
|
||||
if ($this->isLocal()) $imagePath = '/public' . $imagePath;
|
||||
|
||||
while ($storage->exists($imagePath . $imageName)) {
|
||||
$imageName = str_random(3) . $imageName;
|
||||
}
|
||||
@@ -95,10 +91,13 @@ class ImageService
|
||||
|
||||
try {
|
||||
$storage->put($fullPath, $imageData);
|
||||
$storage->setVisibility($fullPath, 'public');
|
||||
} catch (Exception $e) {
|
||||
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
|
||||
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
|
||||
}
|
||||
|
||||
if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
|
||||
|
||||
$imageDetails = [
|
||||
'name' => $imageName,
|
||||
'path' => $fullPath,
|
||||
@@ -107,8 +106,8 @@ class ImageService
|
||||
'uploaded_to' => $uploadedTo
|
||||
];
|
||||
|
||||
if (auth()->user() && auth()->user()->id !== 0) {
|
||||
$userId = auth()->user()->id;
|
||||
if (user()->id !== 0) {
|
||||
$userId = user()->id;
|
||||
$imageDetails['created_by'] = $userId;
|
||||
$imageDetails['updated_by'] = $userId;
|
||||
}
|
||||
@@ -118,6 +117,16 @@ class ImageService
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage path, Dependant of storage type.
|
||||
* @param Image $image
|
||||
* @return mixed|string
|
||||
*/
|
||||
protected function getPath(Image $image)
|
||||
{
|
||||
return ($this->isLocal()) ? ('public/' . $image->path) : $image->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the thumbnail for an image.
|
||||
* If $keepRatio is true only the width will be used.
|
||||
@@ -134,7 +143,8 @@ class ImageService
|
||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||
{
|
||||
$thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
|
||||
$thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path);
|
||||
$imagePath = $this->getPath($image);
|
||||
$thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
|
||||
|
||||
if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
@@ -147,10 +157,10 @@ class ImageService
|
||||
}
|
||||
|
||||
try {
|
||||
$thumb = $this->imageTool->make($storage->get($image->path));
|
||||
$thumb = $this->imageTool->make($storage->get($imagePath));
|
||||
} catch (Exception $e) {
|
||||
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
||||
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
|
||||
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
@@ -167,6 +177,7 @@ class ImageService
|
||||
|
||||
$thumbData = (string)$thumb->encode();
|
||||
$storage->put($thumbFilePath, $thumbData);
|
||||
$storage->setVisibility($thumbFilePath, 'public');
|
||||
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
|
||||
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
@@ -181,8 +192,8 @@ class ImageService
|
||||
{
|
||||
$storage = $this->getStorage();
|
||||
|
||||
$imageFolder = dirname($image->path);
|
||||
$imageFileName = basename($image->path);
|
||||
$imageFolder = dirname($this->getPath($image));
|
||||
$imageFileName = basename($this->getPath($image));
|
||||
$allImages = collect($storage->allFiles($imageFolder));
|
||||
|
||||
$imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
|
||||
@@ -211,7 +222,7 @@ class ImageService
|
||||
public function saveUserGravatar(User $user, $size = 500)
|
||||
{
|
||||
$emailHash = md5(strtolower(trim($user->email)));
|
||||
$url = 'http://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
|
||||
$url = 'https://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
|
||||
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
|
||||
$image = $this->saveNewFromUrl($url, 'user', $imageName);
|
||||
$image->created_by = $user->id;
|
||||
@@ -220,35 +231,9 @@ class ImageService
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage that will be used for storing images.
|
||||
* @return FileSystemInstance
|
||||
*/
|
||||
private function getStorage()
|
||||
{
|
||||
if ($this->storageInstance !== null) return $this->storageInstance;
|
||||
|
||||
$storageType = config('filesystems.default');
|
||||
$this->storageInstance = $this->fileSystem->disk($storageType);
|
||||
|
||||
return $this->storageInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not a folder is empty.
|
||||
* @param $path
|
||||
* @return int
|
||||
*/
|
||||
private function isFolderEmpty($path)
|
||||
{
|
||||
$files = $this->getStorage()->files($path);
|
||||
$folders = $this->getStorage()->directories($path);
|
||||
return count($files) === 0 && count($folders) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a public facing url for an image by checking relevant environment variables.
|
||||
* @param $filePath
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
*/
|
||||
private function getPublicUrl($filePath)
|
||||
@@ -257,16 +242,24 @@ class ImageService
|
||||
$storageUrl = config('filesystems.url');
|
||||
|
||||
// Get the standard public s3 url if s3 is set as storage type
|
||||
// Uses the nice, short URL if bucket name has no periods in otherwise the longer
|
||||
// region-based url will be used to prevent http issues.
|
||||
if ($storageUrl == false && config('filesystems.default') === 's3') {
|
||||
$storageDetails = config('filesystems.disks.s3');
|
||||
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
||||
if (strpos($storageDetails['bucket'], '.') === false) {
|
||||
$storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
|
||||
} else {
|
||||
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->storageUrl = $storageUrl;
|
||||
}
|
||||
|
||||
return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath;
|
||||
if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath);
|
||||
|
||||
return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,17 @@ class Ldap
|
||||
return ldap_set_option($ldapConnection, $option, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the version number for the given ldap connection.
|
||||
* @param $ldapConnection
|
||||
* @param $version
|
||||
* @return bool
|
||||
*/
|
||||
public function setVersion($ldapConnection, $version)
|
||||
{
|
||||
return $this->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search LDAP tree using the provided filter.
|
||||
* @param resource $ldapConnection
|
||||
@@ -83,4 +94,4 @@ class Ldap
|
||||
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ class LdapService
|
||||
// Find user
|
||||
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
|
||||
$baseDn = $this->config['base_dn'];
|
||||
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', 'mail']);
|
||||
$emailAttr = $this->config['email_attribute'];
|
||||
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
|
||||
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
|
||||
if ($users['count'] === 0) return null;
|
||||
|
||||
$user = $users[0];
|
||||
@@ -49,7 +52,7 @@ class LdapService
|
||||
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
|
||||
'name' => $user['cn'][0],
|
||||
'dn' => $user['dn'],
|
||||
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
|
||||
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
|
||||
];
|
||||
}
|
||||
|
||||
@@ -94,7 +97,7 @@ class LdapService
|
||||
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
|
||||
}
|
||||
|
||||
if (!$ldapBind) throw new LdapException('LDAP access failed using ' . ($isAnonymous ? ' anonymous bind.' : ' given dn & pass details'));
|
||||
if (!$ldapBind) throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,20 +112,24 @@ class LdapService
|
||||
|
||||
// Check LDAP extension in installed
|
||||
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
|
||||
throw new LdapException('LDAP PHP extension not installed');
|
||||
throw new LdapException(trans('errors.ldap_extension_not_installed'));
|
||||
}
|
||||
|
||||
// Get port from server string if specified.
|
||||
// Get port from server string and protocol if specified.
|
||||
$ldapServer = explode(':', $this->config['server']);
|
||||
$ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
|
||||
$hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
|
||||
if (!$hasProtocol) array_unshift($ldapServer, '');
|
||||
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
|
||||
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
|
||||
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
|
||||
|
||||
if ($ldapConnection === false) {
|
||||
throw new LdapException('Cannot connect to ldap server, Initial connection failed');
|
||||
throw new LdapException(trans('errors.ldap_cannot_connect'));
|
||||
}
|
||||
|
||||
// Set any required options
|
||||
if ($this->config['version']) {
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']);
|
||||
$this->ldap->setVersion($ldapConnection, $this->config['version']);
|
||||
}
|
||||
|
||||
$this->ldapConnection = $ldapConnection;
|
||||
|
||||
714
app/Services/PermissionService.php
Normal file
714
app/Services/PermissionService.php
Normal file
@@ -0,0 +1,714 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\EntityPermission;
|
||||
use BookStack\JointPermission;
|
||||
use BookStack\Ownable;
|
||||
use BookStack\Page;
|
||||
use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PermissionService
|
||||
{
|
||||
|
||||
protected $currentAction;
|
||||
protected $isAdminUser;
|
||||
protected $userRoles = false;
|
||||
protected $currentUserModel = false;
|
||||
|
||||
public $book;
|
||||
public $chapter;
|
||||
public $page;
|
||||
|
||||
protected $db;
|
||||
|
||||
protected $jointPermission;
|
||||
protected $role;
|
||||
protected $entityPermission;
|
||||
|
||||
protected $entityCache;
|
||||
|
||||
/**
|
||||
* PermissionService constructor.
|
||||
* @param JointPermission $jointPermission
|
||||
* @param EntityPermission $entityPermission
|
||||
* @param Connection $db
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param Role $role
|
||||
*/
|
||||
public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->jointPermission = $jointPermission;
|
||||
$this->entityPermission = $entityPermission;
|
||||
$this->role = $role;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
// TODO - Update so admin still goes through filters
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the database connection
|
||||
* @param Connection $connection
|
||||
*/
|
||||
public function setConnection(Connection $connection)
|
||||
{
|
||||
$this->db = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the local entity cache and ensure it's empty
|
||||
*/
|
||||
protected function readyEntityCache()
|
||||
{
|
||||
$this->entityCache = [
|
||||
'books' => collect(),
|
||||
'chapters' => collect()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a book via ID, Checks local cache
|
||||
* @param $bookId
|
||||
* @return Book
|
||||
*/
|
||||
protected function getBook($bookId)
|
||||
{
|
||||
if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
|
||||
return $this->entityCache['books']->get($bookId);
|
||||
}
|
||||
|
||||
$book = $this->book->find($bookId);
|
||||
if ($book === null) $book = false;
|
||||
if (isset($this->entityCache['books'])) {
|
||||
$this->entityCache['books']->put($bookId, $book);
|
||||
}
|
||||
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chapter via ID, Checks local cache
|
||||
* @param $chapterId
|
||||
* @return Book
|
||||
*/
|
||||
protected function getChapter($chapterId)
|
||||
{
|
||||
if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
|
||||
return $this->entityCache['chapters']->get($chapterId);
|
||||
}
|
||||
|
||||
$chapter = $this->chapter->find($chapterId);
|
||||
if ($chapter === null) $chapter = false;
|
||||
if (isset($this->entityCache['chapters'])) {
|
||||
$this->entityCache['chapters']->put($chapterId, $chapter);
|
||||
}
|
||||
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles for the current user;
|
||||
* @return array|bool
|
||||
*/
|
||||
protected function getRoles()
|
||||
{
|
||||
if ($this->userRoles !== false) return $this->userRoles;
|
||||
|
||||
$roles = [];
|
||||
|
||||
if (auth()->guest()) {
|
||||
$roles[] = $this->role->getSystemRole('public')->id;
|
||||
return $roles;
|
||||
}
|
||||
|
||||
|
||||
foreach ($this->currentUser()->roles as $role) {
|
||||
$roles[] = $role->id;
|
||||
}
|
||||
return $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-generate all entity permission from scratch.
|
||||
*/
|
||||
public function buildJointPermissions()
|
||||
{
|
||||
$this->jointPermission->truncate();
|
||||
$this->readyEntityCache();
|
||||
|
||||
// Get all roles (Should be the most limited dimension)
|
||||
$roles = $this->role->with('permissions')->get()->all();
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a query for fetching a book with it's children.
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
protected function bookFetchQuery()
|
||||
{
|
||||
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id']);
|
||||
}, 'pages' => function($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
|
||||
}]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build joint permissions for an array of books
|
||||
* @param Collection $books
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
*/
|
||||
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false) {
|
||||
$entities = clone $books;
|
||||
|
||||
/** @var Book $book */
|
||||
foreach ($books->all() as $book) {
|
||||
foreach ($book->getRelation('chapters') as $chapter) {
|
||||
$entities->push($chapter);
|
||||
}
|
||||
foreach ($book->getRelation('pages') as $page) {
|
||||
$entities->push($page);
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteOld) $this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function buildJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
$entities = [$entity];
|
||||
if ($entity->isA('book')) {
|
||||
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
|
||||
$this->buildJointPermissionsForBooks($books, $this->role->newQuery()->get(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
$entities[] = $entity->book;
|
||||
if ($entity->isA('page') && $entity->chapter_id) $entities[] = $entity->chapter;
|
||||
if ($entity->isA('chapter')) {
|
||||
foreach ($entity->pages as $page) {
|
||||
$entities[] = $page;
|
||||
}
|
||||
}
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->buildJointPermissionsForEntities(collect($entities));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a collection of entities.
|
||||
* @param Collection $entities
|
||||
*/
|
||||
public function buildJointPermissionsForEntities(Collection $entities)
|
||||
{
|
||||
$roles = $this->role->newQuery()->get();
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the entity jointPermissions for a particular role.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function buildJointPermissionForRole(Role $role)
|
||||
{
|
||||
$roles = [$role];
|
||||
$this->deleteManyJointPermissionsForRoles($roles);
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions attached to a particular role.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function deleteJointPermissionsForRole(Role $role)
|
||||
{
|
||||
$this->deleteManyJointPermissionsForRoles([$role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
* @param Role[] $roles
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForRoles($roles)
|
||||
{
|
||||
$roleIds = array_map(function($role) {
|
||||
return $role->id;
|
||||
}, $roles);
|
||||
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function deleteJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
$this->deleteManyJointPermissionsForEntities([$entity]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForEntities($entities)
|
||||
{
|
||||
if (count($entities) === 0) return;
|
||||
|
||||
$this->db->transaction(function() use ($entities) {
|
||||
|
||||
foreach (array_chunk($entities, 1000) as $entityChunk) {
|
||||
$query = $this->db->table('joint_permissions');
|
||||
foreach ($entityChunk as $entity) {
|
||||
$query->orWhere(function(QueryBuilder $query) use ($entity) {
|
||||
$query->where('entity_id', '=', $entity->id)
|
||||
->where('entity_type', '=', $entity->getMorphClass());
|
||||
});
|
||||
}
|
||||
$query->delete();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & Save entity jointPermissions for many entities and jointPermissions.
|
||||
* @param Collection $entities
|
||||
* @param array $roles
|
||||
*/
|
||||
protected function createManyJointPermissions($entities, $roles)
|
||||
{
|
||||
$this->readyEntityCache();
|
||||
$jointPermissions = [];
|
||||
|
||||
// Fetch Entity Permissions and create a mapping of entity restricted statuses
|
||||
$entityRestrictedMap = [];
|
||||
$permissionFetch = $this->entityPermission->newQuery();
|
||||
foreach ($entities as $entity) {
|
||||
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
|
||||
$permissionFetch->orWhere(function($query) use ($entity) {
|
||||
$query->where('restrictable_id', '=', $entity->id)->where('restrictable_type', '=', $entity->getMorphClass());
|
||||
});
|
||||
}
|
||||
$permissions = $permissionFetch->get();
|
||||
|
||||
// Create a mapping of explicit entity permissions
|
||||
$permissionMap = [];
|
||||
foreach ($permissions as $permission) {
|
||||
$key = $permission->restrictable_type . ':' . $permission->restrictable_id . ':' . $permission->role_id . ':' . $permission->action;
|
||||
$isRestricted = $entityRestrictedMap[$permission->restrictable_type . ':' . $permission->restrictable_id];
|
||||
$permissionMap[$key] = $isRestricted;
|
||||
}
|
||||
|
||||
// Create a mapping of role permissions
|
||||
$rolePermissionMap = [];
|
||||
foreach ($roles as $role) {
|
||||
foreach ($role->getRelationValue('permissions') as $permission) {
|
||||
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Joint Permission Data
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($roles as $role) {
|
||||
foreach ($this->getActions($entity) as $action) {
|
||||
$jointPermissions[] = $this->createJointPermissionData($entity, $role, $action, $permissionMap, $rolePermissionMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->transaction(function() use ($jointPermissions) {
|
||||
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
|
||||
$this->db->table('joint_permissions')->insert($jointPermissionChunk);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the actions related to an entity.
|
||||
* @param Entity $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';
|
||||
return $baseActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create entity permission data for an entity and role
|
||||
* for a particular action.
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param string $action
|
||||
* @param array $permissionMap
|
||||
* @param array $rolePermissionMap
|
||||
* @return array
|
||||
*/
|
||||
protected function createJointPermissionData(Entity $entity, Role $role, $action, $permissionMap, $rolePermissionMap)
|
||||
{
|
||||
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
|
||||
$roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
|
||||
$roleHasPermissionOwn = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-own']);
|
||||
$explodedAction = explode('-', $action);
|
||||
$restrictionAction = end($explodedAction);
|
||||
|
||||
if ($role->system_name === 'admin') {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
|
||||
}
|
||||
|
||||
if ($entity->restricted) {
|
||||
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
|
||||
}
|
||||
|
||||
// For chapters and pages, Check if explicit permissions are set on the Book.
|
||||
$book = $this->getBook($entity->book_id);
|
||||
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $role, $restrictionAction);
|
||||
$hasPermissiveAccessToParents = !$book->restricted;
|
||||
|
||||
// For pages with a chapter, Check if explicit permissions are set on the Chapter
|
||||
if ($entity->isA('page') && $entity->chapter_id !== 0 && $entity->chapter_id !== '0') {
|
||||
$chapter = $this->getChapter($entity->chapter_id);
|
||||
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
|
||||
if ($chapter->restricted) {
|
||||
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $role, $restrictionAction);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action,
|
||||
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
|
||||
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an active restriction in an entity map.
|
||||
* @param $entityMap
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action) {
|
||||
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
|
||||
return isset($entityMap[$key]) ? $entityMap[$key] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of data with the information of an entity jointPermissions.
|
||||
* Used to build data for bulk insertion.
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @param $permissionAll
|
||||
* @param $permissionOwn
|
||||
* @return array
|
||||
*/
|
||||
protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
|
||||
{
|
||||
return [
|
||||
'role_id' => $role->getRawAttribute('id'),
|
||||
'entity_id' => $entity->getRawAttribute('id'),
|
||||
'entity_type' => $entity->getMorphClass(),
|
||||
'action' => $action,
|
||||
'has_permission' => $permissionAll,
|
||||
'has_permission_own' => $permissionOwn,
|
||||
'created_by' => $entity->getRawAttribute('created_by')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entity has a restriction set upon it.
|
||||
* @param Ownable $ownable
|
||||
* @param $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function checkOwnableUserAccess(Ownable $ownable, $permission)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return true;
|
||||
}
|
||||
|
||||
$explodedPermission = explode('-', $permission);
|
||||
|
||||
$baseQuery = $ownable->where('id', '=', $ownable->id);
|
||||
$action = end($explodedPermission);
|
||||
$this->currentAction = $action;
|
||||
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
|
||||
|
||||
// Handle non entity specific jointPermissions
|
||||
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
||||
$allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
|
||||
$ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
|
||||
$this->currentAction = 'view';
|
||||
$isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
|
||||
return ($allPermission || ($isOwner && $ownPermission));
|
||||
}
|
||||
|
||||
// Handle abnormal create jointPermissions
|
||||
if ($action === 'create') {
|
||||
$this->currentAction = $permission;
|
||||
}
|
||||
|
||||
$q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
|
||||
$this->clean();
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has restrictions set on itself or its
|
||||
* parent tree.
|
||||
* @param Entity $entity
|
||||
* @param $action
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function checkIfRestrictionsSet(Entity $entity, $action)
|
||||
{
|
||||
$this->currentAction = $action;
|
||||
if ($entity->isA('page')) {
|
||||
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
return $entity->restricted || $entity->book->restricted;
|
||||
} elseif ($entity->isA('book')) {
|
||||
return $entity->restricted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The general query filter to remove all entities
|
||||
* that the current user does not have access to.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
protected function entityRestrictionQuery($query)
|
||||
{
|
||||
$q = $query->where(function ($parentQuery) {
|
||||
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
|
||||
$permissionQuery->whereIn('role_id', $this->getRoles())
|
||||
->where('action', '=', $this->currentAction)
|
||||
->where(function ($query) {
|
||||
$query->where('has_permission', '=', true)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
$this->clean();
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the children of a book in an efficient single query, Filtered by the permission system.
|
||||
* @param integer $book_id
|
||||
* @param bool $filterDrafts
|
||||
* @param bool $fetchPageContent
|
||||
* @return 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);
|
||||
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
|
||||
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
|
||||
|
||||
if (!$this->isAdmin()) {
|
||||
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
|
||||
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
|
||||
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
|
||||
->where(function($query) {
|
||||
$query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
|
||||
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
|
||||
}
|
||||
|
||||
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions for a generic entity
|
||||
* @param string $entityType
|
||||
* @param Builder|Entity $query
|
||||
* @param string $action
|
||||
* @return Builder
|
||||
*/
|
||||
public function enforceEntityRestrictions($entityType, $query, $action = 'view')
|
||||
{
|
||||
if (strtolower($entityType) === 'page') {
|
||||
// Prevent drafts being visible to others.
|
||||
$query = $query->where(function ($query) {
|
||||
$query->where('draft', '=', false);
|
||||
if ($this->currentUser()) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = $action;
|
||||
return $this->entityRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set as a polymorphic relation.
|
||||
* @param $query
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
* @param string $entityTypeColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
|
||||
->where('action', '=', $this->currentAction)
|
||||
->whereIn('role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('has_permission', '=', true)->orWhere(function ($query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
$this->clean();
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters pages that are a direct relation to another item.
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $entityIdColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails) {
|
||||
$query->where(function ($query) use (&$tableDetails) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where('entity_type', '=', 'Bookstack\\Page')
|
||||
->where('action', '=', $this->currentAction)
|
||||
->whereIn('role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('has_permission', '=', true)->orWhere(function ($query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||
});
|
||||
$this->clean();
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user is an admin.
|
||||
* @return bool
|
||||
*/
|
||||
private function isAdmin()
|
||||
{
|
||||
if ($this->isAdminUser === null) {
|
||||
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
|
||||
}
|
||||
|
||||
return $this->isAdminUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user
|
||||
* @return User
|
||||
*/
|
||||
private function currentUser()
|
||||
{
|
||||
if ($this->currentUserModel === false) {
|
||||
$this->currentUserModel = user();
|
||||
}
|
||||
|
||||
return $this->currentUserModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the cached user elements.
|
||||
*/
|
||||
private function clean()
|
||||
{
|
||||
$this->currentUserModel = false;
|
||||
$this->userRoles = false;
|
||||
$this->isAdminUser = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Entity;
|
||||
|
||||
class RestrictionService
|
||||
{
|
||||
|
||||
protected $userRoles;
|
||||
protected $isAdmin;
|
||||
protected $currentAction;
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* RestrictionService constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->currentUser = auth()->user();
|
||||
$this->userRoles = $this->currentUser ? $this->currentUser->roles->pluck('id') : [];
|
||||
$this->isAdmin = $this->currentUser ? $this->currentUser->hasRole('admin') : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entity has a restriction set upon it.
|
||||
* @param Entity $entity
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
public function checkIfEntityRestricted(Entity $entity, $action)
|
||||
{
|
||||
if ($this->isAdmin) return true;
|
||||
$this->currentAction = $action;
|
||||
$baseQuery = $entity->where('id', '=', $entity->id);
|
||||
if ($entity->isA('page')) {
|
||||
return $this->pageRestrictionQuery($baseQuery)->count() > 0;
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
return $this->chapterRestrictionQuery($baseQuery)->count() > 0;
|
||||
} elseif ($entity->isA('book')) {
|
||||
return $this->bookRestrictionQuery($baseQuery)->count() > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has restrictions set on itself or its
|
||||
* parent tree.
|
||||
* @param Entity $entity
|
||||
* @param $action
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function checkIfRestrictionsSet(Entity $entity, $action)
|
||||
{
|
||||
$this->currentAction = $action;
|
||||
if ($entity->isA('page')) {
|
||||
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
return $entity->restricted || $entity->book->restricted;
|
||||
} elseif ($entity->isA('book')) {
|
||||
return $entity->restricted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions for a page query
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforcePageRestrictions($query, $action = 'view')
|
||||
{
|
||||
// Prevent drafts being visible to others.
|
||||
$query = $query->where(function ($query) {
|
||||
$query->where('draft', '=', false);
|
||||
if ($this->currentUser) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->pageRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for restricting pages.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
private function pageRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentWhereQuery) {
|
||||
|
||||
$parentWhereQuery
|
||||
// (Book & chapter & page) or (Book & page & NO CHAPTER) unrestricted
|
||||
->where(function ($query) {
|
||||
$query->where(function ($query) {
|
||||
$query->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')
|
||||
->where('restricted', '=', false);
|
||||
})->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->where('restricted', '=', false);
|
||||
})->where('restricted', '=', false);
|
||||
})->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)->where('chapter_id', '=', 0)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->where('restricted', '=', false);
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page unrestricted, Has no chapter & book has accepted restrictions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id');
|
||||
}, 'and', true)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page unrestricted, Has an unrestricted chapter & book has accepted restrictions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')->where('restricted', '=', false);
|
||||
})
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=pages.book_id')
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page unrestricted, Has a chapter with accepted permissions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')
|
||||
->where('restricted', '=', true)
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Page has accepted permissions
|
||||
->orWhereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'pages', 'Page');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add on permission restrictions to a chapter query.
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforceChapterRestrictions($query, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->chapterRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for restricting chapters.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
private function chapterRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentWhereQuery) {
|
||||
|
||||
$parentWhereQuery
|
||||
// Book & chapter unrestricted
|
||||
->where(function ($query) {
|
||||
$query->where('restricted', '=', false)->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=chapters.book_id')
|
||||
->where('restricted', '=', false);
|
||||
});
|
||||
})
|
||||
// Chapter unrestricted & book has accepted restrictions
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('books')
|
||||
->whereRaw('books.id=chapters.book_id')
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Chapter has accepted permissions
|
||||
->orWhereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions to a book query.
|
||||
* @param $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function enforceBookRestrictions($query, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = $action;
|
||||
return $this->bookRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for restricting books.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
private function bookRestrictionQuery($query)
|
||||
{
|
||||
return $query->where(function ($parentWhereQuery) {
|
||||
$parentWhereQuery
|
||||
->where('restricted', '=', false)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', true)->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set a a polymorphic relation.
|
||||
* @param $query
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
* @param string $entityTypeColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
return $query->where(function ($query) use ($tableDetails) {
|
||||
$query->where(function ($query) use (&$tableDetails) {
|
||||
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Page')
|
||||
->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->pageRestrictionQuery($query);
|
||||
});
|
||||
});
|
||||
})->orWhere(function ($query) use (&$tableDetails) {
|
||||
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Book')->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('books')->whereRaw('books.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->bookRestrictionQuery($query);
|
||||
});
|
||||
});
|
||||
})->orWhere(function ($query) use (&$tableDetails) {
|
||||
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Chapter')->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('chapters')->whereRaw('chapters.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->chapterRestrictionQuery($query);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters pages that are a direct relation to another item.
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $entityIdColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
||||
{
|
||||
if ($this->isAdmin) return $query;
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
return $query->where(function ($query) use (&$tableDetails) {
|
||||
$query->where(function ($query) use (&$tableDetails) {
|
||||
$query->whereExists(function ($query) use (&$tableDetails) {
|
||||
$query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where(function ($query) {
|
||||
$this->pageRestrictionQuery($query);
|
||||
});
|
||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The query to check the restrictions on an entity.
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $modelName
|
||||
*/
|
||||
private function checkRestrictionsQuery($query, $tableName, $modelName)
|
||||
{
|
||||
$query->select('*')->from('restrictions')
|
||||
->whereRaw('restrictions.restrictable_id=' . $tableName . '.id')
|
||||
->where('restrictions.restrictable_type', '=', 'BookStack\\' . $modelName)
|
||||
->where('restrictions.action', '=', $this->currentAction)
|
||||
->whereIn('restrictions.role_id', $this->userRoles);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
503
app/Services/SearchService.php
Normal file
503
app/Services/SearchService.php
Normal file
@@ -0,0 +1,503 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Page;
|
||||
use BookStack\SearchTerm;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SearchService
|
||||
{
|
||||
protected $searchTerm;
|
||||
protected $book;
|
||||
protected $chapter;
|
||||
protected $page;
|
||||
protected $db;
|
||||
protected $permissionService;
|
||||
protected $entities;
|
||||
|
||||
/**
|
||||
* Acceptable operators to be used in a query
|
||||
* @var array
|
||||
*/
|
||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||
|
||||
/**
|
||||
* SearchService constructor.
|
||||
* @param SearchTerm $searchTerm
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param Connection $db
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService)
|
||||
{
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
$this->db = $db;
|
||||
$this->entities = [
|
||||
'page' => $this->page,
|
||||
'chapter' => $this->chapter,
|
||||
'book' => $this->book
|
||||
];
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the database connection
|
||||
* @param Connection $connection
|
||||
*/
|
||||
public function setConnection(Connection $connection)
|
||||
{
|
||||
$this->db = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search all entities in the system.
|
||||
* @param string $searchString
|
||||
* @param string $entityType
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @return array[int, Collection];
|
||||
*/
|
||||
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$entityTypes = array_keys($this->entities);
|
||||
$entityTypesToSearch = $entityTypes;
|
||||
$results = collect();
|
||||
|
||||
if ($entityType !== 'all') {
|
||||
$entityTypesToSearch = $entityType;
|
||||
} else if (isset($terms['filters']['type'])) {
|
||||
$entityTypesToSearch = explode('|', $terms['filters']['type']);
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
|
||||
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);
|
||||
$results = $results->merge($search);
|
||||
}
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'count' => count($results),
|
||||
'results' => $results->sortByDesc('score')->values()
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search a book for entities
|
||||
* @param integer $bookId
|
||||
* @param string $searchString
|
||||
* @return Collection
|
||||
*/
|
||||
public function searchBook($bookId, $searchString)
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$entityTypes = ['page', 'chapter'];
|
||||
$entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes;
|
||||
|
||||
$results = collect();
|
||||
foreach ($entityTypesToSearch as $entityType) {
|
||||
if (!in_array($entityType, $entityTypes)) continue;
|
||||
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
|
||||
$results = $results->merge($search);
|
||||
}
|
||||
return $results->sortByDesc('score')->take(20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a book for entities
|
||||
* @param integer $chapterId
|
||||
* @param string $searchString
|
||||
* @return Collection
|
||||
*/
|
||||
public function searchChapter($chapterId, $searchString)
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
|
||||
return $pages->sortByDesc('score');
|
||||
}
|
||||
|
||||
/**
|
||||
* Search across a particular entity type.
|
||||
* @param array $terms
|
||||
* @param string $entityType
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param bool $getCount Return the total count of the search
|
||||
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
|
||||
*/
|
||||
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
|
||||
{
|
||||
$query = $this->buildEntitySearchQuery($terms, $entityType);
|
||||
if ($getCount) return $query->count();
|
||||
|
||||
$query = $query->skip(($page-1) * $count)->take($count);
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a search query for an entity
|
||||
* @param array $terms
|
||||
* @param string $entityType
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function buildEntitySearchQuery($terms, $entityType = 'page')
|
||||
{
|
||||
$entity = $this->getEntity($entityType);
|
||||
$entitySelect = $entity->newQuery();
|
||||
|
||||
// Handle normal search terms
|
||||
if (count($terms['search']) > 0) {
|
||||
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
|
||||
$subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType));
|
||||
$subQuery->where(function(Builder $query) use ($terms) {
|
||||
foreach ($terms['search'] as $inputTerm) {
|
||||
$query->orWhere('term', 'like', $inputTerm .'%');
|
||||
}
|
||||
})->groupBy('entity_type', 'entity_id');
|
||||
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) {
|
||||
$join->on('id', '=', 'entity_id');
|
||||
})->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
|
||||
$entitySelect->mergeBindings($subQuery);
|
||||
}
|
||||
|
||||
// Handle exact term matching
|
||||
if (count($terms['exact']) > 0) {
|
||||
$entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
|
||||
foreach ($terms['exact'] as $inputTerm) {
|
||||
$query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) {
|
||||
$query->where('name', 'like', '%'.$inputTerm .'%')
|
||||
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle tag searches
|
||||
foreach ($terms['tags'] as $inputTerm) {
|
||||
$this->applyTagSearch($entitySelect, $inputTerm);
|
||||
}
|
||||
|
||||
// Handle filters
|
||||
foreach ($terms['filters'] as $filterTerm => $filterValue) {
|
||||
$functionName = camel_case('filter_' . $filterTerm);
|
||||
if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue);
|
||||
}
|
||||
|
||||
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse a search string into components.
|
||||
* @param $searchString
|
||||
* @return array
|
||||
*/
|
||||
protected function parseSearchString($searchString)
|
||||
{
|
||||
$terms = [
|
||||
'search' => [],
|
||||
'exact' => [],
|
||||
'tags' => [],
|
||||
'filters' => []
|
||||
];
|
||||
|
||||
$patterns = [
|
||||
'exact' => '/"(.*?)"/',
|
||||
'tags' => '/\[(.*?)\]/',
|
||||
'filters' => '/\{(.*?)\}/'
|
||||
];
|
||||
|
||||
// Parse special terms
|
||||
foreach ($patterns as $termType => $pattern) {
|
||||
$matches = [];
|
||||
preg_match_all($pattern, $searchString, $matches);
|
||||
if (count($matches) > 0) {
|
||||
$terms[$termType] = $matches[1];
|
||||
$searchString = preg_replace($pattern, '', $searchString);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse standard terms
|
||||
foreach (explode(' ', trim($searchString)) as $searchTerm) {
|
||||
if ($searchTerm !== '') $terms['search'][] = $searchTerm;
|
||||
}
|
||||
|
||||
// Split filter values out
|
||||
$splitFilters = [];
|
||||
foreach ($terms['filters'] as $filter) {
|
||||
$explodedFilter = explode(':', $filter, 2);
|
||||
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
|
||||
}
|
||||
$terms['filters'] = $splitFilters;
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available query operators as a regex escaped list.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getRegexEscapedOperators()
|
||||
{
|
||||
$escapedOperators = [];
|
||||
foreach ($this->queryOperators as $operator) {
|
||||
$escapedOperators[] = preg_quote($operator);
|
||||
}
|
||||
return join('|', $escapedOperators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a tag search term onto a entity query.
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $tagTerm
|
||||
* @return mixed
|
||||
*/
|
||||
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) {
|
||||
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
|
||||
$query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
|
||||
$tagName = $tagSplit[1];
|
||||
$tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
|
||||
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
|
||||
$validOperator = in_array($tagOperator, $this->queryOperators);
|
||||
if (!empty($tagOperator) && !empty($tagValue) && $validOperator) {
|
||||
if (!empty($tagName)) $query->where('name', '=', $tagName);
|
||||
if (is_numeric($tagValue) && $tagOperator !== 'like') {
|
||||
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
|
||||
// search the value as a string which prevents being able to do number-based operations
|
||||
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
|
||||
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
|
||||
$query->whereRaw("value ${tagOperator} ${tagValue}");
|
||||
} else {
|
||||
$query->where('value', $tagOperator, $tagValue);
|
||||
}
|
||||
} else {
|
||||
$query->where('name', '=', $tagName);
|
||||
}
|
||||
});
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance via type.
|
||||
* @param $type
|
||||
* @return Entity
|
||||
*/
|
||||
protected function getEntity($type)
|
||||
{
|
||||
return $this->entities[strtolower($type)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Index the given entity.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function indexEntity(Entity $entity)
|
||||
{
|
||||
$this->deleteEntityTerms($entity);
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
|
||||
$terms = array_merge($nameTerms, $bodyTerms);
|
||||
foreach ($terms as $index => $term) {
|
||||
$terms[$index]['entity_type'] = $entity->getMorphClass();
|
||||
$terms[$index]['entity_id'] = $entity->id;
|
||||
}
|
||||
$this->searchTerm->newQuery()->insert($terms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Index multiple Entities at once
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function indexEntities($entities) {
|
||||
$terms = [];
|
||||
foreach ($entities as $entity) {
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
|
||||
foreach (array_merge($nameTerms, $bodyTerms) as $term) {
|
||||
$term['entity_id'] = $entity->id;
|
||||
$term['entity_type'] = $entity->getMorphClass();
|
||||
$terms[] = $term;
|
||||
}
|
||||
}
|
||||
|
||||
$chunkedTerms = array_chunk($terms, 500);
|
||||
foreach ($chunkedTerms as $termChunk) {
|
||||
$this->searchTerm->newQuery()->insert($termChunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete and re-index the terms for all entities in the system.
|
||||
*/
|
||||
public function indexAllEntities()
|
||||
{
|
||||
$this->searchTerm->truncate();
|
||||
|
||||
// Chunk through all books
|
||||
$this->book->chunk(1000, function ($books) {
|
||||
$this->indexEntities($books);
|
||||
});
|
||||
|
||||
// Chunk through all chapters
|
||||
$this->chapter->chunk(1000, function ($chapters) {
|
||||
$this->indexEntities($chapters);
|
||||
});
|
||||
|
||||
// Chunk through all pages
|
||||
$this->page->chunk(1000, function ($pages) {
|
||||
$this->indexEntities($pages);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete related Entity search terms.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function deleteEntityTerms(Entity $entity)
|
||||
{
|
||||
$entity->searchTerms()->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a scored term array from the given text.
|
||||
* @param $text
|
||||
* @param float|int $scoreAdjustment
|
||||
* @return array
|
||||
*/
|
||||
protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
|
||||
{
|
||||
$tokenMap = []; // {TextToken => OccurrenceCount}
|
||||
$splitChars = " \n\t.,";
|
||||
$token = strtok($text, $splitChars);
|
||||
|
||||
while ($token !== false) {
|
||||
if (!isset($tokenMap[$token])) $tokenMap[$token] = 0;
|
||||
$tokenMap[$token]++;
|
||||
$token = strtok($splitChars);
|
||||
}
|
||||
|
||||
$terms = [];
|
||||
foreach ($tokenMap as $token => $count) {
|
||||
$terms[] = [
|
||||
'term' => $token,
|
||||
'score' => $count * $scoreAdjustment
|
||||
];
|
||||
}
|
||||
return $terms;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Custom entity search filters
|
||||
*/
|
||||
|
||||
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
$query->where('updated_at', '>=', $date);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
$query->where('updated_at', '<', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
$query->where('created_at', '>=', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
$query->where('created_at', '<', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') return;
|
||||
if ($input === 'me') $input = user()->id;
|
||||
$query->where('created_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') return;
|
||||
if ($input === 'me') $input = user()->id;
|
||||
$query->where('updated_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where('name', 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);}
|
||||
|
||||
protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where($model->textField, 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where('restricted', '=', true);
|
||||
}
|
||||
|
||||
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereHas('views', function($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereDoesntHave('views', function($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
protected function filterSortBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$functionName = camel_case('sort_by_' . $input);
|
||||
if (method_exists($this, $functionName)) $this->$functionName($query, $model);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sorting filter options
|
||||
*/
|
||||
|
||||
protected function sortByLastCommented(\Illuminate\Database\Eloquent\Builder $query, Entity $model)
|
||||
{
|
||||
$commentsTable = $this->db->getTablePrefix() . 'comments';
|
||||
$morphClass = str_replace('\\', '\\\\', $model->getMorphClass());
|
||||
$commentQuery = $this->db->raw('(SELECT c1.entity_id, c1.entity_type, c1.created_at as last_commented FROM '.$commentsTable.' c1 LEFT JOIN '.$commentsTable.' c2 ON (c1.entity_id = c2.entity_id AND c1.entity_type = c2.entity_type AND c1.created_at < c2.created_at) WHERE c1.entity_type = \''. $morphClass .'\' AND c2.created_at IS NULL) as comments');
|
||||
|
||||
$query->join($commentQuery, $model->getTable() . '.id', '=', 'comments.entity_id')->orderBy('last_commented', 'desc');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Setting;
|
||||
use BookStack\User;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
|
||||
/**
|
||||
@@ -15,6 +16,7 @@ class SettingService
|
||||
|
||||
protected $setting;
|
||||
protected $cache;
|
||||
protected $localCache = [];
|
||||
|
||||
protected $cachePrefix = 'setting-';
|
||||
|
||||
@@ -38,8 +40,25 @@ class SettingService
|
||||
*/
|
||||
public function get($key, $default = false)
|
||||
{
|
||||
if ($default === false) $default = config('setting-defaults.' . $key, false);
|
||||
if (isset($this->localCache[$key])) return $this->localCache[$key];
|
||||
|
||||
$value = $this->getValueFromStore($key, $default);
|
||||
return $this->formatValue($value, $default);
|
||||
$formatted = $this->formatValue($value, $default);
|
||||
$this->localCache[$key] = $formatted;
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user-specific setting from the database or cache.
|
||||
* @param User $user
|
||||
* @param $key
|
||||
* @param bool $default
|
||||
* @return bool|string
|
||||
*/
|
||||
public function getUser($user, $key, $default = false)
|
||||
{
|
||||
return $this->get($this->userKey($user->id, $key), $default);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,9 +76,8 @@ class SettingService
|
||||
|
||||
// Check the cache
|
||||
$cacheKey = $this->cachePrefix . $key;
|
||||
if ($this->cache->has($cacheKey)) {
|
||||
return $this->cache->get($cacheKey);
|
||||
}
|
||||
$cacheVal = $this->cache->get($cacheKey, null);
|
||||
if ($cacheVal !== null) return $cacheVal;
|
||||
|
||||
// Check the database
|
||||
$settingObject = $this->getSettingObjectByKey($key);
|
||||
@@ -69,14 +87,6 @@ class SettingService
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Check the defaults set in the app config.
|
||||
$configPrefix = 'setting-defaults.' . $key;
|
||||
if (config()->has($configPrefix)) {
|
||||
$value = config($configPrefix);
|
||||
$this->cache->forever($cacheKey, $value);
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
@@ -118,6 +128,16 @@ class SettingService
|
||||
return $setting !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user setting is in the database.
|
||||
* @param $key
|
||||
* @return bool
|
||||
*/
|
||||
public function hasUser($key)
|
||||
{
|
||||
return $this->has($this->userKey($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a setting to the database.
|
||||
* @param $key
|
||||
@@ -135,6 +155,28 @@ class SettingService
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a user-specific setting into the database.
|
||||
* @param User $user
|
||||
* @param $key
|
||||
* @param $value
|
||||
* @return bool
|
||||
*/
|
||||
public function putUser($user, $key, $value)
|
||||
{
|
||||
return $this->put($this->userKey($user->id, $key), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a setting key into a user-specific key.
|
||||
* @param $key
|
||||
* @return string
|
||||
*/
|
||||
protected function userKey($userId, $key = '')
|
||||
{
|
||||
return 'user:' . $userId . ':' . $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a setting from the database.
|
||||
* @param $key
|
||||
@@ -150,6 +192,16 @@ class SettingService
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete settings for a given user id.
|
||||
* @param $userId
|
||||
* @return mixed
|
||||
*/
|
||||
public function deleteUserSettings($userId)
|
||||
{
|
||||
return $this->setting->where('setting_key', 'like', $this->userKey($userId) . '%')->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a setting model from the database for the given key.
|
||||
* @param $key
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Http\Controllers\Auth\AuthController;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\SocialAccount;
|
||||
use BookStack\User;
|
||||
|
||||
class SocialAuthService
|
||||
{
|
||||
@@ -17,7 +14,7 @@ class SocialAuthService
|
||||
protected $socialite;
|
||||
protected $socialAccount;
|
||||
|
||||
protected $validSocialDrivers = ['google', 'github'];
|
||||
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure'];
|
||||
|
||||
/**
|
||||
* SocialAuthService constructor.
|
||||
@@ -73,12 +70,12 @@ class SocialAuthService
|
||||
|
||||
// Check social account has not already been used
|
||||
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
|
||||
throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login');
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
|
||||
}
|
||||
|
||||
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
|
||||
$email = $socialUser->getEmail();
|
||||
throw new UserRegistrationException('The email ' . $email . ' is already in use. If you already have an account you can connect your ' . $socialDriver . ' account from your profile settings.', '/login');
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver, 'email' => $email]), '/login');
|
||||
}
|
||||
|
||||
return $socialUser;
|
||||
@@ -101,14 +98,14 @@ class SocialAuthService
|
||||
|
||||
// Get any attached social accounts or users
|
||||
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
|
||||
$user = $this->userRepo->getByEmail($socialUser->getEmail());
|
||||
$isLoggedIn = auth()->check();
|
||||
$currentUser = auth()->user();
|
||||
$currentUser = user();
|
||||
|
||||
// When a user is not logged in and a matching SocialAccount exists,
|
||||
// Simply log the user into the application.
|
||||
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,
|
||||
@@ -116,38 +113,31 @@ class SocialAuthService
|
||||
if ($isLoggedIn && $socialAccount === null) {
|
||||
$this->fillSocialAccount($socialDriver, $socialUser);
|
||||
$currentUser->socialAccounts()->save($this->socialAccount);
|
||||
\Session::flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.');
|
||||
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => title_case($socialDriver)]));
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
// When a user is logged in and the social account exists and is already linked to the current user.
|
||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
|
||||
\Session::flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
|
||||
session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => title_case($socialDriver)]));
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
// When a user is logged in, A social account exists but the users do not match.
|
||||
// Change the user that the social account is assigned to.
|
||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
|
||||
\Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
|
||||
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => title_case($socialDriver)]));
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
// Otherwise let the user know this social account is not used by anyone.
|
||||
$message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings';
|
||||
$message = trans('errors.social_account_not_used', ['socialAccount' => title_case($socialDriver)]);
|
||||
if (setting('registration-enabled')) {
|
||||
$message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option';
|
||||
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
|
||||
}
|
||||
|
||||
throw new SocialSignInException($message . '.', '/login');
|
||||
}
|
||||
|
||||
|
||||
private function logUserIn($user)
|
||||
{
|
||||
auth()->login($user);
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the social driver is correct and supported.
|
||||
*
|
||||
@@ -159,8 +149,8 @@ class SocialAuthService
|
||||
{
|
||||
$driver = trim(strtolower($socialDriver));
|
||||
|
||||
if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
|
||||
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured;
|
||||
if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found'));
|
||||
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
|
||||
|
||||
return $driver;
|
||||
}
|
||||
@@ -185,14 +175,24 @@ class SocialAuthService
|
||||
public function getActiveDrivers()
|
||||
{
|
||||
$activeDrivers = [];
|
||||
foreach ($this->validSocialDrivers as $driverName) {
|
||||
if ($this->checkDriverConfigured($driverName)) {
|
||||
$activeDrivers[$driverName] = true;
|
||||
foreach ($this->validSocialDrivers as $driverKey) {
|
||||
if ($this->checkDriverConfigured($driverKey)) {
|
||||
$activeDrivers[$driverKey] = $this->getDriverName($driverKey);
|
||||
}
|
||||
}
|
||||
return $activeDrivers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the presentational name for a driver.
|
||||
* @param $driver
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDriverName($driver)
|
||||
{
|
||||
return config('services.' . strtolower($driver) . '.name');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $socialDriver
|
||||
* @param \Laravel\Socialite\Contracts\User $socialUser
|
||||
@@ -215,10 +215,9 @@ class SocialAuthService
|
||||
*/
|
||||
public function detachSocialAccount($socialDriver)
|
||||
{
|
||||
session();
|
||||
auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
||||
\Session::flash('success', $socialDriver . ' account successfully detached');
|
||||
return redirect(auth()->user()->getEditUrl());
|
||||
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
||||
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
|
||||
return redirect(user()->getEditUrl());
|
||||
}
|
||||
|
||||
}
|
||||
64
app/Services/UploadService.php
Normal file
64
app/Services/UploadService.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
|
||||
class UploadService
|
||||
{
|
||||
|
||||
/**
|
||||
* @var FileSystem
|
||||
*/
|
||||
protected $fileSystem;
|
||||
|
||||
/**
|
||||
* @var FileSystemInstance
|
||||
*/
|
||||
protected $storageInstance;
|
||||
|
||||
|
||||
/**
|
||||
* FileService constructor.
|
||||
* @param $fileSystem
|
||||
*/
|
||||
public function __construct(FileSystem $fileSystem)
|
||||
{
|
||||
$this->fileSystem = $fileSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage that will be used for storing images.
|
||||
* @return FileSystemInstance
|
||||
*/
|
||||
protected function getStorage()
|
||||
{
|
||||
if ($this->storageInstance !== null) return $this->storageInstance;
|
||||
|
||||
$storageType = config('filesystems.default');
|
||||
$this->storageInstance = $this->fileSystem->disk($storageType);
|
||||
|
||||
return $this->storageInstance;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether or not a folder is empty.
|
||||
* @param $path
|
||||
* @return bool
|
||||
*/
|
||||
protected function isFolderEmpty($path)
|
||||
{
|
||||
$files = $this->getStorage()->files($path);
|
||||
$folders = $this->getStorage()->directories($path);
|
||||
return (count($files) === 0 && count($folders) === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if using a local filesystem.
|
||||
* @return bool
|
||||
*/
|
||||
protected function isLocal()
|
||||
{
|
||||
return strtolower(config('filesystems.default')) === 'local';
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,18 @@ use BookStack\View;
|
||||
|
||||
class ViewService
|
||||
{
|
||||
|
||||
protected $view;
|
||||
protected $user;
|
||||
protected $restrictionService;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* ViewService constructor.
|
||||
* @param View $view
|
||||
* @param RestrictionService $restrictionService
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(View $view, RestrictionService $restrictionService)
|
||||
public function __construct(View $view, PermissionService $permissionService)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->user = auth()->user();
|
||||
$this->restrictionService = $restrictionService;
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,8 +26,9 @@ class ViewService
|
||||
*/
|
||||
public function add(Entity $entity)
|
||||
{
|
||||
if ($this->user === null) return 0;
|
||||
$view = $entity->views()->where('user_id', '=', $this->user->id)->first();
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) return 0;
|
||||
$view = $entity->views()->where('user_id', '=', $user->id)->first();
|
||||
// Add view if model exists
|
||||
if ($view) {
|
||||
$view->increment('views');
|
||||
@@ -39,7 +37,7 @@ class ViewService
|
||||
|
||||
// Otherwise create new view count
|
||||
$entity->views()->save($this->view->create([
|
||||
'user_id' => $this->user->id,
|
||||
'user_id' => $user->id,
|
||||
'views' => 1
|
||||
]));
|
||||
|
||||
@@ -50,17 +48,21 @@ class ViewService
|
||||
* Get the entities with the most views.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|false $filterModel
|
||||
* @param bool|false|array $filterModel
|
||||
*/
|
||||
public function getPopular($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->restrictionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
|
||||
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
|
||||
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
|
||||
if ($filterModel && is_array($filterModel)) {
|
||||
$query->whereIn('viewable_type', $filterModel);
|
||||
} else if ($filterModel) {
|
||||
$query->where('viewable_type', '=', get_class($filterModel));
|
||||
}
|
||||
|
||||
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
||||
}
|
||||
@@ -74,13 +76,14 @@ class ViewService
|
||||
*/
|
||||
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
if ($this->user === null) return collect();
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) return collect();
|
||||
|
||||
$query = $this->restrictionService
|
||||
$query = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
||||
|
||||
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||
$query = $query->where('user_id', '=', auth()->user()->id);
|
||||
$query = $query->where('user_id', '=', $user->id);
|
||||
|
||||
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get()->pluck('viewable');
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SocialAccount extends Model
|
||||
{
|
||||
@@ -11,6 +8,6 @@ class SocialAccount extends Model
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User');
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
|
||||
19
app/Tag.php
Normal file
19
app/Tag.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
/**
|
||||
* Class Attribute
|
||||
* @package BookStack
|
||||
*/
|
||||
class Tag extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'value', 'order'];
|
||||
|
||||
/**
|
||||
* Get the entity that this tag belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
}
|
||||
78
app/User.php
78
app/User.php
@@ -1,16 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
<?php namespace BookStack;
|
||||
|
||||
use BookStack\Notifications\ResetPassword;
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
|
||||
{
|
||||
use Authenticatable, CanResetPassword;
|
||||
use Authenticatable, CanResetPassword, Notifiable;
|
||||
|
||||
/**
|
||||
* The database table used by the model.
|
||||
@@ -37,22 +37,31 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
protected $permissions;
|
||||
|
||||
/**
|
||||
* Returns a default guest user.
|
||||
* Returns the default public user.
|
||||
* @return User
|
||||
*/
|
||||
public static function getDefault()
|
||||
{
|
||||
return new static([
|
||||
'email' => 'guest',
|
||||
'name' => 'Guest'
|
||||
]);
|
||||
return static::where('system_name', '=', 'public')->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is the default public user.
|
||||
* @return bool
|
||||
*/
|
||||
public function isDefault()
|
||||
{
|
||||
return $this->system_name === 'public';
|
||||
}
|
||||
|
||||
/**
|
||||
* The roles that belong to the user.
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Role');
|
||||
if ($this->id === 0) return ;
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,6 +74,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
return $this->roles->pluck('name')->contains($role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has a role.
|
||||
* @param $role
|
||||
* @return mixed
|
||||
*/
|
||||
public function hasSystemRole($role)
|
||||
{
|
||||
return $this->roles->pluck('system_name')->contains('admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions belonging to a the current user.
|
||||
* @param bool $cache
|
||||
@@ -116,7 +135,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function socialAccounts()
|
||||
{
|
||||
return $this->hasMany('BookStack\SocialAccount');
|
||||
return $this->hasMany(SocialAccount::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,8 +160,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getAvatar($size = 50)
|
||||
{
|
||||
if ($this->image_id === 0 || $this->image_id === '0' || $this->image_id === null) return '/user_avatar.png';
|
||||
return $this->avatar->getThumb($size, $size, false);
|
||||
$default = baseUrl('/user_avatar.png');
|
||||
$imageId = $this->image_id;
|
||||
if ($imageId === 0 || $imageId === '0' || $imageId === null) return $default;
|
||||
|
||||
try {
|
||||
$avatar = $this->avatar ? baseUrl($this->avatar->getThumb($size, $size, false)) : $default;
|
||||
} catch (\Exception $err) {
|
||||
$avatar = $default;
|
||||
}
|
||||
return $avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +178,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function avatar()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Image', 'image_id');
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,7 +187,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return '/settings/users/' . $this->id;
|
||||
return baseUrl('/settings/users/' . $this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url that links to this user's profile.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getProfileUrl()
|
||||
{
|
||||
return baseUrl('/user/' . $this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,4 +213,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
$this->notify(new ResetPassword($token));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class View extends Model
|
||||
{
|
||||
|
||||
187
app/helpers.php
187
app/helpers.php
@@ -1,74 +1,169 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('versioned_asset')) {
|
||||
/**
|
||||
* Get the path to a versioned file.
|
||||
*
|
||||
* @param string $file
|
||||
* @return string
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
function versioned_asset($file)
|
||||
{
|
||||
static $manifest = null;
|
||||
use BookStack\Ownable;
|
||||
|
||||
if (is_null($manifest)) {
|
||||
$manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true);
|
||||
}
|
||||
/**
|
||||
* Get the path to a versioned file.
|
||||
*
|
||||
* @param string $file
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
function versioned_asset($file = '')
|
||||
{
|
||||
static $version = null;
|
||||
|
||||
if (isset($manifest[$file])) {
|
||||
return '/' . $manifest[$file];
|
||||
}
|
||||
|
||||
if (file_exists(public_path($file))) {
|
||||
return '/' . $file;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
|
||||
if (is_null($version)) {
|
||||
$versionFile = base_path('version');
|
||||
$version = trim(file_get_contents($versionFile));
|
||||
}
|
||||
|
||||
$additional = '';
|
||||
if (config('app.env') === 'development') {
|
||||
$additional = sha1_file(public_path($file));
|
||||
}
|
||||
|
||||
$path = $file . '?version=' . urlencode($version) . $additional;
|
||||
return baseUrl($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the current User.
|
||||
* Defaults to public 'Guest' user if not logged in.
|
||||
* @return \BookStack\User
|
||||
*/
|
||||
function user()
|
||||
{
|
||||
return auth()->user() ?: \BookStack\User::getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user is a signed in user.
|
||||
* @return bool
|
||||
*/
|
||||
function signedInUser()
|
||||
{
|
||||
return auth()->user() && !auth()->user()->isDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has a permission.
|
||||
* If an ownable element is passed in the permissions are checked against
|
||||
* If an ownable element is passed in the jointPermissions are checked against
|
||||
* that particular item.
|
||||
* @param $permission
|
||||
* @param \BookStack\Ownable $ownable
|
||||
* @param Ownable $ownable
|
||||
* @return mixed
|
||||
*/
|
||||
function userCan($permission, \BookStack\Ownable $ownable = null)
|
||||
function userCan($permission, Ownable $ownable = null)
|
||||
{
|
||||
if (!auth()->check()) return false;
|
||||
if ($ownable === null) {
|
||||
return auth()->user() && auth()->user()->can($permission);
|
||||
return user() && user()->can($permission);
|
||||
}
|
||||
|
||||
// Check permission on ownable item
|
||||
$permissionBaseName = strtolower($permission) . '-';
|
||||
$hasPermission = false;
|
||||
if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true;
|
||||
if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true;
|
||||
|
||||
if (!$ownable instanceof \BookStack\Entity) return $hasPermission;
|
||||
|
||||
// Check restrictions on the entity
|
||||
$restrictionService = app('BookStack\Services\RestrictionService');
|
||||
$explodedPermission = explode('-', $permission);
|
||||
$action = end($explodedPermission);
|
||||
$hasAccess = $restrictionService->checkIfEntityRestricted($ownable, $action);
|
||||
$restrictionsSet = $restrictionService->checkIfRestrictionsSet($ownable, $action);
|
||||
return ($hasAccess && $restrictionsSet) || (!$restrictionsSet && $hasPermission);
|
||||
$permissionService = app(\BookStack\Services\PermissionService::class);
|
||||
return $permissionService->checkOwnableUserAccess($ownable, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to access system settings.
|
||||
* @param $key
|
||||
* @param bool $default
|
||||
* @return mixed
|
||||
* @return bool|string|\BookStack\Services\SettingService
|
||||
*/
|
||||
function setting($key, $default = false)
|
||||
function setting($key = null, $default = false)
|
||||
{
|
||||
$settingService = app('BookStack\Services\SettingService');
|
||||
$settingService = resolve(\BookStack\Services\SettingService::class);
|
||||
if (is_null($key)) return $settingService;
|
||||
return $settingService->get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create url's relative to the applications root path.
|
||||
* @param string $path
|
||||
* @param bool $forceAppDomain
|
||||
* @return string
|
||||
*/
|
||||
function baseUrl($path, $forceAppDomain = false)
|
||||
{
|
||||
$isFullUrl = strpos($path, 'http') === 0;
|
||||
if ($isFullUrl && !$forceAppDomain) return $path;
|
||||
$path = trim($path, '/');
|
||||
|
||||
// Remove non-specified domain if forced and we have a domain
|
||||
if ($isFullUrl && $forceAppDomain) {
|
||||
$explodedPath = explode('/', $path);
|
||||
$path = implode('/', array_splice($explodedPath, 3));
|
||||
}
|
||||
|
||||
// Return normal url path if not specified in config
|
||||
if (config('app.url') === '') {
|
||||
return url($path);
|
||||
}
|
||||
|
||||
return rtrim(config('app.url'), '/') . '/' . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the redirector.
|
||||
* Overrides the default laravel redirect helper.
|
||||
* Ensures it redirects even when the app is in a subdirectory.
|
||||
*
|
||||
* @param string|null $to
|
||||
* @param int $status
|
||||
* @param array $headers
|
||||
* @param bool $secure
|
||||
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
function redirect($to = null, $status = 302, $headers = [], $secure = null)
|
||||
{
|
||||
if (is_null($to)) {
|
||||
return app('redirect');
|
||||
}
|
||||
|
||||
$to = baseUrl($to);
|
||||
|
||||
return app('redirect')->to($to, $status, $headers, $secure);
|
||||
}
|
||||
|
||||
function icon($name, $attrs = []) {
|
||||
$iconPath = resource_path('assets/icons/' . $name . '.svg');
|
||||
$attrString = ' ';
|
||||
foreach ($attrs as $attrName => $attr) {
|
||||
$attrString .= $attrName . '="' . $attr . '" ';
|
||||
}
|
||||
$fileContents = file_get_contents($iconPath);
|
||||
return str_replace('<svg', '<svg' . $attrString, $fileContents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a url with multiple parameters for sorting purposes.
|
||||
* Works out the logic to set the correct sorting direction
|
||||
* Discards empty parameters and allows overriding.
|
||||
* @param $path
|
||||
* @param array $data
|
||||
* @param array $overrideData
|
||||
* @return string
|
||||
*/
|
||||
function sortUrl($path, $data, $overrideData = [])
|
||||
{
|
||||
$queryStringSections = [];
|
||||
$queryData = array_merge($data, $overrideData);
|
||||
|
||||
// Change sorting direction is already sorted on current attribute
|
||||
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
|
||||
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
|
||||
} else {
|
||||
$queryData['order'] = 'asc';
|
||||
}
|
||||
|
||||
foreach ($queryData as $name => $value) {
|
||||
$trimmedVal = trim($value);
|
||||
if ($trimmedVal === '') continue;
|
||||
$queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
|
||||
}
|
||||
|
||||
if (count($queryStringSections) === 0) return $path;
|
||||
|
||||
return baseUrl($path . '?' . implode('&', $queryStringSections));
|
||||
}
|
||||
@@ -14,6 +14,7 @@ define('LARAVEL_START', microtime(true));
|
||||
|
|
||||
*/
|
||||
|
||||
require __DIR__.'/../app/helpers.php';
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
/*
|
||||
|
||||
@@ -5,23 +5,28 @@
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=5.5.9",
|
||||
"laravel/framework": "5.2.*",
|
||||
"php": ">=5.6.4",
|
||||
"laravel/framework": "5.4.*",
|
||||
"ext-tidy": "*",
|
||||
"intervention/image": "^2.3",
|
||||
"laravel/socialite": "^2.0",
|
||||
"barryvdh/laravel-ide-helper": "^2.1",
|
||||
"barryvdh/laravel-debugbar": "^2.0",
|
||||
"laravel/socialite": "^3.0",
|
||||
"barryvdh/laravel-ide-helper": "^2.2.3",
|
||||
"barryvdh/laravel-debugbar": "^2.3.2",
|
||||
"league/flysystem-aws-s3-v3": "^1.0",
|
||||
"barryvdh/laravel-dompdf": "0.6.*",
|
||||
"predis/predis": "^1.0"
|
||||
"barryvdh/laravel-dompdf": "^0.8",
|
||||
"predis/predis": "^1.1",
|
||||
"gathercontent/htmldiff": "^0.2.1",
|
||||
"barryvdh/laravel-snappy": "^0.3.1",
|
||||
"laravel/browser-kit-testing": "^1.0",
|
||||
"socialiteproviders/slack": "^3.0",
|
||||
"socialiteproviders/microsoft-azure": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fzaninotto/faker": "~1.4",
|
||||
"mockery/mockery": "0.9.*",
|
||||
"phpunit/phpunit": "~4.0",
|
||||
"phpspec/phpspec": "~2.1",
|
||||
"symfony/dom-crawler": "~3.0",
|
||||
"symfony/css-selector": "~3.0"
|
||||
"phpunit/phpunit": "~5.0",
|
||||
"symfony/css-selector": "3.1.*",
|
||||
"symfony/dom-crawler": "3.1.*"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
@@ -29,32 +34,41 @@
|
||||
],
|
||||
"psr-4": {
|
||||
"BookStack\\": "app/"
|
||||
},
|
||||
"files": [
|
||||
"app/helpers.php"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"classmap": [
|
||||
"tests/TestCase.php"
|
||||
]
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-install-cmd": [
|
||||
"php artisan clear-compiled",
|
||||
"php artisan optimize"
|
||||
],
|
||||
"pre-update-cmd": [
|
||||
"php artisan clear-compiled"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"php artisan optimize"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"php -r \"copy('.env.example', '.env');\""
|
||||
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"php artisan key:generate"
|
||||
],
|
||||
"pre-update-cmd": [
|
||||
"php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
|
||||
"php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
|
||||
],
|
||||
"pre-install-cmd": [
|
||||
"php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
|
||||
"php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
|
||||
],
|
||||
"post-install-cmd": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postInstall",
|
||||
"php artisan optimize",
|
||||
"php artisan cache:clear",
|
||||
"php artisan view:clear"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
|
||||
"php artisan optimize"
|
||||
],
|
||||
"refresh-test-database": [
|
||||
"php artisan migrate:refresh --database=mysql_testing",
|
||||
"php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
|
||||
2516
composer.lock
generated
2516
composer.lock
generated
File diff suppressed because it is too large
Load Diff
17
config/app.php
Normal file → Executable file
17
config/app.php
Normal file → Executable file
@@ -31,7 +31,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -57,7 +57,8 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => 'en',
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
'locales' => ['en', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl', 'it', 'ru'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -100,7 +101,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'log' => 'single',
|
||||
'log' => env('APP_LOGGING', 'single'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -130,7 +131,6 @@ return [
|
||||
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
|
||||
Illuminate\Hashing\HashServiceProvider::class,
|
||||
Illuminate\Mail\MailServiceProvider::class,
|
||||
Illuminate\Pagination\PaginationServiceProvider::class,
|
||||
Illuminate\Pipeline\PipelineServiceProvider::class,
|
||||
Illuminate\Queue\QueueServiceProvider::class,
|
||||
Illuminate\Redis\RedisServiceProvider::class,
|
||||
@@ -139,7 +139,8 @@ return [
|
||||
Illuminate\Translation\TranslationServiceProvider::class,
|
||||
Illuminate\Validation\ValidationServiceProvider::class,
|
||||
Illuminate\View\ViewServiceProvider::class,
|
||||
Laravel\Socialite\SocialiteServiceProvider::class,
|
||||
Illuminate\Notifications\NotificationServiceProvider::class,
|
||||
SocialiteProviders\Manager\ServiceProvider::class,
|
||||
|
||||
/**
|
||||
* Third Party
|
||||
@@ -148,13 +149,17 @@ return [
|
||||
Barryvdh\DomPDF\ServiceProvider::class,
|
||||
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
|
||||
Barryvdh\Debugbar\ServiceProvider::class,
|
||||
Barryvdh\Snappy\ServiceProvider::class,
|
||||
|
||||
|
||||
/*
|
||||
* Application Service Providers...
|
||||
*/
|
||||
BookStack\Providers\PaginationServiceProvider::class,
|
||||
|
||||
BookStack\Providers\AuthServiceProvider::class,
|
||||
BookStack\Providers\AppServiceProvider::class,
|
||||
BookStack\Providers\BroadcastServiceProvider::class,
|
||||
BookStack\Providers\EventServiceProvider::class,
|
||||
BookStack\Providers\RouteServiceProvider::class,
|
||||
BookStack\Providers\CustomFacadeProvider::class,
|
||||
@@ -193,6 +198,7 @@ return [
|
||||
'Lang' => Illuminate\Support\Facades\Lang::class,
|
||||
'Log' => Illuminate\Support\Facades\Log::class,
|
||||
'Mail' => Illuminate\Support\Facades\Mail::class,
|
||||
'Notification' => Illuminate\Support\Facades\Notification::class,
|
||||
'Password' => Illuminate\Support\Facades\Password::class,
|
||||
'Queue' => Illuminate\Support\Facades\Queue::class,
|
||||
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
||||
@@ -214,6 +220,7 @@ return [
|
||||
|
||||
'ImageTool' => Intervention\Image\Facades\Image::class,
|
||||
'PDF' => Barryvdh\DomPDF\Facade::class,
|
||||
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
|
||||
'Debugbar' => Barryvdh\Debugbar\Facade::class,
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user