mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-25 19:07:25 +03:00
Compare commits
410 Commits
docker_env
...
v25.02.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
573a2dd22a | ||
|
|
b55cc803d3 | ||
|
|
304ade418e | ||
|
|
997931c42f | ||
|
|
268e353431 | ||
|
|
b491b5fbca | ||
|
|
387c786768 | ||
|
|
2641586a6f | ||
|
|
6d2cd20e80 | ||
|
|
b0c574356a | ||
|
|
07e45a20e5 | ||
|
|
14056c69e6 | ||
|
|
fb9c840c46 | ||
|
|
5fba4a5399 | ||
|
|
c0b377050e | ||
|
|
f3efb6441d | ||
|
|
0cf313a21e | ||
|
|
26aadffb20 | ||
|
|
a5f48e3202 | ||
|
|
b0dda6e6a7 | ||
|
|
d4025d95e7 | ||
|
|
d6021f4d22 | ||
|
|
b9a3290731 | ||
|
|
48f235ea5a | ||
|
|
047771b9f4 | ||
|
|
b5375114d3 | ||
|
|
fc13e56cea | ||
|
|
77fc37ac25 | ||
|
|
3424351e84 | ||
|
|
606f9d92d0 | ||
|
|
a5e25abb9c | ||
|
|
b310e87e4c | ||
|
|
425baf9d6e | ||
|
|
825c369ad9 | ||
|
|
10bab70438 | ||
|
|
350e0b281b | ||
|
|
08805ea3c8 | ||
|
|
9441e32c69 | ||
|
|
530fc37067 | ||
|
|
369e499dce | ||
|
|
655815de6d | ||
|
|
457adc1fee | ||
|
|
e86a90967e | ||
|
|
5d08f7cf14 | ||
|
|
8744eb2d62 | ||
|
|
d8383cfa80 | ||
|
|
4626278447 | ||
|
|
c61af9c22b | ||
|
|
72521d0906 | ||
|
|
7e44b195c5 | ||
|
|
5b45eac5e1 | ||
|
|
c1d30341e7 | ||
|
|
80d2b4913b | ||
|
|
3f473528b1 | ||
|
|
d0dcd4f61b | ||
|
|
bde66a1396 | ||
|
|
4de5a2d9bf | ||
|
|
27bf4299cf | ||
|
|
164f01bb25 | ||
|
|
f563a005f5 | ||
|
|
a14d8e30cc | ||
|
|
a9194ffb63 | ||
|
|
2f9c1b7127 | ||
|
|
bbea76668b | ||
|
|
becc630acf | ||
|
|
4ac8ecad6b | ||
|
|
903e88c700 | ||
|
|
ed96aa820e | ||
|
|
63ec079b7b | ||
|
|
d485fcb3db | ||
|
|
0f895668a4 | ||
|
|
6c577ac3bf | ||
|
|
31cc2423d2 | ||
|
|
c9ed32e518 | ||
|
|
6b4c3a0969 | ||
|
|
2dad92d1bd | ||
|
|
c1fb7ab7dc | ||
|
|
98315f3899 | ||
|
|
8c82aaabd6 | ||
|
|
ce9b536b78 | ||
|
|
d9c50e5bc1 | ||
|
|
bf075f7dd8 | ||
|
|
a4fd673285 | ||
|
|
e794c977bc | ||
|
|
0b088ef1d3 | ||
|
|
bf6a6af683 | ||
|
|
914790fd99 | ||
|
|
edb0c6a9e8 | ||
|
|
84049de696 | ||
|
|
da0531e63b | ||
|
|
421dc75f4e | ||
|
|
8ae91df038 | ||
|
|
64b41dd626 | ||
|
|
ebd6e4d3a2 | ||
|
|
80374aea5c | ||
|
|
2ac9efae7d | ||
|
|
a11d565ba4 | ||
|
|
1fdf854ea7 | ||
|
|
e9c9792cb9 | ||
|
|
5ae524c25a | ||
|
|
0d7287fc8b | ||
|
|
e77c96f6b7 | ||
|
|
9b8a10dd3a | ||
|
|
49200ca5ce | ||
|
|
34aa4dbf10 | ||
|
|
5ee79d16c9 | ||
|
|
a1ea4006e0 | ||
|
|
9078188939 | ||
|
|
ed0aad1a7a | ||
|
|
5c59cfb020 | ||
|
|
3ca15ad68a | ||
|
|
60014989f5 | ||
|
|
57b10f195e | ||
|
|
b1e95eb39f | ||
|
|
b3da77b8f9 | ||
|
|
1a345b74bb | ||
|
|
8ffc3a4abf | ||
|
|
7233c1c7b2 | ||
|
|
1309a01131 | ||
|
|
0333185b6d | ||
|
|
83f89f64e8 | ||
|
|
11a1a6fb16 | ||
|
|
882c609296 | ||
|
|
176a0dcd59 | ||
|
|
94b0f70bfa | ||
|
|
08b2a77d41 | ||
|
|
3e8e9a23cf | ||
|
|
58b83b64c8 | ||
|
|
dfe4cde6ee | ||
|
|
d11144d9e2 | ||
|
|
f96b0ea5f3 | ||
|
|
815f8d79ed | ||
|
|
b62dab32e0 | ||
|
|
262f863981 | ||
|
|
a4c94390a1 | ||
|
|
53f3cca85d | ||
|
|
ed08bbcecc | ||
|
|
de97ebf9b7 | ||
|
|
f492a660a8 | ||
|
|
09436836a5 | ||
|
|
bb455d7788 | ||
|
|
009212ab80 | ||
|
|
ba9cb591c8 | ||
|
|
d00ac2f34e | ||
|
|
bd4dc6d463 | ||
|
|
d91180a909 | ||
|
|
bc2913a5cb | ||
|
|
4802394562 | ||
|
|
1755556468 | ||
|
|
01cdbdb7ae | ||
|
|
fc8bbf3eab | ||
|
|
3cdab19319 | ||
|
|
5661d20e87 | ||
|
|
91f80123e8 | ||
|
|
7a0636d0f8 | ||
|
|
0fe5bdfbac | ||
|
|
f88687e977 | ||
|
|
68d437d05b | ||
|
|
1e56aaea04 | ||
|
|
dab170a6fe | ||
|
|
a8de717d9b | ||
|
|
78fe95b6fc | ||
|
|
e0c24e41aa | ||
|
|
fa8553839b | ||
|
|
b8fcefc794 | ||
|
|
88bcb68fcb | ||
|
|
7c000553ae | ||
|
|
391fa35c80 | ||
|
|
c6773a8c9f | ||
|
|
9b226e7d39 | ||
|
|
9865446267 | ||
|
|
926abbe776 | ||
|
|
4fabef3a57 | ||
|
|
5ef4cd80c3 | ||
|
|
e01f23583f | ||
|
|
7792cb3915 | ||
|
|
be26253a18 | ||
|
|
1bdd1f8189 | ||
|
|
fa62c79b17 | ||
|
|
d7d8fa1e5b | ||
|
|
18562f1e10 | ||
|
|
86090a694f | ||
|
|
1ee8287c73 | ||
|
|
8eb98cd591 | ||
|
|
0f9ba21b05 | ||
|
|
834f8e7046 | ||
|
|
32e3399334 | ||
|
|
2d8698a218 | ||
|
|
454fb883a2 | ||
|
|
6f4a6ab8ea | ||
|
|
9c4b6f36f1 | ||
|
|
78886b1e67 | ||
|
|
d9debaf032 | ||
|
|
d4360d6347 | ||
|
|
175b1785c0 | ||
|
|
c8740c0171 | ||
|
|
91ee895a74 | ||
|
|
a045e46571 | ||
|
|
44eaa65c3b | ||
|
|
0a22af7b14 | ||
|
|
b54702ab08 | ||
|
|
c4fdcfc5d1 | ||
|
|
cb8117e8df | ||
|
|
5a218d5056 | ||
|
|
8dbc5cf9c6 | ||
|
|
71e81615a3 | ||
|
|
611d37da04 | ||
|
|
0e799a3857 | ||
|
|
b91d6e2bfa | ||
|
|
ea16ad7e94 | ||
|
|
ba6eb54552 | ||
|
|
f705e7683b | ||
|
|
dc996adb20 | ||
|
|
a64c638ccc | ||
|
|
359c067279 | ||
|
|
66a746e297 | ||
|
|
a4d43ee24b | ||
|
|
f7793a70a9 | ||
|
|
ceba3d31fb | ||
|
|
eecc08edde | ||
|
|
eb19aadc75 | ||
|
|
06c81e69b9 | ||
|
|
3dc3d4a639 | ||
|
|
94c59c1e3d | ||
|
|
4d2205853a | ||
|
|
751772b87a | ||
|
|
76e30869e1 | ||
|
|
3edc9fe9eb | ||
|
|
616c62703e | ||
|
|
ecd56917e7 | ||
|
|
e22c9cae91 | ||
|
|
29ddb6e1b9 | ||
|
|
2ff90e2ff0 | ||
|
|
04ecc128a2 | ||
|
|
87d1d3423b | ||
|
|
4818192a2a | ||
|
|
965dd97f54 | ||
|
|
195b74926c | ||
|
|
2120db12b2 | ||
|
|
ed563fef28 | ||
|
|
0d31a8e3f1 | ||
|
|
b8354b974b | ||
|
|
034c1e289d | ||
|
|
f31605a3de | ||
|
|
e7cc75c74d | ||
|
|
4b79d5e4e8 | ||
|
|
34854915b3 | ||
|
|
af6f34b529 | ||
|
|
fb82a2b896 | ||
|
|
5b464938b6 | ||
|
|
81f954890d | ||
|
|
0e2bbcec62 | ||
|
|
fdd339f525 | ||
|
|
8cf7d6a83d | ||
|
|
58a5008718 | ||
|
|
c44a8df55d | ||
|
|
ff1494c519 | ||
|
|
b8ce8fd852 | ||
|
|
75e7454a5f | ||
|
|
2558ea8931 | ||
|
|
ac0f47a4b2 | ||
|
|
4f16129869 | ||
|
|
64a8037fdd | ||
|
|
7502ba1bc8 | ||
|
|
33a04697ef | ||
|
|
b70a5c0cdb | ||
|
|
9443ae9f40 | ||
|
|
220c2a4102 | ||
|
|
e9914eb301 | ||
|
|
934512d09c | ||
|
|
9102c90986 | ||
|
|
c3e74219c4 | ||
|
|
13c9d7bc2d | ||
|
|
119b539586 | ||
|
|
29a5c180f0 | ||
|
|
7906602291 | ||
|
|
6dafe773ff | ||
|
|
25bc28a1be | ||
|
|
4c561c7fa0 | ||
|
|
95b3e78573 | ||
|
|
63a345bc93 | ||
|
|
e093a172cb | ||
|
|
4b01f8934b | ||
|
|
bc116b45b5 | ||
|
|
a059960b9e | ||
|
|
7770966fed | ||
|
|
d7adcf6c69 | ||
|
|
04a364dcc3 | ||
|
|
db83ac7eaa | ||
|
|
3ca9dddf61 | ||
|
|
bf74f53ca7 | ||
|
|
9d67efb4a4 | ||
|
|
3a39b9f440 | ||
|
|
27f7aab375 | ||
|
|
337da0c467 | ||
|
|
f56b3560c4 | ||
|
|
02dfe11ce6 | ||
|
|
83d06beb70 | ||
|
|
a8cfc059c8 | ||
|
|
1614b2bab0 | ||
|
|
4bdec0d214 | ||
|
|
6a7d7e7c2b | ||
|
|
30d4674657 | ||
|
|
9f961f95f8 | ||
|
|
bab99a26ec | ||
|
|
9a7fecd269 | ||
|
|
a8dc0d449b | ||
|
|
a0381f76bf | ||
|
|
6102f66daa | ||
|
|
c6134d162d | ||
|
|
2046f9b9de | ||
|
|
ac3ba594a4 | ||
|
|
22df25a480 | ||
|
|
8b30c7f02e | ||
|
|
757cdddc7c | ||
|
|
df95e99680 | ||
|
|
5a6d544db7 | ||
|
|
16117d329c | ||
|
|
e90da18ada | ||
|
|
a08d80e1cc | ||
|
|
6258175922 | ||
|
|
15736777a0 | ||
|
|
75915e8a94 | ||
|
|
9bde0ae4ea | ||
|
|
0c802d1f86 | ||
|
|
b7a96c6466 | ||
|
|
4b645a82c7 | ||
|
|
d599b77b6f | ||
|
|
26e93dc8c1 | ||
|
|
a4c9a8491b | ||
|
|
70ee636d87 | ||
|
|
b35f6dbb03 | ||
|
|
67d9e24d8f | ||
|
|
3903fda6ca | ||
|
|
441e46ebaa | ||
|
|
1f4260f359 | ||
|
|
dc0bf8ad4e | ||
|
|
102e326e6a | ||
|
|
2b25bf6f3b | ||
|
|
f93280696d | ||
|
|
1787391b07 | ||
|
|
a74a8ee483 | ||
|
|
7fa5405cb7 | ||
|
|
6725ddcc41 | ||
|
|
bce941db3f | ||
|
|
6d926048ec | ||
|
|
5335c973b4 | ||
|
|
15c3e5c96e | ||
|
|
a5d5904969 | ||
|
|
598758b991 | ||
|
|
9926e23bc8 | ||
|
|
5d3264bc63 | ||
|
|
d71f819f95 | ||
|
|
ee13509760 | ||
|
|
82d7bb1f32 | ||
|
|
cdfda508d8 | ||
|
|
da941e584f | ||
|
|
65874d7b96 | ||
|
|
ac9b8f405c | ||
|
|
8d1419a12e | ||
|
|
04f7a7d301 | ||
|
|
c10d2a1493 | ||
|
|
97bbf79ffd | ||
|
|
f7b01ae53d | ||
|
|
d704e1dbba | ||
|
|
ef2ff5e093 | ||
|
|
7caed3b0db | ||
|
|
45641d0754 | ||
|
|
4b1d08ba99 | ||
|
|
160fa99ba4 | ||
|
|
d2a5ab49ed | ||
|
|
c6404d8917 | ||
|
|
7113807f12 | ||
|
|
be711215e8 | ||
|
|
7e3b404240 | ||
|
|
e86901ca20 | ||
|
|
bdfa61c8b2 | ||
|
|
2cc36787f5 | ||
|
|
448ac61b48 | ||
|
|
753f6394f7 | ||
|
|
b1faf65934 | ||
|
|
09f478bd74 | ||
|
|
a0497feddd | ||
|
|
789693bde9 | ||
|
|
1fe933e4ea | ||
|
|
724b4b5a70 | ||
|
|
1778a56146 | ||
|
|
744865fcb2 | ||
|
|
7f8c8b448d | ||
|
|
a67c53826d | ||
|
|
14b131e850 | ||
|
|
9b55a52b85 | ||
|
|
db1d10e80f | ||
|
|
1be576966f | ||
|
|
b97e792c5f | ||
|
|
8dec674cc3 | ||
|
|
f784c03746 | ||
|
|
148e172fe8 | ||
|
|
56ae86646f | ||
|
|
1d2b6fdfa2 | ||
|
|
4fc75beed4 | ||
|
|
3b3bc0c4bf | ||
|
|
910faab88e | ||
|
|
f184d763ad | ||
|
|
a91d42634d | ||
|
|
f517ef3616 | ||
|
|
e99507ddcf | ||
|
|
d2cacf1945 | ||
|
|
448ac1405b | ||
|
|
6ad21ce885 |
@@ -36,14 +36,10 @@ APP_LANG=en
|
|||||||
# APP_LANG will be used if such a header is not provided.
|
# APP_LANG will be used if such a header is not provided.
|
||||||
APP_AUTO_LANG_PUBLIC=true
|
APP_AUTO_LANG_PUBLIC=true
|
||||||
|
|
||||||
# Application timezones
|
# Application timezone
|
||||||
# The first option is used to determine what timezone is used for date storage.
|
# Used where dates are displayed such as on exported content.
|
||||||
# Leaving that as "UTC" is advised.
|
|
||||||
# The second option is used to set the timezone which will be used for date
|
|
||||||
# formatting and display. This defaults to the "APP_TIMEZONE" value.
|
|
||||||
# Valid timezone values can be found here: https://www.php.net/manual/en/timezones.php
|
# Valid timezone values can be found here: https://www.php.net/manual/en/timezones.php
|
||||||
APP_TIMEZONE=UTC
|
APP_TIMEZONE=UTC
|
||||||
APP_DISPLAY_TIMEZONE=UTC
|
|
||||||
|
|
||||||
# Application theme
|
# Application theme
|
||||||
# Used to specific a themes/<APP_THEME> folder where BookStack UI
|
# Used to specific a themes/<APP_THEME> folder where BookStack UI
|
||||||
|
|||||||
1
.github/ISSUE_TEMPLATE/support_request.yml
vendored
1
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -42,7 +42,6 @@ body:
|
|||||||
label: Log Content
|
label: Log Content
|
||||||
description: If the issue has produced an error, provide any [BookStack or server log](https://www.bookstackapp.com/docs/admin/debugging/) content below.
|
description: If the issue has produced an error, provide any [BookStack or server log](https://www.bookstackapp.com/docs/admin/debugging/) content below.
|
||||||
placeholder: Be sure to remove any confidential details in your logs
|
placeholder: Be sure to remove any confidential details in your logs
|
||||||
render: text
|
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
9
.github/ISSUE_TEMPLATE/z_blank_request.yml
vendored
9
.github/ISSUE_TEMPLATE/z_blank_request.yml
vendored
@@ -1,9 +0,0 @@
|
|||||||
name: Blank Request (Maintainers Only)
|
|
||||||
description: For maintainers only - Start a blank request
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "**This blank request option is only for existing official maintainers of the project!** Please instead use a different request option. If you use this your issue will be closed off."
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
27
.github/translators.txt
vendored
27
.github/translators.txt
vendored
@@ -438,7 +438,7 @@ javadataherian :: Persian
|
|||||||
Ludo-code :: French
|
Ludo-code :: French
|
||||||
hollsten :: Swedish
|
hollsten :: Swedish
|
||||||
Ngoc Lan Phung (lanpncz) :: Vietnamese
|
Ngoc Lan Phung (lanpncz) :: Vietnamese
|
||||||
Worive :: Catalan; French
|
Worive :: Catalan
|
||||||
Илья Скаба (skabailya) :: Russian
|
Илья Скаба (skabailya) :: Russian
|
||||||
Irjan Olsen (Irch) :: Norwegian Bokmal
|
Irjan Olsen (Irch) :: Norwegian Bokmal
|
||||||
Aleksandar Jovanovic (jovanoviczaleksandar) :: Serbian (Cyrillic)
|
Aleksandar Jovanovic (jovanoviczaleksandar) :: Serbian (Cyrillic)
|
||||||
@@ -484,28 +484,3 @@ Ilya Shaulov (ishaulov) :: Russian
|
|||||||
Konstantin Bobkov (b.konstantv) :: Russian
|
Konstantin Bobkov (b.konstantv) :: Russian
|
||||||
Ruben Sutter (rubensutter) :: German
|
Ruben Sutter (rubensutter) :: German
|
||||||
jellium :: French
|
jellium :: French
|
||||||
Qxlkdr :: Swedish
|
|
||||||
Hari (muhhari) :: Indonesian
|
|
||||||
仙君御 (xjy) :: Chinese Simplified
|
|
||||||
TapioM :: Finnish
|
|
||||||
lingb58 :: Chinese Traditional
|
|
||||||
Angel Pandey (angel-pandey) :: Nepali
|
|
||||||
Supriya Shrestha (supriyashrestha) :: Nepali
|
|
||||||
gprabhat :: Nepali
|
|
||||||
CellCat :: Chinese Simplified
|
|
||||||
Al Desrahim (aldesrahim) :: Indonesian
|
|
||||||
ahmad abbaspour (deshneh.dar.diss) :: Persian
|
|
||||||
Erjon K. (ekr) :: Albanian
|
|
||||||
LiZerui (iamzrli) :: Chinese Traditional
|
|
||||||
Ticker (ticker.com) :: Hebrew
|
|
||||||
CrazyComputer :: Chinese Simplified
|
|
||||||
Firr (FirrV) :: Russian
|
|
||||||
João Faro (FaroJoaoFaro) :: Portuguese
|
|
||||||
Danilo dos Santos Barbosa (bozochegou) :: Portuguese, Brazilian
|
|
||||||
Chris (furesoft) :: German
|
|
||||||
Silvia Isern (eiendragon) :: Catalan
|
|
||||||
Dennis Kron Pedersen (ahjdp) :: Danish
|
|
||||||
iamwhoiamwhoami :: Swedish
|
|
||||||
Grogui :: French
|
|
||||||
MrCharlesIII :: Arabic
|
|
||||||
David Olsen (dawin) :: Danish
|
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,10 +8,10 @@ Homestead.yaml
|
|||||||
.idea
|
.idea
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
/public/dist
|
/public/dist/*.map
|
||||||
/public/plugins
|
/public/plugins
|
||||||
/public/css
|
/public/css/*.map
|
||||||
/public/js
|
/public/js/*.map
|
||||||
/public/bower
|
/public/bower
|
||||||
/public/build/
|
/public/build/
|
||||||
/public/favicon.ico
|
/public/favicon.ico
|
||||||
|
|||||||
@@ -2,18 +2,33 @@
|
|||||||
|
|
||||||
namespace BookStack\Access;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use BookStack\Users\Models\User;
|
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Contracts\Auth\UserProvider;
|
use Illuminate\Contracts\Auth\UserProvider;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class ExternalBaseUserProvider implements UserProvider
|
class ExternalBaseUserProvider implements UserProvider
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected string $model
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance of the model.
|
||||||
|
*/
|
||||||
|
public function createModel(): Model
|
||||||
|
{
|
||||||
|
$class = '\\' . ltrim($this->model, '\\');
|
||||||
|
|
||||||
|
return new $class();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a user by their unique identifier.
|
* Retrieve a user by their unique identifier.
|
||||||
*/
|
*/
|
||||||
public function retrieveById(mixed $identifier): ?Authenticatable
|
public function retrieveById(mixed $identifier): ?Authenticatable
|
||||||
{
|
{
|
||||||
return User::query()->find($identifier);
|
return $this->createModel()->newQuery()->find($identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +59,10 @@ class ExternalBaseUserProvider implements UserProvider
|
|||||||
*/
|
*/
|
||||||
public function retrieveByCredentials(array $credentials): ?Authenticatable
|
public function retrieveByCredentials(array $credentials): ?Authenticatable
|
||||||
{
|
{
|
||||||
return User::query()
|
// Search current user base by looking up a uid
|
||||||
|
$model = $this->createModel();
|
||||||
|
|
||||||
|
return $model->newQuery()
|
||||||
->where('external_auth_id', $credentials['external_auth_id'])
|
->where('external_auth_id', $credentials['external_auth_id'])
|
||||||
->first();
|
->first();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,23 @@
|
|||||||
namespace BookStack\Access\Guards;
|
namespace BookStack\Access\Guards;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* External Auth Session Guard.
|
* Saml2 Session Guard.
|
||||||
*
|
*
|
||||||
* The login process for external auth (SAML2/OIDC) is async in nature, meaning it does not fit very well
|
* The saml2 login process is async in nature meaning it does not fit very well
|
||||||
* into the default laravel 'Guard' auth flow. Instead, most of the logic is done via the relevant
|
* into the default laravel 'Guard' auth flow. Instead most of the logic is done
|
||||||
* controller and services. This class provides a safer, thin version of SessionGuard.
|
* via the Saml2 controller & Saml2Service. This class provides a safer, thin
|
||||||
|
* version of SessionGuard.
|
||||||
*/
|
*/
|
||||||
class AsyncExternalBaseSessionGuard extends ExternalBaseSessionGuard
|
class AsyncExternalBaseSessionGuard extends ExternalBaseSessionGuard
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Validate a user's credentials.
|
* Validate a user's credentials.
|
||||||
|
*
|
||||||
|
* @param array $credentials
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function validate(array $credentials = []): bool
|
public function validate(array $credentials = [])
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -22,9 +27,12 @@ class AsyncExternalBaseSessionGuard extends ExternalBaseSessionGuard
|
|||||||
/**
|
/**
|
||||||
* Attempt to authenticate a user using the given credentials.
|
* Attempt to authenticate a user using the given credentials.
|
||||||
*
|
*
|
||||||
|
* @param array $credentials
|
||||||
* @param bool $remember
|
* @param bool $remember
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function attempt(array $credentials = [], $remember = false): bool
|
public function attempt(array $credentials = [], $remember = false)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace BookStack\Access\Guards;
|
|||||||
|
|
||||||
use BookStack\Access\RegistrationService;
|
use BookStack\Access\RegistrationService;
|
||||||
use Illuminate\Auth\GuardHelpers;
|
use Illuminate\Auth\GuardHelpers;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||||
use Illuminate\Contracts\Auth\StatefulGuard;
|
use Illuminate\Contracts\Auth\StatefulGuard;
|
||||||
use Illuminate\Contracts\Auth\UserProvider;
|
use Illuminate\Contracts\Auth\UserProvider;
|
||||||
use Illuminate\Contracts\Session\Session;
|
use Illuminate\Contracts\Session\Session;
|
||||||
@@ -24,31 +24,43 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
|||||||
* The name of the Guard. Typically "session".
|
* The name of the Guard. Typically "session".
|
||||||
*
|
*
|
||||||
* Corresponds to guard name in authentication configuration.
|
* Corresponds to guard name in authentication configuration.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected readonly string $name;
|
protected $name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user we last attempted to retrieve.
|
* The user we last attempted to retrieve.
|
||||||
|
*
|
||||||
|
* @var \Illuminate\Contracts\Auth\Authenticatable
|
||||||
*/
|
*/
|
||||||
protected Authenticatable|null $lastAttempted;
|
protected $lastAttempted;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The session used by the guard.
|
* The session used by the guard.
|
||||||
|
*
|
||||||
|
* @var \Illuminate\Contracts\Session\Session
|
||||||
*/
|
*/
|
||||||
protected Session $session;
|
protected $session;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if the logout method has been called.
|
* Indicates if the logout method has been called.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
*/
|
*/
|
||||||
protected bool $loggedOut = false;
|
protected $loggedOut = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to handle common registration actions.
|
* Service to handle common registration actions.
|
||||||
|
*
|
||||||
|
* @var RegistrationService
|
||||||
*/
|
*/
|
||||||
protected RegistrationService $registrationService;
|
protected $registrationService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new authentication guard.
|
* Create a new authentication guard.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(string $name, UserProvider $provider, Session $session, RegistrationService $registrationService)
|
public function __construct(string $name, UserProvider $provider, Session $session, RegistrationService $registrationService)
|
||||||
{
|
{
|
||||||
@@ -60,11 +72,13 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the currently authenticated user.
|
* Get the currently authenticated user.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||||
*/
|
*/
|
||||||
public function user(): Authenticatable|null
|
public function user()
|
||||||
{
|
{
|
||||||
if ($this->loggedOut) {
|
if ($this->loggedOut) {
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've already retrieved the user for the current request we can just
|
// If we've already retrieved the user for the current request we can just
|
||||||
@@ -87,11 +101,13 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the ID for the currently authenticated user.
|
* Get the ID for the currently authenticated user.
|
||||||
|
*
|
||||||
|
* @return int|null
|
||||||
*/
|
*/
|
||||||
public function id(): int|null
|
public function id()
|
||||||
{
|
{
|
||||||
if ($this->loggedOut) {
|
if ($this->loggedOut) {
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->user()
|
return $this->user()
|
||||||
@@ -101,8 +117,12 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a user into the application without sessions or cookies.
|
* Log a user into the application without sessions or cookies.
|
||||||
|
*
|
||||||
|
* @param array $credentials
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function once(array $credentials = []): bool
|
public function once(array $credentials = [])
|
||||||
{
|
{
|
||||||
if ($this->validate($credentials)) {
|
if ($this->validate($credentials)) {
|
||||||
$this->setUser($this->lastAttempted);
|
$this->setUser($this->lastAttempted);
|
||||||
@@ -115,8 +135,12 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Log the given user ID into the application without sessions or cookies.
|
* Log the given user ID into the application without sessions or cookies.
|
||||||
|
*
|
||||||
|
* @param mixed $id
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Contracts\Auth\Authenticatable|false
|
||||||
*/
|
*/
|
||||||
public function onceUsingId($id): Authenticatable|false
|
public function onceUsingId($id)
|
||||||
{
|
{
|
||||||
if (!is_null($user = $this->provider->retrieveById($id))) {
|
if (!is_null($user = $this->provider->retrieveById($id))) {
|
||||||
$this->setUser($user);
|
$this->setUser($user);
|
||||||
@@ -129,26 +153,38 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a user's credentials.
|
* Validate a user's credentials.
|
||||||
|
*
|
||||||
|
* @param array $credentials
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function validate(array $credentials = []): bool
|
public function validate(array $credentials = [])
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to authenticate a user using the given credentials.
|
* Attempt to authenticate a user using the given credentials.
|
||||||
* @param bool $remember
|
*
|
||||||
|
* @param array $credentials
|
||||||
|
* @param bool $remember
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function attempt(array $credentials = [], $remember = false): bool
|
public function attempt(array $credentials = [], $remember = false)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log the given user ID into the application.
|
* Log the given user ID into the application.
|
||||||
|
*
|
||||||
|
* @param mixed $id
|
||||||
* @param bool $remember
|
* @param bool $remember
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Contracts\Auth\Authenticatable|false
|
||||||
*/
|
*/
|
||||||
public function loginUsingId(mixed $id, $remember = false): Authenticatable|false
|
public function loginUsingId($id, $remember = false)
|
||||||
{
|
{
|
||||||
// Always return false as to disable this method,
|
// Always return false as to disable this method,
|
||||||
// Logins should route through LoginService.
|
// Logins should route through LoginService.
|
||||||
@@ -158,9 +194,12 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
|||||||
/**
|
/**
|
||||||
* Log a user into the application.
|
* Log a user into the application.
|
||||||
*
|
*
|
||||||
* @param bool $remember
|
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||||
|
* @param bool $remember
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function login(Authenticatable $user, $remember = false): void
|
public function login(AuthenticatableContract $user, $remember = false)
|
||||||
{
|
{
|
||||||
$this->updateSession($user->getAuthIdentifier());
|
$this->updateSession($user->getAuthIdentifier());
|
||||||
|
|
||||||
@@ -169,8 +208,12 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the session with the given ID.
|
* Update the session with the given ID.
|
||||||
|
*
|
||||||
|
* @param string $id
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
*/
|
*/
|
||||||
protected function updateSession(string|int $id): void
|
protected function updateSession($id)
|
||||||
{
|
{
|
||||||
$this->session->put($this->getName(), $id);
|
$this->session->put($this->getName(), $id);
|
||||||
|
|
||||||
@@ -179,8 +222,10 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Log the user out of the application.
|
* Log the user out of the application.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function logout(): void
|
public function logout()
|
||||||
{
|
{
|
||||||
$this->clearUserDataFromStorage();
|
$this->clearUserDataFromStorage();
|
||||||
|
|
||||||
@@ -194,48 +239,62 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the user data from the session and cookies.
|
* Remove the user data from the session and cookies.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
*/
|
*/
|
||||||
protected function clearUserDataFromStorage(): void
|
protected function clearUserDataFromStorage()
|
||||||
{
|
{
|
||||||
$this->session->remove($this->getName());
|
$this->session->remove($this->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last user we attempted to authenticate.
|
* Get the last user we attempted to authenticate.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Contracts\Auth\Authenticatable
|
||||||
*/
|
*/
|
||||||
public function getLastAttempted(): Authenticatable
|
public function getLastAttempted()
|
||||||
{
|
{
|
||||||
return $this->lastAttempted;
|
return $this->lastAttempted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a unique identifier for the auth session value.
|
* Get a unique identifier for the auth session value.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getName(): string
|
public function getName()
|
||||||
{
|
{
|
||||||
return 'login_' . $this->name . '_' . sha1(static::class);
|
return 'login_' . $this->name . '_' . sha1(static::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the user was authenticated via "remember me" cookie.
|
* Determine if the user was authenticated via "remember me" cookie.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function viaRemember(): bool
|
public function viaRemember()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the currently cached user.
|
* Return the currently cached user.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||||
*/
|
*/
|
||||||
public function getUser(): Authenticatable|null
|
public function getUser()
|
||||||
{
|
{
|
||||||
return $this->user;
|
return $this->user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current user.
|
* Set the current user.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function setUser(Authenticatable $user): self
|
public function setUser(AuthenticatableContract $user)
|
||||||
{
|
{
|
||||||
$this->user = $user;
|
$this->user = $user;
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,13 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
|||||||
/**
|
/**
|
||||||
* Validate a user's credentials.
|
* Validate a user's credentials.
|
||||||
*
|
*
|
||||||
|
* @param array $credentials
|
||||||
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function validate(array $credentials = []): bool
|
public function validate(array $credentials = [])
|
||||||
{
|
{
|
||||||
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
|
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
|
||||||
|
|
||||||
@@ -53,13 +57,16 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
|||||||
/**
|
/**
|
||||||
* Attempt to authenticate a user using the given credentials.
|
* Attempt to authenticate a user using the given credentials.
|
||||||
*
|
*
|
||||||
|
* @param array $credentials
|
||||||
* @param bool $remember
|
* @param bool $remember
|
||||||
*
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException*@throws \BookStack\Exceptions\JsonDebugException
|
||||||
* @throws LoginAttemptException
|
* @throws LoginAttemptException
|
||||||
* @throws JsonDebugException
|
* @throws JsonDebugException
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function attempt(array $credentials = [], $remember = false): bool
|
public function attempt(array $credentials = [], $remember = false)
|
||||||
{
|
{
|
||||||
$username = $credentials['username'];
|
$username = $credentials['username'];
|
||||||
$userDetails = $this->ldapService->getUserDetails($username);
|
$userDetails = $this->ldapService->getUserDetails($username);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use BookStack\Exceptions\LoginAttemptInvalidUserException;
|
|||||||
use BookStack\Exceptions\StoppedAuthenticationException;
|
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Facades\Theme;
|
use BookStack\Facades\Theme;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Theming\ThemeEvents;
|
use BookStack\Theming\ThemeEvents;
|
||||||
use BookStack\Users\Models\User;
|
use BookStack\Users\Models\User;
|
||||||
use Exception;
|
use Exception;
|
||||||
@@ -51,7 +50,7 @@ class LoginService
|
|||||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $method, $user);
|
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $method, $user);
|
||||||
|
|
||||||
// Authenticate on all session guards if a likely admin
|
// Authenticate on all session guards if a likely admin
|
||||||
if ($user->can(Permission::UsersManage) && $user->can(Permission::UserRolesManage)) {
|
if ($user->can('users-manage') && $user->can('user-roles-manage')) {
|
||||||
$guards = ['standard', 'ldap', 'saml2', 'oidc'];
|
$guards = ['standard', 'ldap', 'saml2', 'oidc'];
|
||||||
foreach ($guards as $guard) {
|
foreach ($guards as $guard) {
|
||||||
auth($guard)->login($user);
|
auth($guard)->login($user);
|
||||||
@@ -96,7 +95,7 @@ class LoginService
|
|||||||
{
|
{
|
||||||
$value = session()->get(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
|
$value = session()->get(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
|
||||||
if (!$value) {
|
if (!$value) {
|
||||||
return ['user_id' => null, 'method' => null, 'remember' => false];
|
return ['user_id' => null, 'method' => null];
|
||||||
}
|
}
|
||||||
|
|
||||||
[$id, $method, $remember, $time] = explode(':', $value);
|
[$id, $method, $remember, $time] = explode(':', $value);
|
||||||
@@ -104,18 +103,18 @@ class LoginService
|
|||||||
if ($time < $hourAgo) {
|
if ($time < $hourAgo) {
|
||||||
$this->clearLastLoginAttempted();
|
$this->clearLastLoginAttempted();
|
||||||
|
|
||||||
return ['user_id' => null, 'method' => null, 'remember' => false];
|
return ['user_id' => null, 'method' => null];
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['user_id' => $id, 'method' => $method, 'remember' => boolval($remember)];
|
return ['user_id' => $id, 'method' => $method, 'remember' => boolval($remember)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the last login-attempted user.
|
* Set the last login attempted user.
|
||||||
* Must be only used when credentials are correct and a login could be
|
* Must be only used when credentials are correct and a login could be
|
||||||
* achieved, but a secondary factor has stopped the login.
|
* achieved but a secondary factor has stopped the login.
|
||||||
*/
|
*/
|
||||||
protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember): void
|
protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember)
|
||||||
{
|
{
|
||||||
session()->put(
|
session()->put(
|
||||||
self::LAST_LOGIN_ATTEMPTED_SESSION_KEY,
|
self::LAST_LOGIN_ATTEMPTED_SESSION_KEY,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use BookStack\Exceptions\UserRegistrationException;
|
|||||||
use BookStack\Facades\Theme;
|
use BookStack\Facades\Theme;
|
||||||
use BookStack\Http\HttpRequestService;
|
use BookStack\Http\HttpRequestService;
|
||||||
use BookStack\Theming\ThemeEvents;
|
use BookStack\Theming\ThemeEvents;
|
||||||
use BookStack\Uploads\UserAvatars;
|
|
||||||
use BookStack\Users\Models\User;
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
|
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
|
||||||
@@ -27,8 +26,7 @@ class OidcService
|
|||||||
protected RegistrationService $registrationService,
|
protected RegistrationService $registrationService,
|
||||||
protected LoginService $loginService,
|
protected LoginService $loginService,
|
||||||
protected HttpRequestService $http,
|
protected HttpRequestService $http,
|
||||||
protected GroupSyncService $groupService,
|
protected GroupSyncService $groupService
|
||||||
protected UserAvatars $userAvatars
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,10 +220,6 @@ class OidcService
|
|||||||
throw new OidcException($exception->getMessage());
|
throw new OidcException($exception->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->config()['fetch_avatar'] && !$user->avatar()->exists() && $userDetails->picture) {
|
|
||||||
$this->userAvatars->assignToUserFromUrl($user, $userDetails->picture);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->shouldSyncGroups()) {
|
if ($this->shouldSyncGroups()) {
|
||||||
$detachExisting = $this->config()['remove_from_groups'];
|
$detachExisting = $this->config()['remove_from_groups'];
|
||||||
$this->groupService->syncUserWithFoundGroups($user, $userDetails->groups ?? [], $detachExisting);
|
$this->groupService->syncUserWithFoundGroups($user, $userDetails->groups ?? [], $detachExisting);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ class OidcUserDetails
|
|||||||
public ?string $email = null,
|
public ?string $email = null,
|
||||||
public ?string $name = null,
|
public ?string $name = null,
|
||||||
public ?array $groups = null,
|
public ?array $groups = null,
|
||||||
public ?string $picture = null,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,16 +40,15 @@ class OidcUserDetails
|
|||||||
$this->email = $claims->getClaim('email') ?? $this->email;
|
$this->email = $claims->getClaim('email') ?? $this->email;
|
||||||
$this->name = static::getUserDisplayName($displayNameClaims, $claims) ?? $this->name;
|
$this->name = static::getUserDisplayName($displayNameClaims, $claims) ?? $this->name;
|
||||||
$this->groups = static::getUserGroups($groupsClaim, $claims) ?? $this->groups;
|
$this->groups = static::getUserGroups($groupsClaim, $claims) ?? $this->groups;
|
||||||
$this->picture = static::getPicture($claims) ?: $this->picture;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function getUserDisplayName(string $displayNameClaims, ProvidesClaims $claims): string
|
protected static function getUserDisplayName(string $displayNameClaims, ProvidesClaims $token): string
|
||||||
{
|
{
|
||||||
$displayNameClaimParts = explode('|', $displayNameClaims);
|
$displayNameClaimParts = explode('|', $displayNameClaims);
|
||||||
|
|
||||||
$displayName = [];
|
$displayName = [];
|
||||||
foreach ($displayNameClaimParts as $claim) {
|
foreach ($displayNameClaimParts as $claim) {
|
||||||
$component = $claims->getClaim(trim($claim)) ?? '';
|
$component = $token->getClaim(trim($claim)) ?? '';
|
||||||
if ($component !== '') {
|
if ($component !== '') {
|
||||||
$displayName[] = $component;
|
$displayName[] = $component;
|
||||||
}
|
}
|
||||||
@@ -59,13 +57,13 @@ class OidcUserDetails
|
|||||||
return implode(' ', $displayName);
|
return implode(' ', $displayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function getUserGroups(string $groupsClaim, ProvidesClaims $claims): ?array
|
protected static function getUserGroups(string $groupsClaim, ProvidesClaims $token): ?array
|
||||||
{
|
{
|
||||||
if (empty($groupsClaim)) {
|
if (empty($groupsClaim)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$groupsList = Arr::get($claims->getAllClaims(), $groupsClaim);
|
$groupsList = Arr::get($token->getAllClaims(), $groupsClaim);
|
||||||
if (!is_array($groupsList)) {
|
if (!is_array($groupsList)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -74,14 +72,4 @@ class OidcUserDetails
|
|||||||
return is_string($val);
|
return is_string($val);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function getPicture(ProvidesClaims $claims): ?string
|
|
||||||
{
|
|
||||||
$picture = $claims->getClaim('picture');
|
|
||||||
if (is_string($picture) && str_starts_with($picture, 'http')) {
|
|
||||||
return $picture;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class Saml2Service
|
|||||||
* Returns the SAML2 request ID, and the URL to redirect the user to.
|
* Returns the SAML2 request ID, and the URL to redirect the user to.
|
||||||
*
|
*
|
||||||
* @throws Error
|
* @throws Error
|
||||||
* @return array{url: string, id: ?string}
|
* @returns array{url: string, id: ?string}
|
||||||
*/
|
*/
|
||||||
public function logout(User $user): array
|
public function logout(User $user): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class SocialDriverManager
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the names of the active social drivers, keyed by driver id.
|
* Gets the names of the active social drivers, keyed by driver id.
|
||||||
* @return array<string, string>
|
* @returns array<string, string>
|
||||||
*/
|
*/
|
||||||
public function getActive(): array
|
public function getActive(): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use BookStack\Entities\Tools\MixedEntityListLoader;
|
|||||||
use BookStack\Permissions\PermissionApplicator;
|
use BookStack\Permissions\PermissionApplicator;
|
||||||
use BookStack\Users\Models\User;
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
|
|
||||||
class ActivityQueries
|
class ActivityQueries
|
||||||
@@ -68,7 +67,6 @@ class ActivityQueries
|
|||||||
|
|
||||||
$activity = $query->orderBy('created_at', 'desc')
|
$activity = $query->orderBy('created_at', 'desc')
|
||||||
->with(['loggable' => function (Relation $query) {
|
->with(['loggable' => function (Relation $query) {
|
||||||
/** @var MorphTo<Entity, Activity> $query */
|
|
||||||
$query->withTrashed();
|
$query->withTrashed();
|
||||||
}, 'user.avatar'])
|
}, 'user.avatar'])
|
||||||
->skip($count * ($page - 1))
|
->skip($count * ($page - 1))
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ namespace BookStack\Activity;
|
|||||||
|
|
||||||
use BookStack\Activity\Models\Comment;
|
use BookStack\Activity\Models\Comment;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Exceptions\NotifyException;
|
|
||||||
use BookStack\Exceptions\PrettyException;
|
|
||||||
use BookStack\Facades\Activity as ActivityService;
|
use BookStack\Facades\Activity as ActivityService;
|
||||||
use BookStack\Util\HtmlDescriptionFilter;
|
use BookStack\Util\HtmlDescriptionFilter;
|
||||||
|
|
||||||
@@ -22,7 +20,7 @@ class CommentRepo
|
|||||||
/**
|
/**
|
||||||
* Create a new comment on an entity.
|
* Create a new comment on an entity.
|
||||||
*/
|
*/
|
||||||
public function create(Entity $entity, string $html, ?int $parentId, string $contentRef): Comment
|
public function create(Entity $entity, string $html, ?int $parent_id): Comment
|
||||||
{
|
{
|
||||||
$userId = user()->id;
|
$userId = user()->id;
|
||||||
$comment = new Comment();
|
$comment = new Comment();
|
||||||
@@ -31,8 +29,7 @@ class CommentRepo
|
|||||||
$comment->created_by = $userId;
|
$comment->created_by = $userId;
|
||||||
$comment->updated_by = $userId;
|
$comment->updated_by = $userId;
|
||||||
$comment->local_id = $this->getNextLocalId($entity);
|
$comment->local_id = $this->getNextLocalId($entity);
|
||||||
$comment->parent_id = $parentId;
|
$comment->parent_id = $parent_id;
|
||||||
$comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $contentRef) === 1 ? $contentRef : '';
|
|
||||||
|
|
||||||
$entity->comments()->save($comment);
|
$entity->comments()->save($comment);
|
||||||
ActivityService::add(ActivityType::COMMENT_CREATE, $comment);
|
ActivityService::add(ActivityType::COMMENT_CREATE, $comment);
|
||||||
@@ -55,41 +52,6 @@ class CommentRepo
|
|||||||
return $comment;
|
return $comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Archive an existing comment.
|
|
||||||
*/
|
|
||||||
public function archive(Comment $comment): Comment
|
|
||||||
{
|
|
||||||
if ($comment->parent_id) {
|
|
||||||
throw new NotifyException('Only top-level comments can be archived.', '/', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$comment->archived = true;
|
|
||||||
$comment->save();
|
|
||||||
|
|
||||||
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
|
|
||||||
|
|
||||||
return $comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Un-archive an existing comment.
|
|
||||||
*/
|
|
||||||
public function unarchive(Comment $comment): Comment
|
|
||||||
{
|
|
||||||
if ($comment->parent_id) {
|
|
||||||
throw new NotifyException('Only top-level comments can be un-archived.', '/', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$comment->archived = false;
|
|
||||||
$comment->save();
|
|
||||||
|
|
||||||
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
|
|
||||||
|
|
||||||
return $comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a comment from the system.
|
* Delete a comment from the system.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace BookStack\Activity\Controllers;
|
|||||||
|
|
||||||
use BookStack\Activity\Models\Activity;
|
use BookStack\Activity\Models\Activity;
|
||||||
use BookStack\Http\ApiController;
|
use BookStack\Http\ApiController;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
|
|
||||||
class AuditLogApiController extends ApiController
|
class AuditLogApiController extends ApiController
|
||||||
{
|
{
|
||||||
@@ -17,8 +16,8 @@ class AuditLogApiController extends ApiController
|
|||||||
*/
|
*/
|
||||||
public function list()
|
public function list()
|
||||||
{
|
{
|
||||||
$this->checkPermission(Permission::SettingsManage);
|
$this->checkPermission('settings-manage');
|
||||||
$this->checkPermission(Permission::UsersManage);
|
$this->checkPermission('users-manage');
|
||||||
|
|
||||||
$query = Activity::query()->with(['user']);
|
$query = Activity::query()->with(['user']);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace BookStack\Activity\Controllers;
|
|||||||
use BookStack\Activity\ActivityType;
|
use BookStack\Activity\ActivityType;
|
||||||
use BookStack\Activity\Models\Activity;
|
use BookStack\Activity\Models\Activity;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Sorting\SortUrl;
|
use BookStack\Sorting\SortUrl;
|
||||||
use BookStack\Util\SimpleListOptions;
|
use BookStack\Util\SimpleListOptions;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -14,8 +13,8 @@ class AuditLogController extends Controller
|
|||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$this->checkPermission(Permission::SettingsManage);
|
$this->checkPermission('settings-manage');
|
||||||
$this->checkPermission(Permission::UsersManage);
|
$this->checkPermission('users-manage');
|
||||||
|
|
||||||
$sort = $request->get('sort', 'activity_date');
|
$sort = $request->get('sort', 'activity_date');
|
||||||
$order = $request->get('order', 'desc');
|
$order = $request->get('order', 'desc');
|
||||||
|
|||||||
@@ -3,11 +3,8 @@
|
|||||||
namespace BookStack\Activity\Controllers;
|
namespace BookStack\Activity\Controllers;
|
||||||
|
|
||||||
use BookStack\Activity\CommentRepo;
|
use BookStack\Activity\CommentRepo;
|
||||||
use BookStack\Activity\Tools\CommentTree;
|
|
||||||
use BookStack\Activity\Tools\CommentTreeNode;
|
|
||||||
use BookStack\Entities\Queries\PageQueries;
|
use BookStack\Entities\Queries\PageQueries;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
@@ -29,7 +26,6 @@ class CommentController extends Controller
|
|||||||
$input = $this->validate($request, [
|
$input = $this->validate($request, [
|
||||||
'html' => ['required', 'string'],
|
'html' => ['required', 'string'],
|
||||||
'parent_id' => ['nullable', 'integer'],
|
'parent_id' => ['nullable', 'integer'],
|
||||||
'content_ref' => ['string'],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$page = $this->pageQueries->findVisibleById($pageId);
|
$page = $this->pageQueries->findVisibleById($pageId);
|
||||||
@@ -43,13 +39,15 @@ class CommentController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new comment.
|
// Create a new comment.
|
||||||
$this->checkPermission(Permission::CommentCreateAll);
|
$this->checkPermission('comment-create-all');
|
||||||
$contentRef = $input['content_ref'] ?? '';
|
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null);
|
||||||
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $contentRef);
|
|
||||||
|
|
||||||
return view('comments.comment-branch', [
|
return view('comments.comment-branch', [
|
||||||
'readOnly' => false,
|
'readOnly' => false,
|
||||||
'branch' => new CommentTreeNode($comment, 0, []),
|
'branch' => [
|
||||||
|
'comment' => $comment,
|
||||||
|
'children' => [],
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,8 +63,8 @@ class CommentController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$comment = $this->commentRepo->getById($commentId);
|
$comment = $this->commentRepo->getById($commentId);
|
||||||
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
|
$this->checkOwnablePermission('page-view', $comment->entity);
|
||||||
$this->checkOwnablePermission(Permission::CommentUpdate, $comment);
|
$this->checkOwnablePermission('comment-update', $comment);
|
||||||
|
|
||||||
$comment = $this->commentRepo->update($comment, $input['html']);
|
$comment = $this->commentRepo->update($comment, $input['html']);
|
||||||
|
|
||||||
@@ -76,53 +74,13 @@ class CommentController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark a comment as archived.
|
|
||||||
*/
|
|
||||||
public function archive(int $id)
|
|
||||||
{
|
|
||||||
$comment = $this->commentRepo->getById($id);
|
|
||||||
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
|
|
||||||
if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) {
|
|
||||||
$this->showPermissionError();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->commentRepo->archive($comment);
|
|
||||||
|
|
||||||
$tree = new CommentTree($comment->entity);
|
|
||||||
return view('comments.comment-branch', [
|
|
||||||
'readOnly' => false,
|
|
||||||
'branch' => $tree->getCommentNodeForId($id),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unmark a comment as archived.
|
|
||||||
*/
|
|
||||||
public function unarchive(int $id)
|
|
||||||
{
|
|
||||||
$comment = $this->commentRepo->getById($id);
|
|
||||||
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
|
|
||||||
if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) {
|
|
||||||
$this->showPermissionError();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->commentRepo->unarchive($comment);
|
|
||||||
|
|
||||||
$tree = new CommentTree($comment->entity);
|
|
||||||
return view('comments.comment-branch', [
|
|
||||||
'readOnly' => false,
|
|
||||||
'branch' => $tree->getCommentNodeForId($id),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a comment from the system.
|
* Delete a comment from the system.
|
||||||
*/
|
*/
|
||||||
public function destroy(int $id)
|
public function destroy(int $id)
|
||||||
{
|
{
|
||||||
$comment = $this->commentRepo->getById($id);
|
$comment = $this->commentRepo->getById($id);
|
||||||
$this->checkOwnablePermission(Permission::CommentDelete, $comment);
|
$this->checkOwnablePermission('comment-delete', $comment);
|
||||||
|
|
||||||
$this->commentRepo->delete($comment);
|
$this->commentRepo->delete($comment);
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ namespace BookStack\Activity\Controllers;
|
|||||||
use BookStack\Activity\Tools\UserEntityWatchOptions;
|
use BookStack\Activity\Tools\UserEntityWatchOptions;
|
||||||
use BookStack\Entities\Tools\MixedEntityRequestHelper;
|
use BookStack\Entities\Tools\MixedEntityRequestHelper;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class WatchController extends Controller
|
class WatchController extends Controller
|
||||||
{
|
{
|
||||||
public function update(Request $request, MixedEntityRequestHelper $entityHelper)
|
public function update(Request $request, MixedEntityRequestHelper $entityHelper)
|
||||||
{
|
{
|
||||||
$this->checkPermission(Permission::ReceiveNotifications);
|
$this->checkPermission('receive-notifications');
|
||||||
$this->preventGuestAccess();
|
$this->preventGuestAccess();
|
||||||
|
|
||||||
$requestData = $this->validate($request, array_merge([
|
$requestData = $this->validate($request, array_merge([
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use BookStack\Activity\ActivityType;
|
|||||||
use BookStack\Activity\Models\Webhook;
|
use BookStack\Activity\Models\Webhook;
|
||||||
use BookStack\Activity\Queries\WebhooksAllPaginatedAndSorted;
|
use BookStack\Activity\Queries\WebhooksAllPaginatedAndSorted;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Util\SimpleListOptions;
|
use BookStack\Util\SimpleListOptions;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ class WebhookController extends Controller
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->middleware([
|
$this->middleware([
|
||||||
Permission::SettingsManage->middleware()
|
'can:settings-manage',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ namespace BookStack\Activity\Models;
|
|||||||
|
|
||||||
use BookStack\App\Model;
|
use BookStack\App\Model;
|
||||||
use BookStack\Users\Models\HasCreatorAndUpdater;
|
use BookStack\Users\Models\HasCreatorAndUpdater;
|
||||||
use BookStack\Users\Models\OwnableInterface;
|
|
||||||
use BookStack\Users\Models\User;
|
|
||||||
use BookStack\Util\HtmlContentFilter;
|
use BookStack\Util\HtmlContentFilter;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
@@ -19,10 +17,10 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|||||||
* @property int $local_id
|
* @property int $local_id
|
||||||
* @property string $entity_type
|
* @property string $entity_type
|
||||||
* @property int $entity_id
|
* @property int $entity_id
|
||||||
* @property string $content_ref
|
* @property int $created_by
|
||||||
* @property bool $archived
|
* @property int $updated_by
|
||||||
*/
|
*/
|
||||||
class Comment extends Model implements Loggable, OwnableInterface
|
class Comment extends Model implements Loggable
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use HasCreatorAndUpdater;
|
use HasCreatorAndUpdater;
|
||||||
@@ -39,7 +37,6 @@ class Comment extends Model implements Loggable, OwnableInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the parent comment this is in reply to (if existing).
|
* Get the parent comment this is in reply to (if existing).
|
||||||
* @return BelongsTo<Comment, $this>
|
|
||||||
*/
|
*/
|
||||||
public function parent(): BelongsTo
|
public function parent(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $value
|
* @property string $value
|
||||||
* @property int $entity_id
|
|
||||||
* @property string $entity_type
|
|
||||||
* @property int $order
|
* @property int $order
|
||||||
*/
|
*/
|
||||||
class Tag extends Model
|
class Tag extends Model
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace BookStack\Activity\Notifications\Handlers;
|
|||||||
use BookStack\Activity\Models\Loggable;
|
use BookStack\Activity\Models\Loggable;
|
||||||
use BookStack\Activity\Notifications\Messages\BaseActivityNotification;
|
use BookStack\Activity\Notifications\Messages\BaseActivityNotification;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Permissions\PermissionApplicator;
|
use BookStack\Permissions\PermissionApplicator;
|
||||||
use BookStack\Users\Models\User;
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
@@ -27,7 +26,7 @@ abstract class BaseNotificationHandler implements NotificationHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prevent sending of the user does not have notification permissions
|
// Prevent sending of the user does not have notification permissions
|
||||||
if (!$user->can(Permission::ReceiveNotifications)) {
|
if (!$user->can('receive-notifications')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ class PageUpdateNotificationHandler extends BaseNotificationHandler
|
|||||||
throw new \InvalidArgumentException("Detail for page update notifications must be a page");
|
throw new \InvalidArgumentException("Detail for page update notifications must be a page");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the last update from activity
|
// Get last update from activity
|
||||||
/** @var ?Activity $lastUpdate */
|
|
||||||
$lastUpdate = $detail->activity()
|
$lastUpdate = $detail->activity()
|
||||||
->where('type', '=', ActivityType::PAGE_UPDATE)
|
->where('type', '=', ActivityType::PAGE_UPDATE)
|
||||||
->where('id', '!=', $activity->id)
|
->where('id', '!=', $activity->id)
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ namespace BookStack\Activity\Tools;
|
|||||||
|
|
||||||
use BookStack\Activity\Models\Comment;
|
use BookStack\Activity\Models\Comment;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
|
|
||||||
class CommentTree
|
class CommentTree
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The built nested tree structure array.
|
* The built nested tree structure array.
|
||||||
* @var CommentTreeNode[]
|
* @var array{comment: Comment, depth: int, children: array}[]
|
||||||
*/
|
*/
|
||||||
protected array $tree;
|
protected array $tree;
|
||||||
protected array $comments;
|
protected array $comments;
|
||||||
@@ -29,7 +28,7 @@ class CommentTree
|
|||||||
|
|
||||||
public function empty(): bool
|
public function empty(): bool
|
||||||
{
|
{
|
||||||
return count($this->getActive()) === 0;
|
return count($this->tree) === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function count(): int
|
public function count(): int
|
||||||
@@ -37,41 +36,15 @@ class CommentTree
|
|||||||
return count($this->comments);
|
return count($this->comments);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getActive(): array
|
public function get(): array
|
||||||
{
|
{
|
||||||
return array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived);
|
return $this->tree;
|
||||||
}
|
|
||||||
|
|
||||||
public function activeThreadCount(): int
|
|
||||||
{
|
|
||||||
return count($this->getActive());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getArchived(): array
|
|
||||||
{
|
|
||||||
return array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function archivedThreadCount(): int
|
|
||||||
{
|
|
||||||
return count($this->getArchived());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCommentNodeForId(int $commentId): ?CommentTreeNode
|
|
||||||
{
|
|
||||||
foreach ($this->tree as $node) {
|
|
||||||
if ($node->comment->id === $commentId) {
|
|
||||||
return $node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function canUpdateAny(): bool
|
public function canUpdateAny(): bool
|
||||||
{
|
{
|
||||||
foreach ($this->comments as $comment) {
|
foreach ($this->comments as $comment) {
|
||||||
if (userCan(Permission::CommentUpdate, $comment)) {
|
if (userCan('comment-update', $comment)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +54,6 @@ class CommentTree
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Comment[] $comments
|
* @param Comment[] $comments
|
||||||
* @return CommentTreeNode[]
|
|
||||||
*/
|
*/
|
||||||
protected function createTree(array $comments): array
|
protected function createTree(array $comments): array
|
||||||
{
|
{
|
||||||
@@ -105,22 +77,26 @@ class CommentTree
|
|||||||
|
|
||||||
$tree = [];
|
$tree = [];
|
||||||
foreach ($childMap[0] ?? [] as $childId) {
|
foreach ($childMap[0] ?? [] as $childId) {
|
||||||
$tree[] = $this->createTreeNodeForId($childId, 0, $byId, $childMap);
|
$tree[] = $this->createTreeForId($childId, 0, $byId, $childMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $tree;
|
return $tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createTreeNodeForId(int $id, int $depth, array &$byId, array &$childMap): CommentTreeNode
|
protected function createTreeForId(int $id, int $depth, array &$byId, array &$childMap): array
|
||||||
{
|
{
|
||||||
$childIds = $childMap[$id] ?? [];
|
$childIds = $childMap[$id] ?? [];
|
||||||
$children = [];
|
$children = [];
|
||||||
|
|
||||||
foreach ($childIds as $childId) {
|
foreach ($childIds as $childId) {
|
||||||
$children[] = $this->createTreeNodeForId($childId, $depth + 1, $byId, $childMap);
|
$children[] = $this->createTreeForId($childId, $depth + 1, $byId, $childMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CommentTreeNode($byId[$id], $depth, $children);
|
return [
|
||||||
|
'comment' => $byId[$id],
|
||||||
|
'depth' => $depth,
|
||||||
|
'children' => $children,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function loadComments(): array
|
protected function loadComments(): array
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Activity\Tools;
|
|
||||||
|
|
||||||
use BookStack\Activity\Models\Comment;
|
|
||||||
|
|
||||||
class CommentTreeNode
|
|
||||||
{
|
|
||||||
public Comment $comment;
|
|
||||||
public int $depth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var CommentTreeNode[]
|
|
||||||
*/
|
|
||||||
public array $children;
|
|
||||||
|
|
||||||
public function __construct(Comment $comment, int $depth, array $children)
|
|
||||||
{
|
|
||||||
$this->comment = $comment;
|
|
||||||
$this->depth = $depth;
|
|
||||||
$this->children = $children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,16 +3,17 @@
|
|||||||
namespace BookStack\Activity\Tools;
|
namespace BookStack\Activity\Tools;
|
||||||
|
|
||||||
use BookStack\Activity\Models\Tag;
|
use BookStack\Activity\Models\Tag;
|
||||||
use BookStack\Entities\Models\BookChild;
|
|
||||||
use BookStack\Entities\Models\Entity;
|
|
||||||
use BookStack\Entities\Models\Page;
|
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
|
|
||||||
class TagClassGenerator
|
class TagClassGenerator
|
||||||
{
|
{
|
||||||
public function __construct(
|
protected array $tags;
|
||||||
protected Entity $entity
|
|
||||||
) {
|
/**
|
||||||
|
* @param Tag[] $tags
|
||||||
|
*/
|
||||||
|
public function __construct(array $tags)
|
||||||
|
{
|
||||||
|
$this->tags = $tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,23 +22,14 @@ class TagClassGenerator
|
|||||||
public function generate(): array
|
public function generate(): array
|
||||||
{
|
{
|
||||||
$classes = [];
|
$classes = [];
|
||||||
$tags = $this->entity->tags->all();
|
|
||||||
|
|
||||||
foreach ($tags as $tag) {
|
foreach ($this->tags as $tag) {
|
||||||
array_push($classes, ...$this->generateClassesForTag($tag));
|
$name = $this->normalizeTagClassString($tag->name);
|
||||||
}
|
$value = $this->normalizeTagClassString($tag->value);
|
||||||
|
$classes[] = 'tag-name-' . $name;
|
||||||
if ($this->entity instanceof BookChild && userCan(Permission::BookView, $this->entity->book)) {
|
if ($value) {
|
||||||
$bookTags = $this->entity->book->tags;
|
$classes[] = 'tag-value-' . $value;
|
||||||
foreach ($bookTags as $bookTag) {
|
$classes[] = 'tag-pair-' . $name . '-' . $value;
|
||||||
array_push($classes, ...$this->generateClassesForTag($bookTag, 'book-'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->entity instanceof Page && $this->entity->chapter && userCan(Permission::ChapterView, $this->entity->chapter)) {
|
|
||||||
$chapterTags = $this->entity->chapter->tags;
|
|
||||||
foreach ($chapterTags as $chapterTag) {
|
|
||||||
array_push($classes, ...$this->generateClassesForTag($chapterTag, 'chapter-'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,22 +41,6 @@ class TagClassGenerator
|
|||||||
return implode(' ', $this->generate());
|
return implode(' ', $this->generate());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string[]
|
|
||||||
*/
|
|
||||||
protected function generateClassesForTag(Tag $tag, string $prefix = ''): array
|
|
||||||
{
|
|
||||||
$classes = [];
|
|
||||||
$name = $this->normalizeTagClassString($tag->name);
|
|
||||||
$value = $this->normalizeTagClassString($tag->value);
|
|
||||||
$classes[] = "{$prefix}tag-name-{$name}";
|
|
||||||
if ($value) {
|
|
||||||
$classes[] = "{$prefix}tag-value-{$value}";
|
|
||||||
$classes[] = "{$prefix}tag-pair-{$name}-{$value}";
|
|
||||||
}
|
|
||||||
return $classes;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function normalizeTagClassString(string $value): string
|
protected function normalizeTagClassString(string $value): string
|
||||||
{
|
{
|
||||||
$value = str_replace(' ', '', strtolower($value));
|
$value = str_replace(' ', '', strtolower($value));
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use BookStack\Activity\WatchLevels;
|
|||||||
use BookStack\Entities\Models\BookChild;
|
use BookStack\Entities\Models\BookChild;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Users\Models\User;
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ class UserEntityWatchOptions
|
|||||||
|
|
||||||
public function canWatch(): bool
|
public function canWatch(): bool
|
||||||
{
|
{
|
||||||
return $this->user->can(Permission::ReceiveNotifications) && !$this->user->isGuest();
|
return $this->user->can('receive-notifications') && !$this->user->isGuest();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getWatchLevel(): string
|
public function getWatchLevel(): string
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class WebhookFormatter
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->detail instanceof Model) {
|
if ($this->detail instanceof Model) {
|
||||||
$data['related_item'] = $this->formatModel($this->detail);
|
$data['related_item'] = $this->formatModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
@@ -83,8 +83,10 @@ class WebhookFormatter
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function formatModel(Model $model): array
|
protected function formatModel(): array
|
||||||
{
|
{
|
||||||
|
/** @var Model $model */
|
||||||
|
$model = $this->detail;
|
||||||
$model->unsetRelations();
|
$model->unsetRelations();
|
||||||
|
|
||||||
foreach ($this->modelFormatters as $formatter) {
|
foreach ($this->modelFormatters as $formatter) {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class WatchLevels
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the possible values as an option_name => value array.
|
* Get all the possible values as an option_name => value array.
|
||||||
* @return array<string, int>
|
* @returns array<string, int>
|
||||||
*/
|
*/
|
||||||
public static function all(): array
|
public static function all(): array
|
||||||
{
|
{
|
||||||
@@ -50,7 +50,7 @@ class WatchLevels
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the watch options suited for the given entity.
|
* Get the watch options suited for the given entity.
|
||||||
* @return array<string, int>
|
* @returns array<string, int>
|
||||||
*/
|
*/
|
||||||
public static function allSuitedFor(Entity $entity): array
|
public static function allSuitedFor(Entity $entity): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace BookStack\Api;
|
namespace BookStack\Api;
|
||||||
|
|
||||||
use BookStack\App\AppVersion;
|
|
||||||
use BookStack\Http\ApiController;
|
use BookStack\Http\ApiController;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||||
@@ -26,7 +25,7 @@ class ApiDocsGenerator
|
|||||||
*/
|
*/
|
||||||
public static function generateConsideringCache(): Collection
|
public static function generateConsideringCache(): Collection
|
||||||
{
|
{
|
||||||
$appVersion = AppVersion::get();
|
$appVersion = trim(file_get_contents(base_path('version')));
|
||||||
$cacheKey = 'api-docs::' . $appVersion;
|
$cacheKey = 'api-docs::' . $appVersion;
|
||||||
$isProduction = config('app.env') === 'production';
|
$isProduction = config('app.env') === 'production';
|
||||||
$cacheVal = $isProduction ? Cache::get($cacheKey) : null;
|
$cacheVal = $isProduction ? Cache::get($cacheKey) : null;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace BookStack\Api;
|
|||||||
|
|
||||||
use BookStack\Access\LoginService;
|
use BookStack\Access\LoginService;
|
||||||
use BookStack\Exceptions\ApiAuthException;
|
use BookStack\Exceptions\ApiAuthException;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use Illuminate\Auth\GuardHelpers;
|
use Illuminate\Auth\GuardHelpers;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Contracts\Auth\Guard;
|
use Illuminate\Contracts\Auth\Guard;
|
||||||
@@ -147,7 +146,7 @@ class ApiTokenGuard implements Guard
|
|||||||
throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);
|
throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$token->user->can(Permission::AccessApi)) {
|
if (!$token->user->can('access-api')) {
|
||||||
throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
|
throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace BookStack\Api;
|
|||||||
|
|
||||||
use BookStack\Activity\ActivityType;
|
use BookStack\Activity\ActivityType;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Users\Models\User;
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
@@ -17,8 +16,8 @@ class UserApiTokenController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function create(Request $request, int $userId)
|
public function create(Request $request, int $userId)
|
||||||
{
|
{
|
||||||
$this->checkPermission(Permission::AccessApi);
|
$this->checkPermission('access-api');
|
||||||
$this->checkPermissionOrCurrentUser(Permission::UsersManage, $userId);
|
$this->checkPermissionOrCurrentUser('users-manage', $userId);
|
||||||
$this->updateContext($request);
|
$this->updateContext($request);
|
||||||
|
|
||||||
$user = User::query()->findOrFail($userId);
|
$user = User::query()->findOrFail($userId);
|
||||||
@@ -36,8 +35,8 @@ class UserApiTokenController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(Request $request, int $userId)
|
public function store(Request $request, int $userId)
|
||||||
{
|
{
|
||||||
$this->checkPermission(Permission::AccessApi);
|
$this->checkPermission('access-api');
|
||||||
$this->checkPermissionOrCurrentUser(Permission::UsersManage, $userId);
|
$this->checkPermissionOrCurrentUser('users-manage', $userId);
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'name' => ['required', 'max:250'],
|
'name' => ['required', 'max:250'],
|
||||||
@@ -144,8 +143,8 @@ class UserApiTokenController extends Controller
|
|||||||
*/
|
*/
|
||||||
protected function checkPermissionAndFetchUserToken(int $userId, int $tokenId): array
|
protected function checkPermissionAndFetchUserToken(int $userId, int $tokenId): array
|
||||||
{
|
{
|
||||||
$this->checkPermissionOr(Permission::UsersManage, function () use ($userId) {
|
$this->checkPermissionOr('users-manage', function () use ($userId) {
|
||||||
return $userId === user()->id && userCan(Permission::AccessApi);
|
return $userId === user()->id && userCan('access-api');
|
||||||
});
|
});
|
||||||
|
|
||||||
$user = User::query()->findOrFail($userId);
|
$user = User::query()->findOrFail($userId);
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\App;
|
|
||||||
|
|
||||||
class AppVersion
|
|
||||||
{
|
|
||||||
protected static string $version = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the application's version number from its top-level `version` text file.
|
|
||||||
*/
|
|
||||||
public static function get(): string
|
|
||||||
{
|
|
||||||
if (!empty(static::$version)) {
|
|
||||||
return static::$version;
|
|
||||||
}
|
|
||||||
|
|
||||||
$versionFile = base_path('version');
|
|
||||||
$version = trim(file_get_contents($versionFile));
|
|
||||||
static::$version = $version;
|
|
||||||
|
|
||||||
return $version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ class Model extends EloquentModel
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Provides public access to get the raw attribute value from the model.
|
* Provides public access to get the raw attribute value from the model.
|
||||||
* Used in areas where no mutations are required, but performance is critical.
|
* Used in areas where no mutations are required but performance is critical.
|
||||||
*
|
*
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ class AuthServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
Auth::provider('external-users', function () {
|
Auth::provider('external-users', function ($app, array $config) {
|
||||||
return new ExternalBaseUserProvider();
|
return new ExternalBaseUserProvider($config['model']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bind and provide the default system user as a singleton to the app instance when needed.
|
// Bind and provide the default system user as a singleton to the app instance when needed.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class EventServiceProvider extends ServiceProvider
|
|||||||
/**
|
/**
|
||||||
* The event listener mappings for the application.
|
* The event listener mappings for the application.
|
||||||
*
|
*
|
||||||
* @var array<class-string, array<int, string>>
|
* @var array<class-string, array<int, class-string>>
|
||||||
*/
|
*/
|
||||||
protected $listen = [
|
protected $listen = [
|
||||||
SocialiteWasCalled::class => [
|
SocialiteWasCalled::class => [
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace BookStack\App\Providers;
|
namespace BookStack\App\Providers;
|
||||||
|
|
||||||
use BookStack\Entities\BreadcrumbsViewComposer;
|
use BookStack\Entities\BreadcrumbsViewComposer;
|
||||||
use BookStack\Util\DateFormatter;
|
|
||||||
use Illuminate\Pagination\Paginator;
|
use Illuminate\Pagination\Paginator;
|
||||||
use Illuminate\Support\Facades\Blade;
|
use Illuminate\Support\Facades\Blade;
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
@@ -11,15 +10,6 @@ use Illuminate\Support\ServiceProvider;
|
|||||||
|
|
||||||
class ViewTweaksServiceProvider extends ServiceProvider
|
class ViewTweaksServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
public function register()
|
|
||||||
{
|
|
||||||
$this->app->singleton(DateFormatter::class, function ($app) {
|
|
||||||
return new DateFormatter(
|
|
||||||
$app['config']->get('app.display_timezone'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bootstrap services.
|
* Bootstrap services.
|
||||||
*/
|
*/
|
||||||
@@ -31,9 +21,6 @@ class ViewTweaksServiceProvider extends ServiceProvider
|
|||||||
// View Composers
|
// View Composers
|
||||||
View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class);
|
View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class);
|
||||||
|
|
||||||
// View Globals
|
|
||||||
View::share('dates', $this->app->make(DateFormatter::class));
|
|
||||||
|
|
||||||
// Custom blade view directives
|
// Custom blade view directives
|
||||||
Blade::directive('icon', function ($expression) {
|
Blade::directive('icon', function ($expression) {
|
||||||
return "<?php echo (new \BookStack\Util\SvgIcon($expression))->toHtml(); ?>";
|
return "<?php echo (new \BookStack\Util\SvgIcon($expression))->toHtml(); ?>";
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ namespace BookStack\App;
|
|||||||
/**
|
/**
|
||||||
* Assigned to models that can have slugs.
|
* Assigned to models that can have slugs.
|
||||||
* Must have the below properties.
|
* Must have the below properties.
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property string $name
|
||||||
*/
|
*/
|
||||||
interface SluggableInterface
|
interface Sluggable
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Regenerate the slug for this model.
|
* Regenerate the slug for this model.
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\App;
|
|
||||||
|
|
||||||
use BookStack\Http\ApiController;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
|
|
||||||
class SystemApiController extends ApiController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Read details regarding the BookStack instance.
|
|
||||||
* Some details may be null where not set, like the app logo for example.
|
|
||||||
*/
|
|
||||||
public function read(): JsonResponse
|
|
||||||
{
|
|
||||||
$logoSetting = setting('app-logo', '');
|
|
||||||
if ($logoSetting === 'none') {
|
|
||||||
$logo = null;
|
|
||||||
} else {
|
|
||||||
$logo = $logoSetting ? url($logoSetting) : url('/logo.png');
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'version' => AppVersion::get(),
|
|
||||||
'instance_id' => setting('instance-id'),
|
|
||||||
'app_name' => setting('app-name'),
|
|
||||||
'app_logo' => $logo,
|
|
||||||
'base_url' => url('/'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use BookStack\App\AppVersion;
|
|
||||||
use BookStack\App\Model;
|
use BookStack\App\Model;
|
||||||
use BookStack\Facades\Theme;
|
use BookStack\Facades\Theme;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Permissions\PermissionApplicator;
|
use BookStack\Permissions\PermissionApplicator;
|
||||||
use BookStack\Settings\SettingService;
|
use BookStack\Settings\SettingService;
|
||||||
use BookStack\Users\Models\User;
|
use BookStack\Users\Models\User;
|
||||||
@@ -15,7 +13,12 @@ use BookStack\Users\Models\User;
|
|||||||
*/
|
*/
|
||||||
function versioned_asset(string $file = ''): string
|
function versioned_asset(string $file = ''): string
|
||||||
{
|
{
|
||||||
$version = AppVersion::get();
|
static $version = null;
|
||||||
|
|
||||||
|
if (is_null($version)) {
|
||||||
|
$versionFile = base_path('version');
|
||||||
|
$version = trim(file_get_contents($versionFile));
|
||||||
|
}
|
||||||
|
|
||||||
$additional = '';
|
$additional = '';
|
||||||
if (config('app.env') === 'development') {
|
if (config('app.env') === 'development') {
|
||||||
@@ -40,7 +43,7 @@ function user(): User
|
|||||||
* Check if the current user has a permission. If an ownable element
|
* Check if the current user has a permission. If an ownable element
|
||||||
* is passed in the jointPermissions are checked against that particular item.
|
* is passed in the jointPermissions are checked against that particular item.
|
||||||
*/
|
*/
|
||||||
function userCan(string|Permission $permission, ?Model $ownable = null): bool
|
function userCan(string $permission, ?Model $ownable = null): bool
|
||||||
{
|
{
|
||||||
if (is_null($ownable)) {
|
if (is_null($ownable)) {
|
||||||
return user()->can($permission);
|
return user()->can($permission);
|
||||||
@@ -56,7 +59,7 @@ function userCan(string|Permission $permission, ?Model $ownable = null): bool
|
|||||||
* Check if the current user can perform the given action on any items in the system.
|
* Check if the current user can perform the given action on any items in the system.
|
||||||
* Can be provided the class name of an entity to filter ability to that specific entity type.
|
* Can be provided the class name of an entity to filter ability to that specific entity type.
|
||||||
*/
|
*/
|
||||||
function userCanOnAny(string|Permission $action, string $entityClass = ''): bool
|
function userCanOnAny(string $action, string $entityClass = ''): bool
|
||||||
{
|
{
|
||||||
$permissions = app()->make(PermissionApplicator::class);
|
$permissions = app()->make(PermissionApplicator::class);
|
||||||
|
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ return [
|
|||||||
// A list of the sources/hostnames that can be reached by application SSR calls.
|
// A list of the sources/hostnames that can be reached by application SSR calls.
|
||||||
// This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
|
// This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
|
||||||
// Host-specific functionality (usually controlled via other options) like auth
|
// Host-specific functionality (usually controlled via other options) like auth
|
||||||
// or user avatars, for example, won't use this list.
|
// or user avatars for example, won't use this list.
|
||||||
// Space separated if multiple. Can use '*' as a wildcard.
|
// Space seperated if multiple. Can use '*' as a wildcard.
|
||||||
// Values will be compared prefix-matched, case-insensitive, against called SSR urls.
|
// Values will be compared prefix-matched, case-insensitive, against called SSR urls.
|
||||||
// Defaults to allow all hosts.
|
// Defaults to allow all hosts.
|
||||||
'ssr_hosts' => env('ALLOWED_SSR_HOSTS', '*'),
|
'ssr_hosts' => env('ALLOWED_SSR_HOSTS', '*'),
|
||||||
@@ -80,10 +80,8 @@ return [
|
|||||||
// Integer value between 0 (IP hidden) to 4 (Full IP usage)
|
// Integer value between 0 (IP hidden) to 4 (Full IP usage)
|
||||||
'ip_address_precision' => env('IP_ADDRESS_PRECISION', 4),
|
'ip_address_precision' => env('IP_ADDRESS_PRECISION', 4),
|
||||||
|
|
||||||
// Application timezone for stored date/time values.
|
// Application timezone for back-end date functions.
|
||||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||||
// Application timezone for displayed date/time values in the UI.
|
|
||||||
'display_timezone' => env('APP_DISPLAY_TIMEZONE', env('APP_TIMEZONE', 'UTC')),
|
|
||||||
|
|
||||||
// Default locale to use
|
// Default locale to use
|
||||||
// A default variant is also stored since Laravel can overwrite
|
// A default variant is also stored since Laravel can overwrite
|
||||||
|
|||||||
@@ -85,6 +85,6 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'prefix' => env('CACHE_PREFIX', 'bookstack_cache_'),
|
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache_'),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ return [
|
|||||||
'collation' => 'utf8mb4_unicode_ci',
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
// Prefixes are only semi-supported and may be unstable
|
// Prefixes are only semi-supported and may be unstable
|
||||||
// since they are not tested as part of our automated test suite.
|
// since they are not tested as part of our automated test suite.
|
||||||
// If used, the prefix should not be changed; otherwise you will likely receive errors.
|
// If used, the prefix should not be changed otherwise you will likely receive errors.
|
||||||
'prefix' => env('DB_TABLE_PREFIX', ''),
|
'prefix' => env('DB_TABLE_PREFIX', ''),
|
||||||
'prefix_indexes' => true,
|
'prefix_indexes' => true,
|
||||||
'strict' => false,
|
'strict' => false,
|
||||||
@@ -103,7 +103,9 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
// Migration Repository Table
|
// Migration Repository Table
|
||||||
// This table keeps track of all the migrations that have already run for the application.
|
// This table keeps track of all the migrations that have already run for
|
||||||
|
// your application. Using this information, we can determine which of
|
||||||
|
// the migrations on disk haven't actually been run in the database.
|
||||||
'migrations' => 'migrations',
|
'migrations' => 'migrations',
|
||||||
|
|
||||||
// Redis configuration to use if set
|
// Redis configuration to use if set
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ return [
|
|||||||
'local' => [
|
'local' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => public_path(),
|
'root' => public_path(),
|
||||||
|
'visibility' => 'public',
|
||||||
'serve' => false,
|
'serve' => false,
|
||||||
'throw' => true,
|
'throw' => true,
|
||||||
'directory_visibility' => 'public',
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'local_secure_attachments' => [
|
'local_secure_attachments' => [
|
||||||
@@ -47,6 +47,7 @@ return [
|
|||||||
'local_secure_images' => [
|
'local_secure_images' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => storage_path('uploads/images/'),
|
'root' => storage_path('uploads/images/'),
|
||||||
|
'visibility' => 'public',
|
||||||
'serve' => false,
|
'serve' => false,
|
||||||
'throw' => true,
|
'throw' => true,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
// Configured mail encryption method.
|
// Configured mail encryption method.
|
||||||
// STARTTLS should still be attempted, but tls/ssl forces TLS usage.
|
// STARTTLS should still be attempted, but tls/ssl forces TLS usage.
|
||||||
$mailEncryption = env('MAIL_ENCRYPTION', null);
|
$mailEncryption = env('MAIL_ENCRYPTION', null);
|
||||||
$mailPort = intval(env('MAIL_PORT', 587));
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@@ -34,13 +33,13 @@ return [
|
|||||||
'transport' => 'smtp',
|
'transport' => 'smtp',
|
||||||
'scheme' => null,
|
'scheme' => null,
|
||||||
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
|
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
|
||||||
'port' => $mailPort,
|
'port' => env('MAIL_PORT', 587),
|
||||||
'username' => env('MAIL_USERNAME'),
|
'username' => env('MAIL_USERNAME'),
|
||||||
'password' => env('MAIL_PASSWORD'),
|
'password' => env('MAIL_PASSWORD'),
|
||||||
'verify_peer' => env('MAIL_VERIFY_SSL', true),
|
'verify_peer' => env('MAIL_VERIFY_SSL', true),
|
||||||
'timeout' => null,
|
'timeout' => null,
|
||||||
'local_domain' => null,
|
'local_domain' => null,
|
||||||
'require_tls' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl' || $mailPort === 465),
|
'tls_required' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'sendmail' => [
|
'sendmail' => [
|
||||||
|
|||||||
@@ -47,12 +47,6 @@ return [
|
|||||||
// Multiple values can be provided comma seperated.
|
// Multiple values can be provided comma seperated.
|
||||||
'additional_scopes' => env('OIDC_ADDITIONAL_SCOPES', null),
|
'additional_scopes' => env('OIDC_ADDITIONAL_SCOPES', null),
|
||||||
|
|
||||||
// Enable fetching of the user's avatar from the 'picture' claim on login.
|
|
||||||
// Will only be fetched if the user doesn't already have an avatar image assigned.
|
|
||||||
// This can be a security risk due to performing server-side fetching (with up to 3 redirects) of
|
|
||||||
// data from external URLs. Only enable if you trust the OIDC auth provider to provide safe URLs for user images.
|
|
||||||
'fetch_avatar' => env('OIDC_FETCH_AVATAR', false),
|
|
||||||
|
|
||||||
// Group sync options
|
// Group sync options
|
||||||
// Enable syncing, upon login, of OIDC groups to BookStack roles
|
// Enable syncing, upon login, of OIDC groups to BookStack roles
|
||||||
'user_to_groups' => env('OIDC_USER_TO_GROUPS', false),
|
'user_to_groups' => env('OIDC_USER_TO_GROUPS', false),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Illuminate\Console\Command;
|
|||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\Rules\Password;
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
use Illuminate\Validation\Rules\Unique;
|
||||||
|
|
||||||
class CreateAdminCommand extends Command
|
class CreateAdminCommand extends Command
|
||||||
{
|
{
|
||||||
@@ -20,9 +21,7 @@ class CreateAdminCommand extends Command
|
|||||||
{--email= : The email address for the new admin user}
|
{--email= : The email address for the new admin user}
|
||||||
{--name= : The name of the new admin user}
|
{--name= : The name of the new admin user}
|
||||||
{--password= : The password to assign to the new admin user}
|
{--password= : The password to assign to the new admin user}
|
||||||
{--external-auth-id= : The external authentication system id for the new admin user (SAML2/LDAP/OIDC)}
|
{--external-auth-id= : The external authentication system id for the new admin user (SAML2/LDAP/OIDC)}';
|
||||||
{--generate-password : Generate a random password for the new admin user}
|
|
||||||
{--initial : Indicate if this should set/update the details of the initial admin user}';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The console command description.
|
* The console command description.
|
||||||
@@ -36,12 +35,26 @@ class CreateAdminCommand extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle(UserRepo $userRepo): int
|
public function handle(UserRepo $userRepo): int
|
||||||
{
|
{
|
||||||
$initialAdminOnly = $this->option('initial');
|
$details = $this->snakeCaseOptions();
|
||||||
$shouldGeneratePassword = $this->option('generate-password');
|
|
||||||
$details = $this->gatherDetails($shouldGeneratePassword, $initialAdminOnly);
|
if (empty($details['email'])) {
|
||||||
|
$details['email'] = $this->ask('Please specify an email address for the new admin user');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($details['name'])) {
|
||||||
|
$details['name'] = $this->ask('Please specify a name for the new admin user');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($details['password'])) {
|
||||||
|
if (empty($details['external_auth_id'])) {
|
||||||
|
$details['password'] = $this->ask('Please specify a password for the new admin user (8 characters min)');
|
||||||
|
} else {
|
||||||
|
$details['password'] = Str::random(32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$validator = Validator::make($details, [
|
$validator = Validator::make($details, [
|
||||||
'email' => ['required', 'email', 'min:5'],
|
'email' => ['required', 'email', 'min:5', new Unique('users', 'email')],
|
||||||
'name' => ['required', 'min:2'],
|
'name' => ['required', 'min:2'],
|
||||||
'password' => ['required_without:external_auth_id', Password::default()],
|
'password' => ['required_without:external_auth_id', Password::default()],
|
||||||
'external_auth_id' => ['required_without:password'],
|
'external_auth_id' => ['required_without:password'],
|
||||||
@@ -55,101 +68,16 @@ class CreateAdminCommand extends Command
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
$adminRole = Role::getSystemRole('admin');
|
|
||||||
|
|
||||||
if ($initialAdminOnly) {
|
|
||||||
$handled = $this->handleInitialAdminIfExists($userRepo, $details, $shouldGeneratePassword, $adminRole);
|
|
||||||
if ($handled !== null) {
|
|
||||||
return $handled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$emailUsed = $userRepo->getByEmail($details['email']) !== null;
|
|
||||||
if ($emailUsed) {
|
|
||||||
$this->error("Could not create admin account.");
|
|
||||||
$this->error("An account with the email address \"{$details['email']}\" already exists.");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $userRepo->createWithoutActivity($validator->validated());
|
$user = $userRepo->createWithoutActivity($validator->validated());
|
||||||
$user->attachRole($adminRole);
|
$user->attachRole(Role::getSystemRole('admin'));
|
||||||
$user->email_confirmed = true;
|
$user->email_confirmed = true;
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
if ($shouldGeneratePassword) {
|
$this->info("Admin account with email \"{$user->email}\" successfully created!");
|
||||||
$this->line($details['password']);
|
|
||||||
} else {
|
|
||||||
$this->info("Admin account with email \"{$user->email}\" successfully created!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle updates to the original admin account if it exists.
|
|
||||||
* Returns an int return status if handled, otherwise returns null if not handled (new user to be created).
|
|
||||||
*/
|
|
||||||
protected function handleInitialAdminIfExists(UserRepo $userRepo, array $data, bool $generatePassword, Role $adminRole): int|null
|
|
||||||
{
|
|
||||||
$defaultAdmin = $userRepo->getByEmail('admin@admin.com');
|
|
||||||
if ($defaultAdmin && $defaultAdmin->hasSystemRole('admin')) {
|
|
||||||
if ($defaultAdmin->email !== $data['email'] && $userRepo->getByEmail($data['email']) !== null) {
|
|
||||||
$this->error("Could not create admin account.");
|
|
||||||
$this->error("An account with the email address \"{$data['email']}\" already exists.");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$userRepo->updateWithoutActivity($defaultAdmin, $data, true);
|
|
||||||
if ($generatePassword) {
|
|
||||||
$this->line($data['password']);
|
|
||||||
} else {
|
|
||||||
$this->info("The default admin user has been updated with the provided details!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
} else if ($adminRole->users()->count() > 0) {
|
|
||||||
$this->warn('Non-default admin user already exists. Skipping creation of new admin user.');
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function gatherDetails(bool $generatePassword, bool $initialAdmin): array
|
|
||||||
{
|
|
||||||
$details = $this->snakeCaseOptions();
|
|
||||||
|
|
||||||
if (empty($details['email'])) {
|
|
||||||
if ($initialAdmin) {
|
|
||||||
$details['email'] = 'admin@example.com';
|
|
||||||
} else {
|
|
||||||
$details['email'] = $this->ask('Please specify an email address for the new admin user');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($details['name'])) {
|
|
||||||
if ($initialAdmin) {
|
|
||||||
$details['name'] = 'Admin';
|
|
||||||
} else {
|
|
||||||
$details['name'] = $this->ask('Please specify a name for the new admin user');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($details['password'])) {
|
|
||||||
if (empty($details['external_auth_id'])) {
|
|
||||||
if ($generatePassword) {
|
|
||||||
$details['password'] = Str::random(32);
|
|
||||||
} else {
|
|
||||||
$details['password'] = $this->ask('Please specify a password for the new admin user (8 characters min)');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$details['password'] = Str::random(32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $details;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function snakeCaseOptions(): array
|
protected function snakeCaseOptions(): array
|
||||||
{
|
{
|
||||||
$returnOpts = [];
|
$returnOpts = [];
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class UpdateUrlCommand extends Command
|
|||||||
'page_revisions' => ['html', 'text', 'markdown'],
|
'page_revisions' => ['html', 'text', 'markdown'],
|
||||||
'images' => ['url'],
|
'images' => ['url'],
|
||||||
'settings' => ['value'],
|
'settings' => ['value'],
|
||||||
'comments' => ['html'],
|
'comments' => ['html', 'text'],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($columnsToUpdateByTable as $table => $columns) {
|
foreach ($columnsToUpdateByTable as $table => $columns) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use BookStack\Entities\Queries\PageQueries;
|
|||||||
use BookStack\Entities\Repos\BookRepo;
|
use BookStack\Entities\Repos\BookRepo;
|
||||||
use BookStack\Entities\Tools\BookContents;
|
use BookStack\Entities\Tools\BookContents;
|
||||||
use BookStack\Http\ApiController;
|
use BookStack\Http\ApiController;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ class BookApiController extends ApiController
|
|||||||
*/
|
*/
|
||||||
public function create(Request $request)
|
public function create(Request $request)
|
||||||
{
|
{
|
||||||
$this->checkPermission(Permission::BookCreateAll);
|
$this->checkPermission('book-create-all');
|
||||||
$requestData = $this->validate($request, $this->rules()['create']);
|
$requestData = $this->validate($request, $this->rules()['create']);
|
||||||
|
|
||||||
$book = $this->bookRepo->create($requestData);
|
$book = $this->bookRepo->create($requestData);
|
||||||
@@ -93,7 +92,7 @@ class BookApiController extends ApiController
|
|||||||
public function update(Request $request, string $id)
|
public function update(Request $request, string $id)
|
||||||
{
|
{
|
||||||
$book = $this->queries->findVisibleByIdOrFail(intval($id));
|
$book = $this->queries->findVisibleByIdOrFail(intval($id));
|
||||||
$this->checkOwnablePermission(Permission::BookUpdate, $book);
|
$this->checkOwnablePermission('book-update', $book);
|
||||||
|
|
||||||
$requestData = $this->validate($request, $this->rules()['update']);
|
$requestData = $this->validate($request, $this->rules()['update']);
|
||||||
$book = $this->bookRepo->update($book, $requestData);
|
$book = $this->bookRepo->update($book, $requestData);
|
||||||
@@ -110,7 +109,7 @@ class BookApiController extends ApiController
|
|||||||
public function delete(string $id)
|
public function delete(string $id)
|
||||||
{
|
{
|
||||||
$book = $this->queries->findVisibleByIdOrFail(intval($id));
|
$book = $this->queries->findVisibleByIdOrFail(intval($id));
|
||||||
$this->checkOwnablePermission(Permission::BookDelete, $book);
|
$this->checkOwnablePermission('book-delete', $book);
|
||||||
|
|
||||||
$this->bookRepo->destroy($book);
|
$this->bookRepo->destroy($book);
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ use BookStack\Exceptions\ImageUploadException;
|
|||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\References\ReferenceFetcher;
|
use BookStack\References\ReferenceFetcher;
|
||||||
use BookStack\Util\DatabaseTransaction;
|
|
||||||
use BookStack\Util\SimpleListOptions;
|
use BookStack\Util\SimpleListOptions;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
@@ -74,12 +72,12 @@ class BookController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function create(?string $shelfSlug = null)
|
public function create(?string $shelfSlug = null)
|
||||||
{
|
{
|
||||||
$this->checkPermission(Permission::BookCreateAll);
|
$this->checkPermission('book-create-all');
|
||||||
|
|
||||||
$bookshelf = null;
|
$bookshelf = null;
|
||||||
if ($shelfSlug !== null) {
|
if ($shelfSlug !== null) {
|
||||||
$bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
|
$bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
|
||||||
$this->checkOwnablePermission(Permission::BookshelfUpdate, $bookshelf);
|
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->setPageTitle(trans('entities.books_create'));
|
$this->setPageTitle(trans('entities.books_create'));
|
||||||
@@ -97,7 +95,7 @@ class BookController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(Request $request, ?string $shelfSlug = null)
|
public function store(Request $request, ?string $shelfSlug = null)
|
||||||
{
|
{
|
||||||
$this->checkPermission(Permission::BookCreateAll);
|
$this->checkPermission('book-create-all');
|
||||||
$validated = $this->validate($request, [
|
$validated = $this->validate($request, [
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'description_html' => ['string', 'max:2000'],
|
'description_html' => ['string', 'max:2000'],
|
||||||
@@ -109,7 +107,7 @@ class BookController extends Controller
|
|||||||
$bookshelf = null;
|
$bookshelf = null;
|
||||||
if ($shelfSlug !== null) {
|
if ($shelfSlug !== null) {
|
||||||
$bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
|
$bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
|
||||||
$this->checkOwnablePermission(Permission::BookshelfUpdate, $bookshelf);
|
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
|
||||||
}
|
}
|
||||||
|
|
||||||
$book = $this->bookRepo->create($validated);
|
$book = $this->bookRepo->create($validated);
|
||||||
@@ -155,7 +153,7 @@ class BookController extends Controller
|
|||||||
public function edit(string $slug)
|
public function edit(string $slug)
|
||||||
{
|
{
|
||||||
$book = $this->queries->findVisibleBySlugOrFail($slug);
|
$book = $this->queries->findVisibleBySlugOrFail($slug);
|
||||||
$this->checkOwnablePermission(Permission::BookUpdate, $book);
|
$this->checkOwnablePermission('book-update', $book);
|
||||||
$this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()]));
|
$this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()]));
|
||||||
|
|
||||||
return view('books.edit', ['book' => $book, 'current' => $book]);
|
return view('books.edit', ['book' => $book, 'current' => $book]);
|
||||||
@@ -171,7 +169,7 @@ class BookController extends Controller
|
|||||||
public function update(Request $request, string $slug)
|
public function update(Request $request, string $slug)
|
||||||
{
|
{
|
||||||
$book = $this->queries->findVisibleBySlugOrFail($slug);
|
$book = $this->queries->findVisibleBySlugOrFail($slug);
|
||||||
$this->checkOwnablePermission(Permission::BookUpdate, $book);
|
$this->checkOwnablePermission('book-update', $book);
|
||||||
|
|
||||||
$validated = $this->validate($request, [
|
$validated = $this->validate($request, [
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
@@ -198,7 +196,7 @@ class BookController extends Controller
|
|||||||
public function showDelete(string $bookSlug)
|
public function showDelete(string $bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
|
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
|
||||||
$this->checkOwnablePermission(Permission::BookDelete, $book);
|
$this->checkOwnablePermission('book-delete', $book);
|
||||||
$this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
|
$this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
|
||||||
|
|
||||||
return view('books.delete', ['book' => $book, 'current' => $book]);
|
return view('books.delete', ['book' => $book, 'current' => $book]);
|
||||||
@@ -212,7 +210,7 @@ class BookController extends Controller
|
|||||||
public function destroy(string $bookSlug)
|
public function destroy(string $bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
|
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
|
||||||
$this->checkOwnablePermission(Permission::BookDelete, $book);
|
$this->checkOwnablePermission('book-delete', $book);
|
||||||
|
|
||||||
$this->bookRepo->destroy($book);
|
$this->bookRepo->destroy($book);
|
||||||
|
|
||||||
@@ -227,7 +225,7 @@ class BookController extends Controller
|
|||||||
public function showCopy(string $bookSlug)
|
public function showCopy(string $bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
|
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
|
||||||
$this->checkOwnablePermission(Permission::BookView, $book);
|
$this->checkOwnablePermission('book-view', $book);
|
||||||
|
|
||||||
session()->flashInput(['name' => $book->name]);
|
session()->flashInput(['name' => $book->name]);
|
||||||
|
|
||||||
@@ -244,8 +242,8 @@ class BookController extends Controller
|
|||||||
public function copy(Request $request, Cloner $cloner, string $bookSlug)
|
public function copy(Request $request, Cloner $cloner, string $bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
|
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
|
||||||
$this->checkOwnablePermission(Permission::BookView, $book);
|
$this->checkOwnablePermission('book-view', $book);
|
||||||
$this->checkPermission(Permission::BookCreateAll);
|
$this->checkPermission('book-create-all');
|
||||||
|
|
||||||
$newName = $request->get('name') ?: $book->name;
|
$newName = $request->get('name') ?: $book->name;
|
||||||
$bookCopy = $cloner->cloneBook($book, $newName);
|
$bookCopy = $cloner->cloneBook($book, $newName);
|
||||||
@@ -260,14 +258,12 @@ class BookController extends Controller
|
|||||||
public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug)
|
public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
|
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
|
||||||
$this->checkOwnablePermission(Permission::BookUpdate, $book);
|
$this->checkOwnablePermission('book-update', $book);
|
||||||
$this->checkOwnablePermission(Permission::BookDelete, $book);
|
$this->checkOwnablePermission('book-delete', $book);
|
||||||
$this->checkPermission(Permission::BookshelfCreateAll);
|
$this->checkPermission('bookshelf-create-all');
|
||||||
$this->checkPermission(Permission::BookCreateAll);
|
$this->checkPermission('book-create-all');
|
||||||
|
|
||||||
$shelf = (new DatabaseTransaction(function () use ($book, $transformer) {
|
$shelf = $transformer->transformBookToShelf($book);
|
||||||
return $transformer->transformBookToShelf($book);
|
|
||||||
}))->run();
|
|
||||||
|
|
||||||
return redirect($shelf->getUrl());
|
return redirect($shelf->getUrl());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use BookStack\Entities\Models\Bookshelf;
|
|||||||
use BookStack\Entities\Queries\BookshelfQueries;
|
use BookStack\Entities\Queries\BookshelfQueries;
|
||||||
use BookStack\Entities\Repos\BookshelfRepo;
|
use BookStack\Entities\Repos\BookshelfRepo;
|
||||||
use BookStack\Http\ApiController;
|
use BookStack\Http\ApiController;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -46,7 +45,7 @@ class BookshelfApiController extends ApiController
|
|||||||
*/
|
*/
|
||||||
public function create(Request $request)
|
public function create(Request $request)
|
||||||
{
|
{
|
||||||
$this->checkPermission(Permission::BookshelfCreateAll);
|
$this->checkPermission('bookshelf-create-all');
|
||||||
$requestData = $this->validate($request, $this->rules()['create']);
|
$requestData = $this->validate($request, $this->rules()['create']);
|
||||||
|
|
||||||
$bookIds = $request->get('books', []);
|
$bookIds = $request->get('books', []);
|
||||||
@@ -85,7 +84,7 @@ class BookshelfApiController extends ApiController
|
|||||||
public function update(Request $request, string $id)
|
public function update(Request $request, string $id)
|
||||||
{
|
{
|
||||||
$shelf = $this->queries->findVisibleByIdOrFail(intval($id));
|
$shelf = $this->queries->findVisibleByIdOrFail(intval($id));
|
||||||
$this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);
|
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||||
|
|
||||||
$requestData = $this->validate($request, $this->rules()['update']);
|
$requestData = $this->validate($request, $this->rules()['update']);
|
||||||
$bookIds = $request->get('books', null);
|
$bookIds = $request->get('books', null);
|
||||||
@@ -104,7 +103,7 @@ class BookshelfApiController extends ApiController
|
|||||||
public function delete(string $id)
|
public function delete(string $id)
|
||||||
{
|
{
|
||||||
$shelf = $this->queries->findVisibleByIdOrFail(intval($id));
|
$shelf = $this->queries->findVisibleByIdOrFail(intval($id));
|
||||||
$this->checkOwnablePermission(Permission::BookshelfDelete, $shelf);
|
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
||||||
|
|
||||||
$this->bookshelfRepo->destroy($shelf);
|
$this->bookshelfRepo->destroy($shelf);
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use BookStack\Entities\Tools\ShelfContext;
|
|||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\References\ReferenceFetcher;
|
use BookStack\References\ReferenceFetcher;
|
||||||
use BookStack\Util\SimpleListOptions;
|
use BookStack\Util\SimpleListOptions;
|
||||||
use Exception;
|
use Exception;
|
||||||
@@ -69,7 +68,7 @@ class BookshelfController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
$this->checkPermission(Permission::BookshelfCreateAll);
|
$this->checkPermission('bookshelf-create-all');
|
||||||
$books = $this->bookQueries->visibleForList()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
|
$books = $this->bookQueries->visibleForList()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
|
||||||
$this->setPageTitle(trans('entities.shelves_create'));
|
$this->setPageTitle(trans('entities.shelves_create'));
|
||||||
|
|
||||||
@@ -84,7 +83,7 @@ class BookshelfController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
$this->checkPermission(Permission::BookshelfCreateAll);
|
$this->checkPermission('bookshelf-create-all');
|
||||||
$validated = $this->validate($request, [
|
$validated = $this->validate($request, [
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'description_html' => ['string', 'max:2000'],
|
'description_html' => ['string', 'max:2000'],
|
||||||
@@ -106,7 +105,7 @@ class BookshelfController extends Controller
|
|||||||
public function show(Request $request, ActivityQueries $activities, string $slug)
|
public function show(Request $request, ActivityQueries $activities, string $slug)
|
||||||
{
|
{
|
||||||
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
|
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
|
||||||
$this->checkOwnablePermission(Permission::BookshelfView, $shelf);
|
$this->checkOwnablePermission('bookshelf-view', $shelf);
|
||||||
|
|
||||||
$listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([
|
$listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([
|
||||||
'default' => trans('common.sort_default'),
|
'default' => trans('common.sort_default'),
|
||||||
@@ -144,7 +143,7 @@ class BookshelfController extends Controller
|
|||||||
public function edit(string $slug)
|
public function edit(string $slug)
|
||||||
{
|
{
|
||||||
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
|
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
|
||||||
$this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);
|
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||||
|
|
||||||
$shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
|
$shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
|
||||||
$books = $this->bookQueries->visibleForList()
|
$books = $this->bookQueries->visibleForList()
|
||||||
@@ -170,7 +169,7 @@ class BookshelfController extends Controller
|
|||||||
public function update(Request $request, string $slug)
|
public function update(Request $request, string $slug)
|
||||||
{
|
{
|
||||||
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
|
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
|
||||||
$this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);
|
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||||
$validated = $this->validate($request, [
|
$validated = $this->validate($request, [
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'description_html' => ['string', 'max:2000'],
|
'description_html' => ['string', 'max:2000'],
|
||||||
@@ -196,7 +195,7 @@ class BookshelfController extends Controller
|
|||||||
public function showDelete(string $slug)
|
public function showDelete(string $slug)
|
||||||
{
|
{
|
||||||
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
|
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
|
||||||
$this->checkOwnablePermission(Permission::BookshelfDelete, $shelf);
|
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
||||||
|
|
||||||
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
|
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
|
||||||
|
|
||||||
@@ -211,7 +210,7 @@ class BookshelfController extends Controller
|
|||||||
public function destroy(string $slug)
|
public function destroy(string $slug)
|
||||||
{
|
{
|
||||||
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
|
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
|
||||||
$this->checkOwnablePermission(Permission::BookshelfDelete, $shelf);
|
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
||||||
|
|
||||||
$this->shelfRepo->destroy($shelf);
|
$this->shelfRepo->destroy($shelf);
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,19 @@
|
|||||||
|
|
||||||
namespace BookStack\Entities\Controllers;
|
namespace BookStack\Entities\Controllers;
|
||||||
|
|
||||||
use BookStack\Entities\Models\Book;
|
|
||||||
use BookStack\Entities\Models\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Queries\ChapterQueries;
|
use BookStack\Entities\Queries\ChapterQueries;
|
||||||
use BookStack\Entities\Queries\EntityQueries;
|
use BookStack\Entities\Queries\EntityQueries;
|
||||||
use BookStack\Entities\Repos\ChapterRepo;
|
use BookStack\Entities\Repos\ChapterRepo;
|
||||||
use BookStack\Exceptions\PermissionsException;
|
use BookStack\Exceptions\PermissionsException;
|
||||||
use BookStack\Http\ApiController;
|
use BookStack\Http\ApiController;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class ChapterApiController extends ApiController
|
class ChapterApiController extends ApiController
|
||||||
{
|
{
|
||||||
protected array $rules = [
|
protected $rules = [
|
||||||
'create' => [
|
'create' => [
|
||||||
'book_id' => ['required', 'integer'],
|
'book_id' => ['required', 'integer'],
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
@@ -66,7 +65,7 @@ class ChapterApiController extends ApiController
|
|||||||
|
|
||||||
$bookId = $request->get('book_id');
|
$bookId = $request->get('book_id');
|
||||||
$book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId));
|
$book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId));
|
||||||
$this->checkOwnablePermission(Permission::ChapterCreate, $book);
|
$this->checkOwnablePermission('chapter-create', $book);
|
||||||
|
|
||||||
$chapter = $this->chapterRepo->create($requestData, $book);
|
$chapter = $this->chapterRepo->create($requestData, $book);
|
||||||
|
|
||||||
@@ -102,10 +101,10 @@ class ChapterApiController extends ApiController
|
|||||||
{
|
{
|
||||||
$requestData = $this->validate($request, $this->rules()['update']);
|
$requestData = $this->validate($request, $this->rules()['update']);
|
||||||
$chapter = $this->queries->findVisibleByIdOrFail(intval($id));
|
$chapter = $this->queries->findVisibleByIdOrFail(intval($id));
|
||||||
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
|
|
||||||
if ($request->has('book_id') && $chapter->book_id !== intval($requestData['book_id'])) {
|
if ($request->has('book_id') && $chapter->book_id !== intval($requestData['book_id'])) {
|
||||||
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
|
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->chapterRepo->move($chapter, "book:{$requestData['book_id']}");
|
$this->chapterRepo->move($chapter, "book:{$requestData['book_id']}");
|
||||||
@@ -130,7 +129,7 @@ class ChapterApiController extends ApiController
|
|||||||
public function delete(string $id)
|
public function delete(string $id)
|
||||||
{
|
{
|
||||||
$chapter = $this->queries->findVisibleByIdOrFail(intval($id));
|
$chapter = $this->queries->findVisibleByIdOrFail(intval($id));
|
||||||
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
|
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||||
|
|
||||||
$this->chapterRepo->destroy($chapter);
|
$this->chapterRepo->destroy($chapter);
|
||||||
|
|
||||||
@@ -145,10 +144,7 @@ class ChapterApiController extends ApiController
|
|||||||
$chapter->load(['tags']);
|
$chapter->load(['tags']);
|
||||||
$chapter->makeVisible('description_html');
|
$chapter->makeVisible('description_html');
|
||||||
$chapter->setAttribute('description_html', $chapter->descriptionHtml());
|
$chapter->setAttribute('description_html', $chapter->descriptionHtml());
|
||||||
|
$chapter->setAttribute('book_slug', $chapter->book()->first()->slug);
|
||||||
/** @var Book $book */
|
|
||||||
$book = $chapter->book()->first();
|
|
||||||
$chapter->setAttribute('book_slug', $book->slug);
|
|
||||||
|
|
||||||
return $chapter;
|
return $chapter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ use BookStack\Exceptions\NotFoundException;
|
|||||||
use BookStack\Exceptions\NotifyException;
|
use BookStack\Exceptions\NotifyException;
|
||||||
use BookStack\Exceptions\PermissionsException;
|
use BookStack\Exceptions\PermissionsException;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\References\ReferenceFetcher;
|
use BookStack\References\ReferenceFetcher;
|
||||||
use BookStack\Util\DatabaseTransaction;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@@ -40,7 +38,7 @@ class ChapterController extends Controller
|
|||||||
public function create(string $bookSlug)
|
public function create(string $bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
|
$book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
|
||||||
$this->checkOwnablePermission(Permission::ChapterCreate, $book);
|
$this->checkOwnablePermission('chapter-create', $book);
|
||||||
|
|
||||||
$this->setPageTitle(trans('entities.chapters_create'));
|
$this->setPageTitle(trans('entities.chapters_create'));
|
||||||
|
|
||||||
@@ -65,7 +63,7 @@ class ChapterController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
|
$book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
|
||||||
$this->checkOwnablePermission(Permission::ChapterCreate, $book);
|
$this->checkOwnablePermission('chapter-create', $book);
|
||||||
|
|
||||||
$chapter = $this->chapterRepo->create($validated, $book);
|
$chapter = $this->chapterRepo->create($validated, $book);
|
||||||
|
|
||||||
@@ -78,6 +76,7 @@ class ChapterController extends Controller
|
|||||||
public function show(string $bookSlug, string $chapterSlug)
|
public function show(string $bookSlug, string $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||||
|
$this->checkOwnablePermission('chapter-view', $chapter);
|
||||||
|
|
||||||
$sidebarTree = (new BookContents($chapter->book))->getTree();
|
$sidebarTree = (new BookContents($chapter->book))->getTree();
|
||||||
$pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get();
|
$pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get();
|
||||||
@@ -106,7 +105,7 @@ class ChapterController extends Controller
|
|||||||
public function edit(string $bookSlug, string $chapterSlug)
|
public function edit(string $bookSlug, string $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||||
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
|
|
||||||
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
|
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
|
||||||
|
|
||||||
@@ -128,7 +127,7 @@ class ChapterController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||||
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
|
|
||||||
$this->chapterRepo->update($chapter, $validated);
|
$this->chapterRepo->update($chapter, $validated);
|
||||||
|
|
||||||
@@ -143,7 +142,7 @@ class ChapterController extends Controller
|
|||||||
public function showDelete(string $bookSlug, string $chapterSlug)
|
public function showDelete(string $bookSlug, string $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||||
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
|
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||||
|
|
||||||
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
|
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
|
||||||
|
|
||||||
@@ -159,7 +158,7 @@ class ChapterController extends Controller
|
|||||||
public function destroy(string $bookSlug, string $chapterSlug)
|
public function destroy(string $bookSlug, string $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||||
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
|
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||||
|
|
||||||
$this->chapterRepo->destroy($chapter);
|
$this->chapterRepo->destroy($chapter);
|
||||||
|
|
||||||
@@ -175,8 +174,8 @@ class ChapterController extends Controller
|
|||||||
{
|
{
|
||||||
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||||
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
|
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
|
||||||
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
|
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||||
|
|
||||||
return view('chapters.move', [
|
return view('chapters.move', [
|
||||||
'chapter' => $chapter,
|
'chapter' => $chapter,
|
||||||
@@ -192,8 +191,8 @@ class ChapterController extends Controller
|
|||||||
public function move(Request $request, string $bookSlug, string $chapterSlug)
|
public function move(Request $request, string $bookSlug, string $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||||
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
|
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||||
|
|
||||||
$entitySelection = $request->get('entity_selection', null);
|
$entitySelection = $request->get('entity_selection', null);
|
||||||
if ($entitySelection === null || $entitySelection === '') {
|
if ($entitySelection === null || $entitySelection === '') {
|
||||||
@@ -221,6 +220,7 @@ class ChapterController extends Controller
|
|||||||
public function showCopy(string $bookSlug, string $chapterSlug)
|
public function showCopy(string $bookSlug, string $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||||
|
$this->checkOwnablePermission('chapter-view', $chapter);
|
||||||
|
|
||||||
session()->flashInput(['name' => $chapter->name]);
|
session()->flashInput(['name' => $chapter->name]);
|
||||||
|
|
||||||
@@ -239,6 +239,7 @@ class ChapterController extends Controller
|
|||||||
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug)
|
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||||
|
$this->checkOwnablePermission('chapter-view', $chapter);
|
||||||
|
|
||||||
$entitySelection = $request->get('entity_selection') ?: null;
|
$entitySelection = $request->get('entity_selection') ?: null;
|
||||||
$newParentBook = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $chapter->getParent();
|
$newParentBook = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $chapter->getParent();
|
||||||
@@ -249,7 +250,7 @@ class ChapterController extends Controller
|
|||||||
return redirect($chapter->getUrl('/copy'));
|
return redirect($chapter->getUrl('/copy'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->checkOwnablePermission(Permission::ChapterCreate, $newParentBook);
|
$this->checkOwnablePermission('chapter-create', $newParentBook);
|
||||||
|
|
||||||
$newName = $request->get('name') ?: $chapter->name;
|
$newName = $request->get('name') ?: $chapter->name;
|
||||||
$chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName);
|
$chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName);
|
||||||
@@ -264,13 +265,11 @@ class ChapterController extends Controller
|
|||||||
public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug)
|
public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||||
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
|
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||||
$this->checkPermission(Permission::BookCreateAll);
|
$this->checkPermission('book-create-all');
|
||||||
|
|
||||||
$book = (new DatabaseTransaction(function () use ($chapter, $transformer) {
|
$book = $transformer->transformChapterToBook($chapter);
|
||||||
return $transformer->transformChapterToBook($chapter);
|
|
||||||
}))->run();
|
|
||||||
|
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ use BookStack\Entities\Queries\PageQueries;
|
|||||||
use BookStack\Entities\Repos\PageRepo;
|
use BookStack\Entities\Repos\PageRepo;
|
||||||
use BookStack\Exceptions\PermissionsException;
|
use BookStack\Exceptions\PermissionsException;
|
||||||
use BookStack\Http\ApiController;
|
use BookStack\Http\ApiController;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class PageApiController extends ApiController
|
class PageApiController extends ApiController
|
||||||
{
|
{
|
||||||
protected array $rules = [
|
protected $rules = [
|
||||||
'create' => [
|
'create' => [
|
||||||
'book_id' => ['required_without:chapter_id', 'integer'],
|
'book_id' => ['required_without:chapter_id', 'integer'],
|
||||||
'chapter_id' => ['required_without:book_id', 'integer'],
|
'chapter_id' => ['required_without:book_id', 'integer'],
|
||||||
@@ -77,7 +76,7 @@ class PageApiController extends ApiController
|
|||||||
} else {
|
} else {
|
||||||
$parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id')));
|
$parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id')));
|
||||||
}
|
}
|
||||||
$this->checkOwnablePermission(Permission::PageCreate, $parent);
|
$this->checkOwnablePermission('page-create', $parent);
|
||||||
|
|
||||||
$draft = $this->pageRepo->getNewDraftPage($parent);
|
$draft = $this->pageRepo->getNewDraftPage($parent);
|
||||||
$this->pageRepo->publishDraft($draft, $request->only(array_keys($this->rules['create'])));
|
$this->pageRepo->publishDraft($draft, $request->only(array_keys($this->rules['create'])));
|
||||||
@@ -117,7 +116,7 @@ class PageApiController extends ApiController
|
|||||||
$requestData = $this->validate($request, $this->rules['update']);
|
$requestData = $this->validate($request, $this->rules['update']);
|
||||||
|
|
||||||
$page = $this->queries->findVisibleByIdOrFail($id);
|
$page = $this->queries->findVisibleByIdOrFail($id);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
$parent = null;
|
$parent = null;
|
||||||
if ($request->has('chapter_id')) {
|
if ($request->has('chapter_id')) {
|
||||||
@@ -127,7 +126,7 @@ class PageApiController extends ApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($parent && !$parent->matches($page->getParent())) {
|
if ($parent && !$parent->matches($page->getParent())) {
|
||||||
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->pageRepo->move($page, $parent->getType() . ':' . $parent->id);
|
$this->pageRepo->move($page, $parent->getType() . ':' . $parent->id);
|
||||||
@@ -152,7 +151,7 @@ class PageApiController extends ApiController
|
|||||||
public function delete(string $id)
|
public function delete(string $id)
|
||||||
{
|
{
|
||||||
$page = $this->queries->findVisibleByIdOrFail($id);
|
$page = $this->queries->findVisibleByIdOrFail($id);
|
||||||
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
|
|
||||||
$this->pageRepo->destroy($page);
|
$this->pageRepo->destroy($page);
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,8 @@ use BookStack\Entities\Tools\PageContent;
|
|||||||
use BookStack\Entities\Tools\PageEditActivity;
|
use BookStack\Entities\Tools\PageEditActivity;
|
||||||
use BookStack\Entities\Tools\PageEditorData;
|
use BookStack\Entities\Tools\PageEditorData;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Exceptions\NotifyException;
|
|
||||||
use BookStack\Exceptions\PermissionsException;
|
use BookStack\Exceptions\PermissionsException;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\References\ReferenceFetcher;
|
use BookStack\References\ReferenceFetcher;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
@@ -51,7 +49,7 @@ class PageController extends Controller
|
|||||||
$parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
|
$parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->checkOwnablePermission(Permission::PageCreate, $parent);
|
$this->checkOwnablePermission('page-create', $parent);
|
||||||
|
|
||||||
// Redirect to draft edit screen if signed in
|
// Redirect to draft edit screen if signed in
|
||||||
if ($this->isSignedIn()) {
|
if ($this->isSignedIn()) {
|
||||||
@@ -83,7 +81,7 @@ class PageController extends Controller
|
|||||||
$parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
|
$parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->checkOwnablePermission(Permission::PageCreate, $parent);
|
$this->checkOwnablePermission('page-create', $parent);
|
||||||
|
|
||||||
$page = $this->pageRepo->getNewDraftPage($parent);
|
$page = $this->pageRepo->getNewDraftPage($parent);
|
||||||
$this->pageRepo->publishDraft($page, [
|
$this->pageRepo->publishDraft($page, [
|
||||||
@@ -101,7 +99,7 @@ class PageController extends Controller
|
|||||||
public function editDraft(Request $request, string $bookSlug, int $pageId)
|
public function editDraft(Request $request, string $bookSlug, int $pageId)
|
||||||
{
|
{
|
||||||
$draft = $this->queries->findVisibleByIdOrFail($pageId);
|
$draft = $this->queries->findVisibleByIdOrFail($pageId);
|
||||||
$this->checkOwnablePermission(Permission::PageCreate, $draft->getParent());
|
$this->checkOwnablePermission('page-create', $draft->getParent());
|
||||||
|
|
||||||
$editorData = new PageEditorData($draft, $this->entityQueries, $request->query('editor', ''));
|
$editorData = new PageEditorData($draft, $this->entityQueries, $request->query('editor', ''));
|
||||||
$this->setPageTitle(trans('entities.pages_edit_draft'));
|
$this->setPageTitle(trans('entities.pages_edit_draft'));
|
||||||
@@ -121,7 +119,7 @@ class PageController extends Controller
|
|||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
]);
|
]);
|
||||||
$draftPage = $this->queries->findVisibleByIdOrFail($pageId);
|
$draftPage = $this->queries->findVisibleByIdOrFail($pageId);
|
||||||
$this->checkOwnablePermission(Permission::PageCreate, $draftPage->getParent());
|
$this->checkOwnablePermission('page-create', $draftPage->getParent());
|
||||||
|
|
||||||
$page = $this->pageRepo->publishDraft($draftPage, $request->all());
|
$page = $this->pageRepo->publishDraft($draftPage, $request->all());
|
||||||
|
|
||||||
@@ -149,6 +147,8 @@ class PageController extends Controller
|
|||||||
return redirect($page->getUrl());
|
return redirect($page->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->checkOwnablePermission('page-view', $page);
|
||||||
|
|
||||||
$pageContent = (new PageContent($page));
|
$pageContent = (new PageContent($page));
|
||||||
$page->html = $pageContent->render();
|
$page->html = $pageContent->render();
|
||||||
$pageNav = $pageContent->getNavigation($page->html);
|
$pageNav = $pageContent->getNavigation($page->html);
|
||||||
@@ -196,7 +196,7 @@ class PageController extends Controller
|
|||||||
public function edit(Request $request, string $bookSlug, string $pageSlug)
|
public function edit(Request $request, string $bookSlug, string $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page, $page->getUrl());
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
$editorData = new PageEditorData($page, $this->entityQueries, $request->query('editor', ''));
|
$editorData = new PageEditorData($page, $this->entityQueries, $request->query('editor', ''));
|
||||||
if ($editorData->getWarnings()) {
|
if ($editorData->getWarnings()) {
|
||||||
@@ -220,7 +220,7 @@ class PageController extends Controller
|
|||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
]);
|
]);
|
||||||
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
$this->pageRepo->update($page, $request->all());
|
$this->pageRepo->update($page, $request->all());
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ class PageController extends Controller
|
|||||||
public function saveDraft(Request $request, int $pageId)
|
public function saveDraft(Request $request, int $pageId)
|
||||||
{
|
{
|
||||||
$page = $this->queries->findVisibleByIdOrFail($pageId);
|
$page = $this->queries->findVisibleByIdOrFail($pageId);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
if (!$this->isSignedIn()) {
|
if (!$this->isSignedIn()) {
|
||||||
return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
|
return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
|
||||||
@@ -272,7 +272,7 @@ class PageController extends Controller
|
|||||||
public function showDelete(string $bookSlug, string $pageSlug)
|
public function showDelete(string $bookSlug, string $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
|
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
|
||||||
$usedAsTemplate =
|
$usedAsTemplate =
|
||||||
$this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
|
$this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
|
||||||
@@ -294,7 +294,7 @@ class PageController extends Controller
|
|||||||
public function showDeleteDraft(string $bookSlug, int $pageId)
|
public function showDeleteDraft(string $bookSlug, int $pageId)
|
||||||
{
|
{
|
||||||
$page = $this->queries->findVisibleByIdOrFail($pageId);
|
$page = $this->queries->findVisibleByIdOrFail($pageId);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
|
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
|
||||||
$usedAsTemplate =
|
$usedAsTemplate =
|
||||||
$this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
|
$this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
|
||||||
@@ -317,7 +317,7 @@ class PageController extends Controller
|
|||||||
public function destroy(string $bookSlug, string $pageSlug)
|
public function destroy(string $bookSlug, string $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
$parent = $page->getParent();
|
$parent = $page->getParent();
|
||||||
|
|
||||||
$this->pageRepo->destroy($page);
|
$this->pageRepo->destroy($page);
|
||||||
@@ -336,13 +336,13 @@ class PageController extends Controller
|
|||||||
$page = $this->queries->findVisibleByIdOrFail($pageId);
|
$page = $this->queries->findVisibleByIdOrFail($pageId);
|
||||||
$book = $page->book;
|
$book = $page->book;
|
||||||
$chapter = $page->chapter;
|
$chapter = $page->chapter;
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
$this->pageRepo->destroy($page);
|
$this->pageRepo->destroy($page);
|
||||||
|
|
||||||
$this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
|
$this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
|
||||||
|
|
||||||
if ($chapter && userCan(Permission::ChapterView, $chapter)) {
|
if ($chapter && userCan('view', $chapter)) {
|
||||||
return redirect($chapter->getUrl());
|
return redirect($chapter->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,8 +383,8 @@ class PageController extends Controller
|
|||||||
public function showMove(string $bookSlug, string $pageSlug)
|
public function showMove(string $bookSlug, string $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
|
|
||||||
return view('pages.move', [
|
return view('pages.move', [
|
||||||
'book' => $page->book,
|
'book' => $page->book,
|
||||||
@@ -401,8 +401,8 @@ class PageController extends Controller
|
|||||||
public function move(Request $request, string $bookSlug, string $pageSlug)
|
public function move(Request $request, string $bookSlug, string $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
|
|
||||||
$entitySelection = $request->get('entity_selection', null);
|
$entitySelection = $request->get('entity_selection', null);
|
||||||
if ($entitySelection === null || $entitySelection === '') {
|
if ($entitySelection === null || $entitySelection === '') {
|
||||||
@@ -430,6 +430,7 @@ class PageController extends Controller
|
|||||||
public function showCopy(string $bookSlug, string $pageSlug)
|
public function showCopy(string $bookSlug, string $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
|
$this->checkOwnablePermission('page-view', $page);
|
||||||
session()->flashInput(['name' => $page->name]);
|
session()->flashInput(['name' => $page->name]);
|
||||||
|
|
||||||
return view('pages.copy', [
|
return view('pages.copy', [
|
||||||
@@ -447,7 +448,7 @@ class PageController extends Controller
|
|||||||
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
|
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$this->checkOwnablePermission(Permission::PageView, $page);
|
$this->checkOwnablePermission('page-view', $page);
|
||||||
|
|
||||||
$entitySelection = $request->get('entity_selection') ?: null;
|
$entitySelection = $request->get('entity_selection') ?: null;
|
||||||
$newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent();
|
$newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent();
|
||||||
@@ -458,7 +459,7 @@ class PageController extends Controller
|
|||||||
return redirect($page->getUrl('/copy'));
|
return redirect($page->getUrl('/copy'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->checkOwnablePermission(Permission::PageCreate, $newParent);
|
$this->checkOwnablePermission('page-create', $newParent);
|
||||||
|
|
||||||
$newName = $request->get('name') ?: $page->name;
|
$newName = $request->get('name') ?: $page->name;
|
||||||
$pageCopy = $cloner->clonePage($page, $newParent, $newName);
|
$pageCopy = $cloner->clonePage($page, $newParent, $newName);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use BookStack\Entities\Tools\PageContent;
|
|||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Util\SimpleListOptions;
|
use BookStack\Util\SimpleListOptions;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Ssddanbrown\HtmlDiff\Diff;
|
use Ssddanbrown\HtmlDiff\Diff;
|
||||||
@@ -99,7 +98,7 @@ class PageRevisionController extends Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$prev = $revision->getPreviousRevision();
|
$prev = $revision->getPrevious();
|
||||||
$prevContent = $prev->html ?? '';
|
$prevContent = $prev->html ?? '';
|
||||||
$diff = Diff::excecute($prevContent, $revision->html);
|
$diff = Diff::excecute($prevContent, $revision->html);
|
||||||
|
|
||||||
@@ -125,7 +124,7 @@ class PageRevisionController extends Controller
|
|||||||
public function restore(string $bookSlug, string $pageSlug, int $revisionId)
|
public function restore(string $bookSlug, string $pageSlug, int $revisionId)
|
||||||
{
|
{
|
||||||
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
$page = $this->pageRepo->restoreRevision($page, $revisionId);
|
$page = $this->pageRepo->restoreRevision($page, $revisionId);
|
||||||
|
|
||||||
@@ -140,7 +139,7 @@ class PageRevisionController extends Controller
|
|||||||
public function destroy(string $bookSlug, string $pageSlug, int $revId)
|
public function destroy(string $bookSlug, string $pageSlug, int $revId)
|
||||||
{
|
{
|
||||||
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
|
|
||||||
$revision = $page->revisions()->where('id', '=', $revId)->first();
|
$revision = $page->revisions()->where('id', '=', $revId)->first();
|
||||||
if ($revision === null) {
|
if ($revision === null) {
|
||||||
|
|||||||
@@ -6,20 +6,18 @@ use BookStack\Entities\Models\Book;
|
|||||||
use BookStack\Entities\Models\BookChild;
|
use BookStack\Entities\Models\BookChild;
|
||||||
use BookStack\Entities\Models\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Models\Deletion;
|
use BookStack\Entities\Models\Deletion;
|
||||||
use BookStack\Entities\Models\Page;
|
|
||||||
use BookStack\Entities\Repos\DeletionRepo;
|
use BookStack\Entities\Repos\DeletionRepo;
|
||||||
use BookStack\Http\ApiController;
|
use BookStack\Http\ApiController;
|
||||||
use BookStack\Permissions\Permission;
|
use Closure;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
|
|
||||||
class RecycleBinApiController extends ApiController
|
class RecycleBinApiController extends ApiController
|
||||||
{
|
{
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->middleware(function ($request, $next) {
|
$this->middleware(function ($request, $next) {
|
||||||
$this->checkPermission(Permission::SettingsManage);
|
$this->checkPermission('settings-manage');
|
||||||
$this->checkPermission(Permission::RestrictionsManageAll);
|
$this->checkPermission('restrictions-manage-all');
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
});
|
});
|
||||||
@@ -42,7 +40,7 @@ class RecycleBinApiController extends ApiController
|
|||||||
'updated_at',
|
'updated_at',
|
||||||
'deletable_type',
|
'deletable_type',
|
||||||
'deletable_id',
|
'deletable_id',
|
||||||
], [$this->listFormatter(...)]);
|
], [Closure::fromCallable([$this, 'listFormatter'])]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,9 +69,10 @@ class RecycleBinApiController extends ApiController
|
|||||||
/**
|
/**
|
||||||
* Load some related details for the deletion listing.
|
* Load some related details for the deletion listing.
|
||||||
*/
|
*/
|
||||||
protected function listFormatter(Deletion $deletion): void
|
protected function listFormatter(Deletion $deletion)
|
||||||
{
|
{
|
||||||
$deletable = $deletion->deletable;
|
$deletable = $deletion->deletable;
|
||||||
|
$withTrashedQuery = fn (Builder $query) => $query->withTrashed();
|
||||||
|
|
||||||
if ($deletable instanceof BookChild) {
|
if ($deletable instanceof BookChild) {
|
||||||
$parent = $deletable->getParent();
|
$parent = $deletable->getParent();
|
||||||
@@ -82,19 +81,11 @@ class RecycleBinApiController extends ApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($deletable instanceof Book || $deletable instanceof Chapter) {
|
if ($deletable instanceof Book || $deletable instanceof Chapter) {
|
||||||
$countsToLoad = ['pages' => static::withTrashedQuery(...)];
|
$countsToLoad = ['pages' => $withTrashedQuery];
|
||||||
if ($deletable instanceof Book) {
|
if ($deletable instanceof Book) {
|
||||||
$countsToLoad['chapters'] = static::withTrashedQuery(...);
|
$countsToLoad['chapters'] = $withTrashedQuery;
|
||||||
}
|
}
|
||||||
$deletable->loadCount($countsToLoad);
|
$deletable->loadCount($countsToLoad);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Builder<Chapter|Page> $query
|
|
||||||
*/
|
|
||||||
protected static function withTrashedQuery(Builder $query): void
|
|
||||||
{
|
|
||||||
$query->withTrashed();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use BookStack\Entities\Models\Entity;
|
|||||||
use BookStack\Entities\Repos\DeletionRepo;
|
use BookStack\Entities\Repos\DeletionRepo;
|
||||||
use BookStack\Entities\Tools\TrashCan;
|
use BookStack\Entities\Tools\TrashCan;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
|
|
||||||
class RecycleBinController extends Controller
|
class RecycleBinController extends Controller
|
||||||
{
|
{
|
||||||
@@ -21,8 +20,8 @@ class RecycleBinController extends Controller
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->middleware(function ($request, $next) {
|
$this->middleware(function ($request, $next) {
|
||||||
$this->checkPermission(Permission::SettingsManage);
|
$this->checkPermission('settings-manage');
|
||||||
$this->checkPermission(Permission::RestrictionsManageAll);
|
$this->checkPermission('restrictions-manage-all');
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ use Illuminate\Support\Collection;
|
|||||||
* @property ?Page $defaultTemplate
|
* @property ?Page $defaultTemplate
|
||||||
* @property ?SortRule $sortRule
|
* @property ?SortRule $sortRule
|
||||||
*/
|
*/
|
||||||
class Book extends Entity implements CoverImageInterface, HtmlDescriptionInterface
|
class Book extends Entity implements HasCoverImage
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use HtmlDescriptionTrait;
|
use HasHtmlDescription;
|
||||||
|
|
||||||
public float $searchFactor = 1.2;
|
public float $searchFactor = 1.2;
|
||||||
|
|
||||||
@@ -95,7 +95,6 @@ class Book extends Entity implements CoverImageInterface, HtmlDescriptionInterfa
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all pages within this book.
|
* Get all pages within this book.
|
||||||
* @return HasMany<Page, $this>
|
|
||||||
*/
|
*/
|
||||||
public function pages(): HasMany
|
public function pages(): HasMany
|
||||||
{
|
{
|
||||||
@@ -112,7 +111,6 @@ class Book extends Entity implements CoverImageInterface, HtmlDescriptionInterfa
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all chapters within this book.
|
* Get all chapters within this book.
|
||||||
* @return HasMany<Chapter, $this>
|
|
||||||
*/
|
*/
|
||||||
public function chapters(): HasMany
|
public function chapters(): HasMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
class Bookshelf extends Entity implements CoverImageInterface, HtmlDescriptionInterface
|
class Bookshelf extends Entity implements HasCoverImage
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use HtmlDescriptionTrait;
|
use HasHtmlDescription;
|
||||||
|
|
||||||
protected $table = 'bookshelves';
|
protected $table = 'bookshelves';
|
||||||
|
|
||||||
@@ -70,7 +70,6 @@ class Bookshelf extends Entity implements CoverImageInterface, HtmlDescriptionIn
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the cover image of the shelf.
|
* Get the cover image of the shelf.
|
||||||
* @return BelongsTo<Image, $this>
|
|
||||||
*/
|
*/
|
||||||
public function cover(): BelongsTo
|
public function cover(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ use Illuminate\Support\Collection;
|
|||||||
* @property ?int $default_template_id
|
* @property ?int $default_template_id
|
||||||
* @property ?Page $defaultTemplate
|
* @property ?Page $defaultTemplate
|
||||||
*/
|
*/
|
||||||
class Chapter extends BookChild implements HtmlDescriptionInterface
|
class Chapter extends BookChild
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use HtmlDescriptionTrait;
|
use HasHtmlDescription;
|
||||||
|
|
||||||
public float $searchFactor = 1.2;
|
public float $searchFactor = 1.2;
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ class Chapter extends BookChild implements HtmlDescriptionInterface
|
|||||||
/**
|
/**
|
||||||
* Get the pages that this chapter contains.
|
* Get the pages that this chapter contains.
|
||||||
*
|
*
|
||||||
* @return HasMany<Page, $this>
|
* @return HasMany<Page>
|
||||||
*/
|
*/
|
||||||
public function pages(string $dir = 'ASC'): HasMany
|
public function pages(string $dir = 'ASC'): HasMany
|
||||||
{
|
{
|
||||||
@@ -60,7 +60,7 @@ class Chapter extends BookChild implements HtmlDescriptionInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the visible pages in this chapter.
|
* Get the visible pages in this chapter.
|
||||||
* @return Collection<Page>
|
* @returns Collection<Page>
|
||||||
*/
|
*/
|
||||||
public function getVisiblePages(): Collection
|
public function getVisiblePages(): Collection
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
|
|||||||
* A model that can be deleted in a manner that deletions
|
* A model that can be deleted in a manner that deletions
|
||||||
* are tracked to be part of the recycle bin system.
|
* are tracked to be part of the recycle bin system.
|
||||||
*/
|
*/
|
||||||
interface DeletableInterface
|
interface Deletable
|
||||||
{
|
{
|
||||||
public function deletions(): MorphMany;
|
public function deletions(): MorphMany;
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|||||||
* @property int $deleted_by
|
* @property int $deleted_by
|
||||||
* @property string $deletable_type
|
* @property string $deletable_type
|
||||||
* @property int $deletable_id
|
* @property int $deletable_id
|
||||||
* @property DeletableInterface $deletable
|
* @property Deletable $deletable
|
||||||
*/
|
*/
|
||||||
class Deletion extends Model implements Loggable
|
class Deletion extends Model implements Loggable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use BookStack\Activity\Models\View;
|
|||||||
use BookStack\Activity\Models\Viewable;
|
use BookStack\Activity\Models\Viewable;
|
||||||
use BookStack\Activity\Models\Watch;
|
use BookStack\Activity\Models\Watch;
|
||||||
use BookStack\App\Model;
|
use BookStack\App\Model;
|
||||||
use BookStack\App\SluggableInterface;
|
use BookStack\App\Sluggable;
|
||||||
use BookStack\Entities\Tools\SlugGenerator;
|
use BookStack\Entities\Tools\SlugGenerator;
|
||||||
use BookStack\Permissions\JointPermissionBuilder;
|
use BookStack\Permissions\JointPermissionBuilder;
|
||||||
use BookStack\Permissions\Models\EntityPermission;
|
use BookStack\Permissions\Models\EntityPermission;
|
||||||
@@ -22,12 +22,10 @@ use BookStack\References\Reference;
|
|||||||
use BookStack\Search\SearchIndex;
|
use BookStack\Search\SearchIndex;
|
||||||
use BookStack\Search\SearchTerm;
|
use BookStack\Search\SearchTerm;
|
||||||
use BookStack\Users\Models\HasCreatorAndUpdater;
|
use BookStack\Users\Models\HasCreatorAndUpdater;
|
||||||
use BookStack\Users\Models\OwnableInterface;
|
use BookStack\Users\Models\HasOwner;
|
||||||
use BookStack\Users\Models\User;
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
@@ -44,23 +42,17 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
* @property Carbon $deleted_at
|
* @property Carbon $deleted_at
|
||||||
* @property int $created_by
|
* @property int $created_by
|
||||||
* @property int $updated_by
|
* @property int $updated_by
|
||||||
* @property int $owned_by
|
|
||||||
* @property Collection $tags
|
* @property Collection $tags
|
||||||
*
|
*
|
||||||
* @method static Entity|Builder visible()
|
* @method static Entity|Builder visible()
|
||||||
* @method static Builder withLastView()
|
* @method static Builder withLastView()
|
||||||
* @method static Builder withViewCount()
|
* @method static Builder withViewCount()
|
||||||
*/
|
*/
|
||||||
abstract class Entity extends Model implements
|
abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, Deletable, Loggable
|
||||||
SluggableInterface,
|
|
||||||
Favouritable,
|
|
||||||
Viewable,
|
|
||||||
DeletableInterface,
|
|
||||||
OwnableInterface,
|
|
||||||
Loggable
|
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes;
|
||||||
use HasCreatorAndUpdater;
|
use HasCreatorAndUpdater;
|
||||||
|
use HasOwner;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string - Name of property where the main text content is found
|
* @var string - Name of property where the main text content is found
|
||||||
@@ -207,20 +199,6 @@ abstract class Entity extends Model implements
|
|||||||
return $this->morphMany(JointPermission::class, 'entity');
|
return $this->morphMany(JointPermission::class, 'entity');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user who owns this entity.
|
|
||||||
* @return BelongsTo<User, $this>
|
|
||||||
*/
|
|
||||||
public function ownedBy(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'owned_by');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getOwnerFieldName(): string
|
|
||||||
{
|
|
||||||
return 'owned_by';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the related delete records for this entity.
|
* Get the related delete records for this entity.
|
||||||
*/
|
*/
|
||||||
@@ -305,14 +283,10 @@ abstract class Entity extends Model implements
|
|||||||
public function getParent(): ?self
|
public function getParent(): ?self
|
||||||
{
|
{
|
||||||
if ($this instanceof Page) {
|
if ($this instanceof Page) {
|
||||||
/** @var BelongsTo<Chapter|Book, Page> $builder */
|
return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
|
||||||
$builder = $this->chapter_id ? $this->chapter() : $this->book();
|
|
||||||
return $builder->withTrashed()->first();
|
|
||||||
}
|
}
|
||||||
if ($this instanceof Chapter) {
|
if ($this instanceof Chapter) {
|
||||||
/** @var BelongsTo<Book, Page> $builder */
|
return $this->book()->withTrashed()->first();
|
||||||
$builder = $this->book();
|
|
||||||
return $builder->withTrashed()->first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -321,7 +295,7 @@ abstract class Entity extends Model implements
|
|||||||
/**
|
/**
|
||||||
* Rebuild the permissions for this entity.
|
* Rebuild the permissions for this entity.
|
||||||
*/
|
*/
|
||||||
public function rebuildPermissions(): void
|
public function rebuildPermissions()
|
||||||
{
|
{
|
||||||
app()->make(JointPermissionBuilder::class)->rebuildForEntity(clone $this);
|
app()->make(JointPermissionBuilder::class)->rebuildForEntity(clone $this);
|
||||||
}
|
}
|
||||||
@@ -329,7 +303,7 @@ abstract class Entity extends Model implements
|
|||||||
/**
|
/**
|
||||||
* Index the current entity for search.
|
* Index the current entity for search.
|
||||||
*/
|
*/
|
||||||
public function indexForSearch(): void
|
public function indexForSearch()
|
||||||
{
|
{
|
||||||
app()->make(SearchIndex::class)->indexEntity(clone $this);
|
app()->make(SearchIndex::class)->indexEntity(clone $this);
|
||||||
}
|
}
|
||||||
@@ -339,7 +313,7 @@ abstract class Entity extends Model implements
|
|||||||
*/
|
*/
|
||||||
public function refreshSlug(): string
|
public function refreshSlug(): string
|
||||||
{
|
{
|
||||||
$this->slug = app()->make(SlugGenerator::class)->generate($this, $this->name);
|
$this->slug = app()->make(SlugGenerator::class)->generate($this);
|
||||||
|
|
||||||
return $this->slug;
|
return $this->slug;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace BookStack\Entities\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
interface CoverImageInterface
|
interface HasCoverImage
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the cover image for this item.
|
* Get the cover image for this item.
|
||||||
21
app/Entities/Models/HasHtmlDescription.php
Normal file
21
app/Entities/Models/HasHtmlDescription.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
|
use BookStack\Util\HtmlContentFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string $description
|
||||||
|
* @property string $description_html
|
||||||
|
*/
|
||||||
|
trait HasHtmlDescription
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the HTML description for this book.
|
||||||
|
*/
|
||||||
|
public function descriptionHtml(): string
|
||||||
|
{
|
||||||
|
$html = $this->description_html ?: '<p>' . nl2br(e($this->description)) . '</p>';
|
||||||
|
return HtmlContentFilter::removeScriptsFromHtmlString($html);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Entities\Models;
|
|
||||||
|
|
||||||
interface HtmlDescriptionInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get the HTML-based description for this item.
|
|
||||||
* By default, the content should be sanitised unless raw is set to true.
|
|
||||||
*/
|
|
||||||
public function descriptionHtml(bool $raw = false): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the HTML-based description for this item.
|
|
||||||
*/
|
|
||||||
public function setDescriptionHtml(string $html, string|null $plaintext = null): void;
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Entities\Models;
|
|
||||||
|
|
||||||
use BookStack\Util\HtmlContentFilter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @property string $description
|
|
||||||
* @property string $description_html
|
|
||||||
*/
|
|
||||||
trait HtmlDescriptionTrait
|
|
||||||
{
|
|
||||||
public function descriptionHtml(bool $raw = false): string
|
|
||||||
{
|
|
||||||
$html = $this->description_html ?: '<p>' . nl2br(e($this->description)) . '</p>';
|
|
||||||
if ($raw) {
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
return HtmlContentFilter::removeScriptsFromHtmlString($html);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setDescriptionHtml(string $html, string|null $plaintext = null): void
|
|
||||||
{
|
|
||||||
$this->description_html = $html;
|
|
||||||
|
|
||||||
if ($plaintext !== null) {
|
|
||||||
$this->description = $plaintext;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($html) && !empty($plaintext)) {
|
|
||||||
$this->description_html = $this->descriptionHtml();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -60,7 +60,7 @@ class PageRevision extends Model implements Loggable
|
|||||||
/**
|
/**
|
||||||
* Get the previous revision for the same page if existing.
|
* Get the previous revision for the same page if existing.
|
||||||
*/
|
*/
|
||||||
public function getPreviousRevision(): ?PageRevision
|
public function getPrevious(): ?PageRevision
|
||||||
{
|
{
|
||||||
$id = static::newQuery()->where('page_id', '=', $this->page_id)
|
$id = static::newQuery()->where('page_id', '=', $this->page_id)
|
||||||
->where('id', '<', $this->id)
|
->where('id', '<', $this->id)
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ use BookStack\Entities\Models\Book;
|
|||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements ProvidesEntityQueries<Book>
|
|
||||||
*/
|
|
||||||
class BookQueries implements ProvidesEntityQueries
|
class BookQueries implements ProvidesEntityQueries
|
||||||
{
|
{
|
||||||
protected static array $listAttributes = [
|
protected static array $listAttributes = [
|
||||||
@@ -16,9 +13,6 @@ class BookQueries implements ProvidesEntityQueries
|
|||||||
'created_at', 'updated_at', 'image_id', 'owned_by',
|
'created_at', 'updated_at', 'image_id', 'owned_by',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Builder<Book>
|
|
||||||
*/
|
|
||||||
public function start(): Builder
|
public function start(): Builder
|
||||||
{
|
{
|
||||||
return Book::query();
|
return Book::query();
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ use BookStack\Entities\Models\Bookshelf;
|
|||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements ProvidesEntityQueries<Bookshelf>
|
|
||||||
*/
|
|
||||||
class BookshelfQueries implements ProvidesEntityQueries
|
class BookshelfQueries implements ProvidesEntityQueries
|
||||||
{
|
{
|
||||||
protected static array $listAttributes = [
|
protected static array $listAttributes = [
|
||||||
@@ -16,9 +13,6 @@ class BookshelfQueries implements ProvidesEntityQueries
|
|||||||
'created_at', 'updated_at', 'image_id', 'owned_by',
|
'created_at', 'updated_at', 'image_id', 'owned_by',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Builder<Bookshelf>
|
|
||||||
*/
|
|
||||||
public function start(): Builder
|
public function start(): Builder
|
||||||
{
|
{
|
||||||
return Bookshelf::query();
|
return Bookshelf::query();
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ use BookStack\Entities\Models\Chapter;
|
|||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements ProvidesEntityQueries<Chapter>
|
|
||||||
*/
|
|
||||||
class ChapterQueries implements ProvidesEntityQueries
|
class ChapterQueries implements ProvidesEntityQueries
|
||||||
{
|
{
|
||||||
protected static array $listAttributes = [
|
protected static array $listAttributes = [
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ class EntityQueries
|
|||||||
/**
|
/**
|
||||||
* Start a query of visible entities of the given type,
|
* Start a query of visible entities of the given type,
|
||||||
* suitable for listing display.
|
* suitable for listing display.
|
||||||
* @return Builder<Entity>
|
|
||||||
*/
|
*/
|
||||||
public function visibleForList(string $entityType): Builder
|
public function visibleForList(string $entityType): Builder
|
||||||
{
|
{
|
||||||
@@ -45,6 +44,7 @@ class EntityQueries
|
|||||||
|
|
||||||
protected function getQueriesForType(string $type): ProvidesEntityQueries
|
protected function getQueriesForType(string $type): ProvidesEntityQueries
|
||||||
{
|
{
|
||||||
|
/** @var ?ProvidesEntityQueries $queries */
|
||||||
$queries = match ($type) {
|
$queries = match ($type) {
|
||||||
'page' => $this->pages,
|
'page' => $this->pages,
|
||||||
'chapter' => $this->chapters,
|
'chapter' => $this->chapters,
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ use BookStack\Entities\Models\Page;
|
|||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements ProvidesEntityQueries<Page>
|
|
||||||
*/
|
|
||||||
class PageQueries implements ProvidesEntityQueries
|
class PageQueries implements ProvidesEntityQueries
|
||||||
{
|
{
|
||||||
protected static array $contentAttributes = [
|
protected static array $contentAttributes = [
|
||||||
@@ -21,9 +18,6 @@ class PageQueries implements ProvidesEntityQueries
|
|||||||
'template', 'text', 'created_at', 'updated_at', 'priority', 'owned_by',
|
'template', 'text', 'created_at', 'updated_at', 'priority', 'owned_by',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Builder<Page>
|
|
||||||
*/
|
|
||||||
public function start(): Builder
|
public function start(): Builder
|
||||||
{
|
{
|
||||||
return Page::query();
|
return Page::query();
|
||||||
@@ -72,9 +66,6 @@ class PageQueries implements ProvidesEntityQueries
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Builder<Page>
|
|
||||||
*/
|
|
||||||
public function visibleForList(): Builder
|
public function visibleForList(): Builder
|
||||||
{
|
{
|
||||||
return $this->start()
|
return $this->start()
|
||||||
|
|||||||
@@ -7,32 +7,28 @@ use Illuminate\Database\Eloquent\Builder;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for our classes which provide common queries for our
|
* Interface for our classes which provide common queries for our
|
||||||
* entity objects. Ideally, all queries for entities should run through
|
* entity objects. Ideally all queries for entities should run through
|
||||||
* these classes.
|
* these classes.
|
||||||
* Any added methods should return a builder instances to allow extension
|
* Any added methods should return a builder instances to allow extension
|
||||||
* via building on the query, unless the method starts with 'find'
|
* via building on the query, unless the method starts with 'find'
|
||||||
* in which case an entity object should be returned.
|
* in which case an entity object should be returned.
|
||||||
* (nullable unless it's a *OrFail method).
|
* (nullable unless it's a *OrFail method).
|
||||||
*
|
|
||||||
* @template TModel of Entity
|
|
||||||
*/
|
*/
|
||||||
interface ProvidesEntityQueries
|
interface ProvidesEntityQueries
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Start a new query for this entity type.
|
* Start a new query for this entity type.
|
||||||
* @return Builder<TModel>
|
|
||||||
*/
|
*/
|
||||||
public function start(): Builder;
|
public function start(): Builder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the entity of the given ID or return null if not found.
|
* Find the entity of the given ID, or return null if not found.
|
||||||
*/
|
*/
|
||||||
public function findVisibleById(int $id): ?Entity;
|
public function findVisibleById(int $id): ?Entity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a query for items that are visible, with selection
|
* Start a query for items that are visible, with selection
|
||||||
* configured for list display of this item.
|
* configured for list display of this item.
|
||||||
* @return Builder<TModel>
|
|
||||||
*/
|
*/
|
||||||
public function visibleForList(): Builder;
|
public function visibleForList(): Builder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ use BookStack\Entities\Models\Book;
|
|||||||
use BookStack\Entities\Models\BookChild;
|
use BookStack\Entities\Models\BookChild;
|
||||||
use BookStack\Entities\Models\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Models\CoverImageInterface;
|
use BookStack\Entities\Models\HasCoverImage;
|
||||||
use BookStack\Entities\Models\HtmlDescriptionInterface;
|
use BookStack\Entities\Models\HasHtmlDescription;
|
||||||
use BookStack\Entities\Models\HtmlDescriptionTrait;
|
|
||||||
use BookStack\Entities\Queries\PageQueries;
|
use BookStack\Entities\Queries\PageQueries;
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\References\ReferenceStore;
|
use BookStack\References\ReferenceStore;
|
||||||
@@ -78,6 +77,7 @@ class BaseRepo
|
|||||||
$entity->touch();
|
$entity->touch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$entity->rebuildPermissions();
|
||||||
$entity->indexForSearch();
|
$entity->indexForSearch();
|
||||||
$this->referenceStore->updateForEntity($entity);
|
$this->referenceStore->updateForEntity($entity);
|
||||||
|
|
||||||
@@ -89,10 +89,12 @@ class BaseRepo
|
|||||||
/**
|
/**
|
||||||
* Update the given items' cover image, or clear it.
|
* Update the given items' cover image, or clear it.
|
||||||
*
|
*
|
||||||
|
* @param Entity&HasCoverImage $entity
|
||||||
|
*
|
||||||
* @throws ImageUploadException
|
* @throws ImageUploadException
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function updateCoverImage(Entity&CoverImageInterface $entity, ?UploadedFile $coverImage, bool $removeImage = false)
|
public function updateCoverImage($entity, ?UploadedFile $coverImage, bool $removeImage = false)
|
||||||
{
|
{
|
||||||
if ($coverImage) {
|
if ($coverImage) {
|
||||||
$imageType = $entity->coverImageTypeKey();
|
$imageType = $entity->coverImageTypeKey();
|
||||||
@@ -104,7 +106,7 @@ class BaseRepo
|
|||||||
|
|
||||||
if ($removeImage) {
|
if ($removeImage) {
|
||||||
$this->imageRepo->destroyImage($entity->cover()->first());
|
$this->imageRepo->destroyImage($entity->cover()->first());
|
||||||
$entity->cover()->dissociate();
|
$entity->image_id = 0;
|
||||||
$entity->save();
|
$entity->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,7 +139,7 @@ class BaseRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort the parent of the given entity, if any auto sort actions are set for it.
|
* Sort the parent of the given entity, if any auto sort actions are set for it.
|
||||||
* Typically ran during create/update/insert events.
|
* Typical ran during create/update/insert events.
|
||||||
*/
|
*/
|
||||||
public function sortParent(Entity $entity): void
|
public function sortParent(Entity $entity): void
|
||||||
{
|
{
|
||||||
@@ -149,17 +151,18 @@ class BaseRepo
|
|||||||
|
|
||||||
protected function updateDescription(Entity $entity, array $input): void
|
protected function updateDescription(Entity $entity, array $input): void
|
||||||
{
|
{
|
||||||
if (!($entity instanceof HtmlDescriptionInterface)) {
|
if (!in_array(HasHtmlDescription::class, class_uses($entity))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @var HasHtmlDescription $entity */
|
||||||
if (isset($input['description_html'])) {
|
if (isset($input['description_html'])) {
|
||||||
$entity->setDescriptionHtml(
|
$entity->description_html = HtmlDescriptionFilter::filterFromString($input['description_html']);
|
||||||
HtmlDescriptionFilter::filterFromString($input['description_html']),
|
$entity->description = html_entity_decode(strip_tags($input['description_html']));
|
||||||
html_entity_decode(strip_tags($input['description_html']))
|
|
||||||
);
|
|
||||||
} else if (isset($input['description'])) {
|
} else if (isset($input['description'])) {
|
||||||
$entity->setDescriptionHtml('', $input['description']);
|
$entity->description = $input['description'];
|
||||||
|
$entity->description_html = '';
|
||||||
|
$entity->description_html = $entity->descriptionHtml();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use BookStack\Exceptions\ImageUploadException;
|
|||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Sorting\SortRule;
|
use BookStack\Sorting\SortRule;
|
||||||
use BookStack\Uploads\ImageRepo;
|
use BookStack\Uploads\ImageRepo;
|
||||||
use BookStack\Util\DatabaseTransaction;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
|
|
||||||
@@ -29,22 +28,19 @@ class BookRepo
|
|||||||
*/
|
*/
|
||||||
public function create(array $input): Book
|
public function create(array $input): Book
|
||||||
{
|
{
|
||||||
return (new DatabaseTransaction(function () use ($input) {
|
$book = new Book();
|
||||||
$book = new Book();
|
$this->baseRepo->create($book, $input);
|
||||||
|
$this->baseRepo->updateCoverImage($book, $input['image'] ?? null);
|
||||||
|
$this->baseRepo->updateDefaultTemplate($book, intval($input['default_template_id'] ?? null));
|
||||||
|
Activity::add(ActivityType::BOOK_CREATE, $book);
|
||||||
|
|
||||||
$this->baseRepo->create($book, $input);
|
$defaultBookSortSetting = intval(setting('sorting-book-default', '0'));
|
||||||
$this->baseRepo->updateCoverImage($book, $input['image'] ?? null);
|
if ($defaultBookSortSetting && SortRule::query()->find($defaultBookSortSetting)) {
|
||||||
$this->baseRepo->updateDefaultTemplate($book, intval($input['default_template_id'] ?? null));
|
$book->sort_rule_id = $defaultBookSortSetting;
|
||||||
Activity::add(ActivityType::BOOK_CREATE, $book);
|
$book->save();
|
||||||
|
}
|
||||||
|
|
||||||
$defaultBookSortSetting = intval(setting('sorting-book-default', '0'));
|
return $book;
|
||||||
if ($defaultBookSortSetting && SortRule::query()->find($defaultBookSortSetting)) {
|
|
||||||
$book->sort_rule_id = $defaultBookSortSetting;
|
|
||||||
$book->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $book;
|
|
||||||
}))->run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use BookStack\Entities\Models\Bookshelf;
|
|||||||
use BookStack\Entities\Queries\BookQueries;
|
use BookStack\Entities\Queries\BookQueries;
|
||||||
use BookStack\Entities\Tools\TrashCan;
|
use BookStack\Entities\Tools\TrashCan;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Util\DatabaseTransaction;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class BookshelfRepo
|
class BookshelfRepo
|
||||||
@@ -24,14 +23,13 @@ class BookshelfRepo
|
|||||||
*/
|
*/
|
||||||
public function create(array $input, array $bookIds): Bookshelf
|
public function create(array $input, array $bookIds): Bookshelf
|
||||||
{
|
{
|
||||||
return (new DatabaseTransaction(function () use ($input, $bookIds) {
|
$shelf = new Bookshelf();
|
||||||
$shelf = new Bookshelf();
|
$this->baseRepo->create($shelf, $input);
|
||||||
$this->baseRepo->create($shelf, $input);
|
$this->baseRepo->updateCoverImage($shelf, $input['image'] ?? null);
|
||||||
$this->baseRepo->updateCoverImage($shelf, $input['image'] ?? null);
|
$this->updateBooks($shelf, $bookIds);
|
||||||
$this->updateBooks($shelf, $bookIds);
|
Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
|
||||||
Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
|
|
||||||
return $shelf;
|
return $shelf;
|
||||||
}))->run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,37 +54,20 @@ class BookshelfRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update which books are assigned to this shelf by syncing the given book ids.
|
* Update which books are assigned to this shelf by syncing the given book ids.
|
||||||
* Function ensures the managed books are visible to the current user and existing,
|
* Function ensures the books are visible to the current user and existing.
|
||||||
* and that the user does not alter the assignment of books that are not visible to them.
|
|
||||||
*/
|
*/
|
||||||
protected function updateBooks(Bookshelf $shelf, array $bookIds): void
|
protected function updateBooks(Bookshelf $shelf, array $bookIds)
|
||||||
{
|
{
|
||||||
$numericIDs = collect($bookIds)->map(function ($id) {
|
$numericIDs = collect($bookIds)->map(function ($id) {
|
||||||
return intval($id);
|
return intval($id);
|
||||||
});
|
});
|
||||||
|
|
||||||
$existingBookIds = $shelf->books()->pluck('id')->toArray();
|
$syncData = $this->bookQueries->visibleForList()
|
||||||
$visibleExistingBookIds = $this->bookQueries->visibleForList()
|
|
||||||
->whereIn('id', $existingBookIds)
|
|
||||||
->pluck('id')
|
|
||||||
->toArray();
|
|
||||||
$nonVisibleExistingBookIds = array_values(array_diff($existingBookIds, $visibleExistingBookIds));
|
|
||||||
|
|
||||||
$newIdsToAssign = $this->bookQueries->visibleForList()
|
|
||||||
->whereIn('id', $bookIds)
|
->whereIn('id', $bookIds)
|
||||||
->pluck('id')
|
->pluck('id')
|
||||||
->toArray();
|
->mapWithKeys(function ($bookId) use ($numericIDs) {
|
||||||
|
return [$bookId => ['order' => $numericIDs->search($bookId)]];
|
||||||
$maxNewIndex = max($numericIDs->keys()->toArray() ?: [0]);
|
});
|
||||||
|
|
||||||
$syncData = [];
|
|
||||||
foreach ($newIdsToAssign as $id) {
|
|
||||||
$syncData[$id] = ['order' => $numericIDs->search($id)];
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($nonVisibleExistingBookIds as $index => $id) {
|
|
||||||
$syncData[$id] = ['order' => $maxNewIndex + ($index + 1)];
|
|
||||||
}
|
|
||||||
|
|
||||||
$shelf->books()->sync($syncData);
|
$shelf->books()->sync($syncData);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ use BookStack\Entities\Tools\TrashCan;
|
|||||||
use BookStack\Exceptions\MoveOperationException;
|
use BookStack\Exceptions\MoveOperationException;
|
||||||
use BookStack\Exceptions\PermissionsException;
|
use BookStack\Exceptions\PermissionsException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Util\DatabaseTransaction;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class ChapterRepo
|
class ChapterRepo
|
||||||
@@ -29,18 +27,16 @@ class ChapterRepo
|
|||||||
*/
|
*/
|
||||||
public function create(array $input, Book $parentBook): Chapter
|
public function create(array $input, Book $parentBook): Chapter
|
||||||
{
|
{
|
||||||
return (new DatabaseTransaction(function () use ($input, $parentBook) {
|
$chapter = new Chapter();
|
||||||
$chapter = new Chapter();
|
$chapter->book_id = $parentBook->id;
|
||||||
$chapter->book_id = $parentBook->id;
|
$chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
|
||||||
$chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
|
$this->baseRepo->create($chapter, $input);
|
||||||
$this->baseRepo->create($chapter, $input);
|
$this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id'] ?? null));
|
||||||
$this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id'] ?? null));
|
Activity::add(ActivityType::CHAPTER_CREATE, $chapter);
|
||||||
Activity::add(ActivityType::CHAPTER_CREATE, $chapter);
|
|
||||||
|
|
||||||
$this->baseRepo->sortParent($chapter);
|
$this->baseRepo->sortParent($chapter);
|
||||||
|
|
||||||
return $chapter;
|
return $chapter;
|
||||||
}))->run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,18 +84,16 @@ class ChapterRepo
|
|||||||
throw new MoveOperationException('Book to move chapter into not found');
|
throw new MoveOperationException('Book to move chapter into not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userCan(Permission::ChapterCreate, $parent)) {
|
if (!userCan('chapter-create', $parent)) {
|
||||||
throw new PermissionsException('User does not have permission to create a chapter within the chosen book');
|
throw new PermissionsException('User does not have permission to create a chapter within the chosen book');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (new DatabaseTransaction(function () use ($chapter, $parent) {
|
$chapter->changeBook($parent->id);
|
||||||
$chapter->changeBook($parent->id);
|
$chapter->rebuildPermissions();
|
||||||
$chapter->rebuildPermissions();
|
Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
|
||||||
Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
|
|
||||||
|
|
||||||
$this->baseRepo->sortParent($chapter);
|
$this->baseRepo->sortParent($chapter);
|
||||||
|
|
||||||
return $parent;
|
return $parent;
|
||||||
}))->run();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,8 @@ use BookStack\Entities\Tools\TrashCan;
|
|||||||
use BookStack\Exceptions\MoveOperationException;
|
use BookStack\Exceptions\MoveOperationException;
|
||||||
use BookStack\Exceptions\PermissionsException;
|
use BookStack\Exceptions\PermissionsException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\References\ReferenceStore;
|
use BookStack\References\ReferenceStore;
|
||||||
use BookStack\References\ReferenceUpdater;
|
use BookStack\References\ReferenceUpdater;
|
||||||
use BookStack\Util\DatabaseTransaction;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class PageRepo
|
class PageRepo
|
||||||
@@ -56,17 +54,15 @@ class PageRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
$defaultTemplate = $page->chapter->defaultTemplate ?? $page->book->defaultTemplate;
|
$defaultTemplate = $page->chapter->defaultTemplate ?? $page->book->defaultTemplate;
|
||||||
if ($defaultTemplate && userCan(Permission::PageView, $defaultTemplate)) {
|
if ($defaultTemplate && userCan('view', $defaultTemplate)) {
|
||||||
$page->forceFill([
|
$page->forceFill([
|
||||||
'html' => $defaultTemplate->html,
|
'html' => $defaultTemplate->html,
|
||||||
'markdown' => $defaultTemplate->markdown,
|
'markdown' => $defaultTemplate->markdown,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
(new DatabaseTransaction(function () use ($page) {
|
$page->save();
|
||||||
$page->save();
|
$page->refresh()->rebuildPermissions();
|
||||||
$page->refresh()->rebuildPermissions();
|
|
||||||
}))->run();
|
|
||||||
|
|
||||||
return $page;
|
return $page;
|
||||||
}
|
}
|
||||||
@@ -76,29 +72,26 @@ class PageRepo
|
|||||||
*/
|
*/
|
||||||
public function publishDraft(Page $draft, array $input): Page
|
public function publishDraft(Page $draft, array $input): Page
|
||||||
{
|
{
|
||||||
return (new DatabaseTransaction(function () use ($draft, $input) {
|
$draft->draft = false;
|
||||||
$draft->draft = false;
|
$draft->revision_count = 1;
|
||||||
$draft->revision_count = 1;
|
$draft->priority = $this->getNewPriority($draft);
|
||||||
$draft->priority = $this->getNewPriority($draft);
|
$this->updateTemplateStatusAndContentFromInput($draft, $input);
|
||||||
$this->updateTemplateStatusAndContentFromInput($draft, $input);
|
$this->baseRepo->update($draft, $input);
|
||||||
$this->baseRepo->update($draft, $input);
|
|
||||||
$draft->rebuildPermissions();
|
|
||||||
|
|
||||||
$summary = trim($input['summary'] ?? '') ?: trans('entities.pages_initial_revision');
|
$summary = trim($input['summary'] ?? '') ?: trans('entities.pages_initial_revision');
|
||||||
$this->revisionRepo->storeNewForPage($draft, $summary);
|
$this->revisionRepo->storeNewForPage($draft, $summary);
|
||||||
$draft->refresh();
|
$draft->refresh();
|
||||||
|
|
||||||
Activity::add(ActivityType::PAGE_CREATE, $draft);
|
Activity::add(ActivityType::PAGE_CREATE, $draft);
|
||||||
$this->baseRepo->sortParent($draft);
|
$this->baseRepo->sortParent($draft);
|
||||||
|
|
||||||
return $draft;
|
return $draft;
|
||||||
}))->run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directly update the content for the given page from the provided input.
|
* Directly update the content for the given page from the provided input.
|
||||||
* Used for direct content access in a way that performs required changes
|
* Used for direct content access in a way that performs required changes
|
||||||
* (Search index and reference regen) without performing an official update.
|
* (Search index & reference regen) without performing an official update.
|
||||||
*/
|
*/
|
||||||
public function setContentFromInput(Page $page, array $input): void
|
public function setContentFromInput(Page $page, array $input): void
|
||||||
{
|
{
|
||||||
@@ -123,7 +116,7 @@ class PageRepo
|
|||||||
$page->revision_count++;
|
$page->revision_count++;
|
||||||
$page->save();
|
$page->save();
|
||||||
|
|
||||||
// Remove all update drafts for this user and page.
|
// Remove all update drafts for this user & page.
|
||||||
$this->revisionRepo->deleteDraftsForCurrentUser($page);
|
$this->revisionRepo->deleteDraftsForCurrentUser($page);
|
||||||
|
|
||||||
// Save a revision after updating
|
// Save a revision after updating
|
||||||
@@ -143,7 +136,7 @@ class PageRepo
|
|||||||
|
|
||||||
protected function updateTemplateStatusAndContentFromInput(Page $page, array $input): void
|
protected function updateTemplateStatusAndContentFromInput(Page $page, array $input): void
|
||||||
{
|
{
|
||||||
if (isset($input['template']) && userCan(Permission::TemplatesManage)) {
|
if (isset($input['template']) && userCan('templates-manage')) {
|
||||||
$page->template = ($input['template'] === 'true');
|
$page->template = ($input['template'] === 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +159,7 @@ class PageRepo
|
|||||||
$pageContent->setNewHTML($input['html'], user());
|
$pageContent->setNewHTML($input['html'], user());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($newEditor !== $currentEditor || empty($page->editor)) && userCan(Permission::EditorChange)) {
|
if (($newEditor !== $currentEditor || empty($page->editor)) && userCan('editor-change')) {
|
||||||
$page->editor = $newEditor->value;
|
$page->editor = $newEditor->value;
|
||||||
} elseif (empty($page->editor)) {
|
} elseif (empty($page->editor)) {
|
||||||
$page->editor = $defaultEditor->value;
|
$page->editor = $defaultEditor->value;
|
||||||
@@ -272,22 +265,20 @@ class PageRepo
|
|||||||
throw new MoveOperationException('Book or chapter to move page into not found');
|
throw new MoveOperationException('Book or chapter to move page into not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userCan(Permission::PageCreate, $parent)) {
|
if (!userCan('page-create', $parent)) {
|
||||||
throw new PermissionsException('User does not have permission to create a page within the new parent');
|
throw new PermissionsException('User does not have permission to create a page within the new parent');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (new DatabaseTransaction(function () use ($page, $parent) {
|
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null;
|
||||||
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null;
|
$newBookId = ($parent instanceof Chapter) ? $parent->book->id : $parent->id;
|
||||||
$newBookId = ($parent instanceof Chapter) ? $parent->book->id : $parent->id;
|
$page->changeBook($newBookId);
|
||||||
$page->changeBook($newBookId);
|
$page->rebuildPermissions();
|
||||||
$page->rebuildPermissions();
|
|
||||||
|
|
||||||
Activity::add(ActivityType::PAGE_MOVE, $page);
|
Activity::add(ActivityType::PAGE_MOVE, $page);
|
||||||
|
|
||||||
$this->baseRepo->sortParent($page);
|
$this->baseRepo->sortParent($page);
|
||||||
|
|
||||||
return $parent;
|
return $parent;
|
||||||
}))->run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ use BookStack\Entities\Models\Book;
|
|||||||
use BookStack\Entities\Models\Bookshelf;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
use BookStack\Entities\Models\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Models\CoverImageInterface;
|
use BookStack\Entities\Models\HasCoverImage;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Entities\Repos\BookRepo;
|
use BookStack\Entities\Repos\BookRepo;
|
||||||
use BookStack\Entities\Repos\ChapterRepo;
|
use BookStack\Entities\Repos\ChapterRepo;
|
||||||
use BookStack\Entities\Repos\PageRepo;
|
use BookStack\Entities\Repos\PageRepo;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Uploads\Image;
|
use BookStack\Uploads\Image;
|
||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
@@ -50,7 +49,7 @@ class Cloner
|
|||||||
|
|
||||||
$copyChapter = $this->chapterRepo->create($chapterDetails, $parent);
|
$copyChapter = $this->chapterRepo->create($chapterDetails, $parent);
|
||||||
|
|
||||||
if (userCan(Permission::PageCreate, $copyChapter)) {
|
if (userCan('page-create', $copyChapter)) {
|
||||||
/** @var Page $page */
|
/** @var Page $page */
|
||||||
foreach ($original->getVisiblePages() as $page) {
|
foreach ($original->getVisiblePages() as $page) {
|
||||||
$this->clonePage($page, $copyChapter, $page->name);
|
$this->clonePage($page, $copyChapter, $page->name);
|
||||||
@@ -62,7 +61,7 @@ class Cloner
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Clone the given book.
|
* Clone the given book.
|
||||||
* Clones all child chapters and pages.
|
* Clones all child chapters & pages.
|
||||||
*/
|
*/
|
||||||
public function cloneBook(Book $original, string $newName): Book
|
public function cloneBook(Book $original, string $newName): Book
|
||||||
{
|
{
|
||||||
@@ -75,11 +74,11 @@ class Cloner
|
|||||||
// Clone contents
|
// Clone contents
|
||||||
$directChildren = $original->getDirectVisibleChildren();
|
$directChildren = $original->getDirectVisibleChildren();
|
||||||
foreach ($directChildren as $child) {
|
foreach ($directChildren as $child) {
|
||||||
if ($child instanceof Chapter && userCan(Permission::ChapterCreate, $copyBook)) {
|
if ($child instanceof Chapter && userCan('chapter-create', $copyBook)) {
|
||||||
$this->cloneChapter($child, $copyBook, $child->name);
|
$this->cloneChapter($child, $copyBook, $child->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($child instanceof Page && !$child->draft && userCan(Permission::PageCreate, $copyBook)) {
|
if ($child instanceof Page && !$child->draft && userCan('page-create', $copyBook)) {
|
||||||
$this->clonePage($child, $copyBook, $child->name);
|
$this->clonePage($child, $copyBook, $child->name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +86,7 @@ class Cloner
|
|||||||
// Clone bookshelf relationships
|
// Clone bookshelf relationships
|
||||||
/** @var Bookshelf $shelf */
|
/** @var Bookshelf $shelf */
|
||||||
foreach ($original->shelves as $shelf) {
|
foreach ($original->shelves as $shelf) {
|
||||||
if (userCan(Permission::BookshelfUpdate, $shelf)) {
|
if (userCan('bookshelf-update', $shelf)) {
|
||||||
$shelf->appendBook($copyBook);
|
$shelf->appendBook($copyBook);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +105,7 @@ class Cloner
|
|||||||
$inputData['tags'] = $this->entityTagsToInputArray($entity);
|
$inputData['tags'] = $this->entityTagsToInputArray($entity);
|
||||||
|
|
||||||
// Add a cover to the data if existing on the original entity
|
// Add a cover to the data if existing on the original entity
|
||||||
if ($entity instanceof CoverImageInterface) {
|
if ($entity instanceof HasCoverImage) {
|
||||||
$cover = $entity->cover()->first();
|
$cover = $entity->cover()->first();
|
||||||
if ($cover) {
|
if ($cover) {
|
||||||
$inputData['image'] = $this->imageToUploadedFile($cover);
|
$inputData['image'] = $this->imageToUploadedFile($cover);
|
||||||
|
|||||||
@@ -13,12 +13,17 @@ use BookStack\Facades\Activity;
|
|||||||
|
|
||||||
class HierarchyTransformer
|
class HierarchyTransformer
|
||||||
{
|
{
|
||||||
public function __construct(
|
protected BookRepo $bookRepo;
|
||||||
protected BookRepo $bookRepo,
|
protected BookshelfRepo $shelfRepo;
|
||||||
protected BookshelfRepo $shelfRepo,
|
protected Cloner $cloner;
|
||||||
protected Cloner $cloner,
|
protected TrashCan $trashCan;
|
||||||
protected TrashCan $trashCan
|
|
||||||
) {
|
public function __construct(BookRepo $bookRepo, BookshelfRepo $shelfRepo, Cloner $cloner, TrashCan $trashCan)
|
||||||
|
{
|
||||||
|
$this->bookRepo = $bookRepo;
|
||||||
|
$this->shelfRepo = $shelfRepo;
|
||||||
|
$this->cloner = $cloner;
|
||||||
|
$this->trashCan = $trashCan;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use BookStack\Entities\Queries\PageQueries;
|
|||||||
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
|
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\Facades\Theme;
|
use BookStack\Facades\Theme;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Theming\ThemeEvents;
|
use BookStack\Theming\ThemeEvents;
|
||||||
use BookStack\Uploads\ImageRepo;
|
use BookStack\Uploads\ImageRepo;
|
||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
@@ -123,7 +122,7 @@ class PageContent
|
|||||||
$imageInfo = $this->parseBase64ImageUri($uri);
|
$imageInfo = $this->parseBase64ImageUri($uri);
|
||||||
|
|
||||||
// Validate user has permission to create images
|
// Validate user has permission to create images
|
||||||
if (!$updater->can(Permission::ImageCreateAll)) {
|
if (!$updater->can('image-create-all')) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ namespace BookStack\Entities\Tools;
|
|||||||
|
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Entities\Models\PageRevision;
|
use BookStack\Entities\Models\PageRevision;
|
||||||
use BookStack\Util\DateFormatter;
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
class PageEditActivity
|
class PageEditActivity
|
||||||
{
|
{
|
||||||
public function __construct(
|
protected Page $page;
|
||||||
protected Page $page
|
|
||||||
) {
|
/**
|
||||||
|
* PageEditActivity constructor.
|
||||||
|
*/
|
||||||
|
public function __construct(Page $page)
|
||||||
|
{
|
||||||
|
$this->page = $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,9 +50,11 @@ class PageEditActivity
|
|||||||
/**
|
/**
|
||||||
* Get any editor clash warning messages to show for the given draft revision.
|
* Get any editor clash warning messages to show for the given draft revision.
|
||||||
*
|
*
|
||||||
|
* @param PageRevision|Page $draft
|
||||||
|
*
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
public function getWarningMessagesForDraft(Page|PageRevision $draft): array
|
public function getWarningMessagesForDraft($draft): array
|
||||||
{
|
{
|
||||||
$warnings = [];
|
$warnings = [];
|
||||||
|
|
||||||
@@ -76,8 +82,7 @@ class PageEditActivity
|
|||||||
*/
|
*/
|
||||||
public function getEditingActiveDraftMessage(PageRevision $draft): string
|
public function getEditingActiveDraftMessage(PageRevision $draft): string
|
||||||
{
|
{
|
||||||
$formatter = resolve(DateFormatter::class);
|
$message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
|
||||||
$message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $formatter->relative($draft->updated_at)]);
|
|
||||||
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
|
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
|
||||||
return $message;
|
return $message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use BookStack\Entities\Models\Page;
|
|||||||
use BookStack\Entities\Queries\EntityQueries;
|
use BookStack\Entities\Queries\EntityQueries;
|
||||||
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
|
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
|
||||||
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
|
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
|
|
||||||
class PageEditorData
|
class PageEditorData
|
||||||
{
|
{
|
||||||
@@ -99,9 +98,9 @@ class PageEditorData
|
|||||||
{
|
{
|
||||||
$editorType = PageEditorType::forPage($page) ?: PageEditorType::getSystemDefault();
|
$editorType = PageEditorType::forPage($page) ?: PageEditorType::getSystemDefault();
|
||||||
|
|
||||||
// Use the requested editor if valid and if we have permission
|
// Use requested editor if valid and if we have permission
|
||||||
$requestedType = PageEditorType::fromRequestValue($this->requestedEditor);
|
$requestedType = PageEditorType::fromRequestValue($this->requestedEditor);
|
||||||
if ($requestedType && userCan(Permission::EditorChange)) {
|
if ($requestedType && userCan('editor-change')) {
|
||||||
$editorType = $requestedType;
|
$editorType = $requestedType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ use Closure;
|
|||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
use DOMElement;
|
use DOMElement;
|
||||||
use DOMNode;
|
use DOMNode;
|
||||||
|
use DOMText;
|
||||||
|
|
||||||
class PageIncludeParser
|
class PageIncludeParser
|
||||||
{
|
{
|
||||||
protected static string $includeTagRegex = "/{{@\s?([0-9].*?)}}/";
|
protected static string $includeTagRegex = "/{{@\s?([0-9].*?)}}/";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nodes to clean up and remove if left empty after a parsing operation.
|
* Elements to clean up and remove if left empty after a parsing operation.
|
||||||
* @var DOMNode[]
|
* @var DOMElement[]
|
||||||
*/
|
*/
|
||||||
protected array $toCleanup = [];
|
protected array $toCleanup = [];
|
||||||
|
|
||||||
@@ -158,7 +159,7 @@ class PageIncludeParser
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits the given $parentNode at the location of the $domNode within it.
|
* Splits the given $parentNode at the location of the $domNode within it.
|
||||||
* Attempts to replicate the original $parentNode, moving some of their parent
|
* Attempts replicate the original $parentNode, moving some of their parent
|
||||||
* children in where needed, before adding the $domNode between.
|
* children in where needed, before adding the $domNode between.
|
||||||
*/
|
*/
|
||||||
protected function splitNodeAtChildNode(DOMElement $parentNode, DOMNode $domNode): void
|
protected function splitNodeAtChildNode(DOMElement $parentNode, DOMNode $domNode): void
|
||||||
@@ -170,10 +171,6 @@ class PageIncludeParser
|
|||||||
}
|
}
|
||||||
|
|
||||||
$parentClone = $parentNode->cloneNode();
|
$parentClone = $parentNode->cloneNode();
|
||||||
if (!($parentClone instanceof DOMElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$parentNode->parentNode->insertBefore($parentClone, $parentNode);
|
$parentNode->parentNode->insertBefore($parentClone, $parentNode);
|
||||||
$parentClone->removeAttribute('id');
|
$parentClone->removeAttribute('id');
|
||||||
|
|
||||||
@@ -206,7 +203,7 @@ class PageIncludeParser
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up after a parse operation.
|
* Cleanup after a parse operation.
|
||||||
* Removes stranded elements we may have left during the parse.
|
* Removes stranded elements we may have left during the parse.
|
||||||
*/
|
*/
|
||||||
protected function cleanup(): void
|
protected function cleanup(): void
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use BookStack\Entities\Models\Bookshelf;
|
|||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Permissions\Models\EntityPermission;
|
use BookStack\Permissions\Models\EntityPermission;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Users\Models\Role;
|
use BookStack\Users\Models\Role;
|
||||||
use BookStack\Users\Models\User;
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -94,9 +93,8 @@ class PermissionsUpdater
|
|||||||
|
|
||||||
foreach ($permissions as $roleId => $info) {
|
foreach ($permissions as $roleId => $info) {
|
||||||
$entityPermissionData = ['role_id' => $roleId];
|
$entityPermissionData = ['role_id' => $roleId];
|
||||||
foreach (Permission::genericForEntity() as $permission) {
|
foreach (EntityPermission::PERMISSIONS as $permission) {
|
||||||
$permName = $permission->value;
|
$entityPermissionData[$permission] = (($info[$permission] ?? false) === "true");
|
||||||
$entityPermissionData[$permName] = (($info[$permName] ?? false) === "true");
|
|
||||||
}
|
}
|
||||||
$formatted[] = $entityPermissionData;
|
$formatted[] = $entityPermissionData;
|
||||||
}
|
}
|
||||||
@@ -110,9 +108,8 @@ class PermissionsUpdater
|
|||||||
|
|
||||||
foreach ($permissions as $requestPermissionData) {
|
foreach ($permissions as $requestPermissionData) {
|
||||||
$entityPermissionData = ['role_id' => $requestPermissionData['role_id']];
|
$entityPermissionData = ['role_id' => $requestPermissionData['role_id']];
|
||||||
foreach (Permission::genericForEntity() as $permission) {
|
foreach (EntityPermission::PERMISSIONS as $permission) {
|
||||||
$permName = $permission->value;
|
$entityPermissionData[$permission] = boolval($requestPermissionData[$permission] ?? false);
|
||||||
$entityPermissionData[$permName] = boolval($requestPermissionData[$permName] ?? false);
|
|
||||||
}
|
}
|
||||||
$formatted[] = $entityPermissionData;
|
$formatted[] = $entityPermissionData;
|
||||||
}
|
}
|
||||||
@@ -150,7 +147,7 @@ class PermissionsUpdater
|
|||||||
|
|
||||||
/** @var Book $book */
|
/** @var Book $book */
|
||||||
foreach ($shelfBooks as $book) {
|
foreach ($shelfBooks as $book) {
|
||||||
if ($checkUserPermissions && !userCan(Permission::RestrictionsManage, $book)) {
|
if ($checkUserPermissions && !userCan('restrictions-manage', $book)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$book->permissions()->delete();
|
$book->permissions()->delete();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace BookStack\Entities\Tools;
|
namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
use BookStack\App\Model;
|
use BookStack\App\Model;
|
||||||
use BookStack\App\SluggableInterface;
|
use BookStack\App\Sluggable;
|
||||||
use BookStack\Entities\Models\BookChild;
|
use BookStack\Entities\Models\BookChild;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
@@ -13,9 +13,9 @@ class SlugGenerator
|
|||||||
* Generate a fresh slug for the given entity.
|
* Generate a fresh slug for the given entity.
|
||||||
* The slug will be generated so that it doesn't conflict within the same parent item.
|
* The slug will be generated so that it doesn't conflict within the same parent item.
|
||||||
*/
|
*/
|
||||||
public function generate(SluggableInterface&Model $model, string $slugSource): string
|
public function generate(Sluggable $model): string
|
||||||
{
|
{
|
||||||
$slug = $this->formatNameAsSlug($slugSource);
|
$slug = $this->formatNameAsSlug($model->name);
|
||||||
while ($this->slugInUse($slug, $model)) {
|
while ($this->slugInUse($slug, $model)) {
|
||||||
$slug .= '-' . Str::random(3);
|
$slug .= '-' . Str::random(3);
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ class SlugGenerator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a name as a URL slug.
|
* Format a name as a url slug.
|
||||||
*/
|
*/
|
||||||
protected function formatNameAsSlug(string $name): string
|
protected function formatNameAsSlug(string $name): string
|
||||||
{
|
{
|
||||||
@@ -39,8 +39,10 @@ class SlugGenerator
|
|||||||
/**
|
/**
|
||||||
* Check if a slug is already in-use for this
|
* Check if a slug is already in-use for this
|
||||||
* type of model within the same parent.
|
* type of model within the same parent.
|
||||||
|
*
|
||||||
|
* @param Sluggable&Model $model
|
||||||
*/
|
*/
|
||||||
protected function slugInUse(string $slug, SluggableInterface&Model $model): bool
|
protected function slugInUse(string $slug, Sluggable $model): bool
|
||||||
{
|
{
|
||||||
$query = $model->newQuery()->where('slug', '=', $slug);
|
$query = $model->newQuery()->where('slug', '=', $slug);
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ use BookStack\Entities\Models\Bookshelf;
|
|||||||
use BookStack\Entities\Models\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Models\Deletion;
|
use BookStack\Entities\Models\Deletion;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Models\CoverImageInterface;
|
use BookStack\Entities\Models\HasCoverImage;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Entities\Queries\EntityQueries;
|
use BookStack\Entities\Queries\EntityQueries;
|
||||||
use BookStack\Exceptions\NotifyException;
|
use BookStack\Exceptions\NotifyException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Uploads\AttachmentService;
|
use BookStack\Uploads\AttachmentService;
|
||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
use BookStack\Util\DatabaseTransaction;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
@@ -358,26 +357,25 @@ class TrashCan
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy the given entity.
|
* Destroy the given entity.
|
||||||
* Returns the number of total entities destroyed in the operation.
|
|
||||||
*
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function destroyEntity(Entity $entity): int
|
public function destroyEntity(Entity $entity): int
|
||||||
{
|
{
|
||||||
$result = (new DatabaseTransaction(function () use ($entity) {
|
if ($entity instanceof Page) {
|
||||||
if ($entity instanceof Page) {
|
return $this->destroyPage($entity);
|
||||||
return $this->destroyPage($entity);
|
}
|
||||||
} else if ($entity instanceof Chapter) {
|
if ($entity instanceof Chapter) {
|
||||||
return $this->destroyChapter($entity);
|
return $this->destroyChapter($entity);
|
||||||
} else if ($entity instanceof Book) {
|
}
|
||||||
return $this->destroyBook($entity);
|
if ($entity instanceof Book) {
|
||||||
} else if ($entity instanceof Bookshelf) {
|
return $this->destroyBook($entity);
|
||||||
return $this->destroyShelf($entity);
|
}
|
||||||
}
|
if ($entity instanceof Bookshelf) {
|
||||||
return null;
|
return $this->destroyShelf($entity);
|
||||||
}))->run();
|
}
|
||||||
|
|
||||||
return $result ?? 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -398,7 +396,7 @@ class TrashCan
|
|||||||
$entity->referencesTo()->delete();
|
$entity->referencesTo()->delete();
|
||||||
$entity->referencesFrom()->delete();
|
$entity->referencesFrom()->delete();
|
||||||
|
|
||||||
if ($entity instanceof CoverImageInterface && $entity->cover()->exists()) {
|
if ($entity instanceof HasCoverImage && $entity->cover()->exists()) {
|
||||||
$imageService = app()->make(ImageService::class);
|
$imageService = app()->make(ImageService::class);
|
||||||
$imageService->destroy($entity->cover()->first());
|
$imageService->destroy($entity->cover()->first());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace BookStack\Exceptions;
|
namespace BookStack\Exceptions;
|
||||||
|
|
||||||
use BookStack\App\AppVersion;
|
|
||||||
use Illuminate\Contracts\Foundation\ExceptionRenderer;
|
use Illuminate\Contracts\Foundation\ExceptionRenderer;
|
||||||
|
|
||||||
class BookStackExceptionHandlerPage implements ExceptionRenderer
|
class BookStackExceptionHandlerPage implements ExceptionRenderer
|
||||||
@@ -31,7 +30,9 @@ class BookStackExceptionHandlerPage implements ExceptionRenderer
|
|||||||
return [
|
return [
|
||||||
'PHP Version' => phpversion(),
|
'PHP Version' => phpversion(),
|
||||||
'BookStack Version' => $this->safeReturn(function () {
|
'BookStack Version' => $this->safeReturn(function () {
|
||||||
return AppVersion::get();
|
$versionFile = base_path('version');
|
||||||
|
|
||||||
|
return trim(file_get_contents($versionFile));
|
||||||
}, 'unknown'),
|
}, 'unknown'),
|
||||||
'Theme Configured' => $this->safeReturn(function () {
|
'Theme Configured' => $this->safeReturn(function () {
|
||||||
return config('view.theme');
|
return config('view.theme');
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Exceptions;
|
namespace BookStack\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
use Illuminate\Auth\AuthenticationException;
|
use Illuminate\Auth\AuthenticationException;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
@@ -11,7 +12,6 @@ use Illuminate\Http\Request;
|
|||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Symfony\Component\ErrorHandler\Error\FatalError;
|
use Symfony\Component\ErrorHandler\Error\FatalError;
|
||||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class Handler extends ExceptionHandler
|
|||||||
/**
|
/**
|
||||||
* A list of the exception types that are not reported.
|
* A list of the exception types that are not reported.
|
||||||
*
|
*
|
||||||
* @var array<int, class-string<Throwable>>
|
* @var array<int, class-string<\Throwable>>
|
||||||
*/
|
*/
|
||||||
protected $dontReport = [
|
protected $dontReport = [
|
||||||
NotFoundException::class,
|
NotFoundException::class,
|
||||||
@@ -50,11 +50,11 @@ class Handler extends ExceptionHandler
|
|||||||
/**
|
/**
|
||||||
* Report or log an exception.
|
* Report or log an exception.
|
||||||
*
|
*
|
||||||
* @param Throwable $exception
|
* @param \Throwable $exception
|
||||||
|
*
|
||||||
|
* @throws \Throwable
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*@throws Throwable
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public function report(Throwable $exception)
|
public function report(Throwable $exception)
|
||||||
{
|
{
|
||||||
@@ -64,9 +64,12 @@ class Handler extends ExceptionHandler
|
|||||||
/**
|
/**
|
||||||
* Render an exception into an HTTP response.
|
* Render an exception into an HTTP response.
|
||||||
*
|
*
|
||||||
* @param Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param Exception $e
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
*/
|
*/
|
||||||
public function render($request, Throwable $e): SymfonyResponse
|
public function render($request, Throwable $e)
|
||||||
{
|
{
|
||||||
if ($e instanceof FatalError && str_contains($e->getMessage(), 'bytes exhausted (tried to allocate') && $this->onOutOfMemory) {
|
if ($e instanceof FatalError && str_contains($e->getMessage(), 'bytes exhausted (tried to allocate') && $this->onOutOfMemory) {
|
||||||
$response = call_user_func($this->onOutOfMemory);
|
$response = call_user_func($this->onOutOfMemory);
|
||||||
@@ -91,7 +94,7 @@ class Handler extends ExceptionHandler
|
|||||||
* If the callable returns a response, this response will be returned
|
* If the callable returns a response, this response will be returned
|
||||||
* to the request upon error.
|
* to the request upon error.
|
||||||
*/
|
*/
|
||||||
public function prepareForOutOfMemory(callable $onOutOfMemory): void
|
public function prepareForOutOfMemory(callable $onOutOfMemory)
|
||||||
{
|
{
|
||||||
$this->onOutOfMemory = $onOutOfMemory;
|
$this->onOutOfMemory = $onOutOfMemory;
|
||||||
}
|
}
|
||||||
@@ -99,7 +102,7 @@ class Handler extends ExceptionHandler
|
|||||||
/**
|
/**
|
||||||
* Forget the current out of memory handler, if existing.
|
* Forget the current out of memory handler, if existing.
|
||||||
*/
|
*/
|
||||||
public function forgetOutOfMemoryHandler(): void
|
public function forgetOutOfMemoryHandler()
|
||||||
{
|
{
|
||||||
$this->onOutOfMemory = null;
|
$this->onOutOfMemory = null;
|
||||||
}
|
}
|
||||||
@@ -149,9 +152,12 @@ class Handler extends ExceptionHandler
|
|||||||
/**
|
/**
|
||||||
* Convert an authentication exception into an unauthenticated response.
|
* Convert an authentication exception into an unauthenticated response.
|
||||||
*
|
*
|
||||||
* @param Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Illuminate\Auth\AuthenticationException $exception
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
*/
|
*/
|
||||||
protected function unauthenticated($request, AuthenticationException $exception): SymfonyResponse
|
protected function unauthenticated($request, AuthenticationException $exception)
|
||||||
{
|
{
|
||||||
if ($request->expectsJson()) {
|
if ($request->expectsJson()) {
|
||||||
return response()->json(['error' => 'Unauthenticated.'], 401);
|
return response()->json(['error' => 'Unauthenticated.'], 401);
|
||||||
@@ -163,9 +169,12 @@ class Handler extends ExceptionHandler
|
|||||||
/**
|
/**
|
||||||
* Convert a validation exception into a JSON response.
|
* Convert a validation exception into a JSON response.
|
||||||
*
|
*
|
||||||
* @param Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Illuminate\Validation\ValidationException $exception
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*/
|
*/
|
||||||
protected function invalidJson($request, ValidationException $exception): JsonResponse
|
protected function invalidJson($request, ValidationException $exception)
|
||||||
{
|
{
|
||||||
return response()->json($exception->errors(), $exception->status);
|
return response()->json($exception->errors(), $exception->status);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ namespace BookStack\Exports\Controllers;
|
|||||||
|
|
||||||
use BookStack\Entities\Queries\BookQueries;
|
use BookStack\Entities\Queries\BookQueries;
|
||||||
use BookStack\Exports\ExportFormatter;
|
use BookStack\Exports\ExportFormatter;
|
||||||
use BookStack\Exports\ZipExports\ZipExportBuilder;
|
|
||||||
use BookStack\Http\ApiController;
|
use BookStack\Http\ApiController;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class BookExportApiController extends ApiController
|
class BookExportApiController extends ApiController
|
||||||
@@ -15,7 +13,7 @@ class BookExportApiController extends ApiController
|
|||||||
protected ExportFormatter $exportFormatter,
|
protected ExportFormatter $exportFormatter,
|
||||||
protected BookQueries $queries,
|
protected BookQueries $queries,
|
||||||
) {
|
) {
|
||||||
$this->middleware(Permission::ContentExport->middleware());
|
$this->middleware('can:content-export');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,15 +63,4 @@ class BookExportApiController extends ApiController
|
|||||||
|
|
||||||
return $this->download()->directly($markdown, $book->slug . '.md');
|
return $this->download()->directly($markdown, $book->slug . '.md');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Export a book as a contained ZIP export file.
|
|
||||||
*/
|
|
||||||
public function exportZip(int $id, ZipExportBuilder $builder)
|
|
||||||
{
|
|
||||||
$book = $this->queries->findVisibleByIdOrFail($id);
|
|
||||||
$zip = $builder->buildForBook($book);
|
|
||||||
|
|
||||||
return $this->download()->streamedFileDirectly($zip, $book->slug . '.zip', true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use BookStack\Exceptions\NotFoundException;
|
|||||||
use BookStack\Exports\ExportFormatter;
|
use BookStack\Exports\ExportFormatter;
|
||||||
use BookStack\Exports\ZipExports\ZipExportBuilder;
|
use BookStack\Exports\ZipExports\ZipExportBuilder;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class BookExportController extends Controller
|
class BookExportController extends Controller
|
||||||
@@ -16,7 +15,7 @@ class BookExportController extends Controller
|
|||||||
protected BookQueries $queries,
|
protected BookQueries $queries,
|
||||||
protected ExportFormatter $exportFormatter,
|
protected ExportFormatter $exportFormatter,
|
||||||
) {
|
) {
|
||||||
$this->middleware(Permission::ContentExport->middleware());
|
$this->middleware('can:content-export');
|
||||||
$this->middleware('throttle:exports');
|
$this->middleware('throttle:exports');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ namespace BookStack\Exports\Controllers;
|
|||||||
|
|
||||||
use BookStack\Entities\Queries\ChapterQueries;
|
use BookStack\Entities\Queries\ChapterQueries;
|
||||||
use BookStack\Exports\ExportFormatter;
|
use BookStack\Exports\ExportFormatter;
|
||||||
use BookStack\Exports\ZipExports\ZipExportBuilder;
|
|
||||||
use BookStack\Http\ApiController;
|
use BookStack\Http\ApiController;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class ChapterExportApiController extends ApiController
|
class ChapterExportApiController extends ApiController
|
||||||
@@ -15,7 +13,7 @@ class ChapterExportApiController extends ApiController
|
|||||||
protected ExportFormatter $exportFormatter,
|
protected ExportFormatter $exportFormatter,
|
||||||
protected ChapterQueries $queries,
|
protected ChapterQueries $queries,
|
||||||
) {
|
) {
|
||||||
$this->middleware(Permission::ContentExport->middleware());
|
$this->middleware('can:content-export');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,15 +63,4 @@ class ChapterExportApiController extends ApiController
|
|||||||
|
|
||||||
return $this->download()->directly($markdown, $chapter->slug . '.md');
|
return $this->download()->directly($markdown, $chapter->slug . '.md');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Export a chapter as a contained ZIP file.
|
|
||||||
*/
|
|
||||||
public function exportZip(int $id, ZipExportBuilder $builder)
|
|
||||||
{
|
|
||||||
$chapter = $this->queries->findVisibleByIdOrFail($id);
|
|
||||||
$zip = $builder->buildForChapter($chapter);
|
|
||||||
|
|
||||||
return $this->download()->streamedFileDirectly($zip, $chapter->slug . '.zip', true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use BookStack\Exceptions\NotFoundException;
|
|||||||
use BookStack\Exports\ExportFormatter;
|
use BookStack\Exports\ExportFormatter;
|
||||||
use BookStack\Exports\ZipExports\ZipExportBuilder;
|
use BookStack\Exports\ZipExports\ZipExportBuilder;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class ChapterExportController extends Controller
|
class ChapterExportController extends Controller
|
||||||
@@ -16,7 +15,7 @@ class ChapterExportController extends Controller
|
|||||||
protected ChapterQueries $queries,
|
protected ChapterQueries $queries,
|
||||||
protected ExportFormatter $exportFormatter,
|
protected ExportFormatter $exportFormatter,
|
||||||
) {
|
) {
|
||||||
$this->middleware(Permission::ContentExport->middleware());
|
$this->middleware('can:content-export');
|
||||||
$this->middleware('throttle:exports');
|
$this->middleware('throttle:exports');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace BookStack\Exports\Controllers;
|
|
||||||
|
|
||||||
use BookStack\Exceptions\ZipImportException;
|
|
||||||
use BookStack\Exceptions\ZipValidationException;
|
|
||||||
use BookStack\Exports\ImportRepo;
|
|
||||||
use BookStack\Http\ApiController;
|
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Uploads\AttachmentService;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
|
|
||||||
class ImportApiController extends ApiController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected ImportRepo $imports,
|
|
||||||
) {
|
|
||||||
$this->middleware(Permission::ContentImport->middleware());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List existing ZIP imports visible to the user.
|
|
||||||
* Requires permission to import content.
|
|
||||||
*/
|
|
||||||
public function list(): JsonResponse
|
|
||||||
{
|
|
||||||
$query = $this->imports->queryVisible();
|
|
||||||
|
|
||||||
return $this->apiListingResponse($query, [
|
|
||||||
'id', 'name', 'size', 'type', 'created_by', 'created_at', 'updated_at'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a new import from a ZIP file.
|
|
||||||
* This does not actually run the import since that is performed via the "run" endpoint.
|
|
||||||
* This uploads, validates and stores the ZIP file so it's ready to be imported.
|
|
||||||
*
|
|
||||||
* This "file" parameter must be a BookStack-compatible ZIP file, and this must be
|
|
||||||
* sent via a 'multipart/form-data' type request.
|
|
||||||
*
|
|
||||||
* Requires permission to import content.
|
|
||||||
*/
|
|
||||||
public function create(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$this->validate($request, $this->rules()['create']);
|
|
||||||
|
|
||||||
$file = $request->file('file');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$import = $this->imports->storeFromUpload($file);
|
|
||||||
} catch (ZipValidationException $exception) {
|
|
||||||
$message = "ZIP upload failed with the following validation errors: \n" . $this->formatErrors($exception->errors);
|
|
||||||
return $this->jsonError($message, 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json($import);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read details of a pending ZIP import.
|
|
||||||
* The "details" property contains high-level metadata regarding the ZIP import content,
|
|
||||||
* and the structure of this will change depending on import "type".
|
|
||||||
* Requires permission to import content.
|
|
||||||
*/
|
|
||||||
public function read(int $id): JsonResponse
|
|
||||||
{
|
|
||||||
$import = $this->imports->findVisible($id);
|
|
||||||
|
|
||||||
$import->setAttribute('details', $import->decodeMetadata());
|
|
||||||
|
|
||||||
return response()->json($import);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the import process for an uploaded ZIP import.
|
|
||||||
* The "parent_id" and "parent_type" parameters are required when the import type is "chapter" or "page".
|
|
||||||
* On success, this endpoint returns the imported item.
|
|
||||||
* Requires permission to import content.
|
|
||||||
*/
|
|
||||||
public function run(int $id, Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$import = $this->imports->findVisible($id);
|
|
||||||
$parent = null;
|
|
||||||
$rules = $this->rules()['run'];
|
|
||||||
|
|
||||||
if ($import->type === 'page' || $import->type === 'chapter') {
|
|
||||||
$rules['parent_type'][] = 'required';
|
|
||||||
$rules['parent_id'][] = 'required';
|
|
||||||
$data = $this->validate($request, $rules);
|
|
||||||
$parent = "{$data['parent_type']}:{$data['parent_id']}";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$entity = $this->imports->runImport($import, $parent);
|
|
||||||
} catch (ZipImportException $exception) {
|
|
||||||
$message = "ZIP import failed with the following errors: \n" . $this->formatErrors($exception->errors);
|
|
||||||
return $this->jsonError($message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json($entity->withoutRelations());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a pending ZIP import from the system.
|
|
||||||
* Requires permission to import content.
|
|
||||||
*/
|
|
||||||
public function delete(int $id): Response
|
|
||||||
{
|
|
||||||
$import = $this->imports->findVisible($id);
|
|
||||||
$this->imports->deleteImport($import);
|
|
||||||
|
|
||||||
return response('', 204);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'create' => [
|
|
||||||
'file' => ['required', ...AttachmentService::getFileValidationRules()],
|
|
||||||
],
|
|
||||||
'run' => [
|
|
||||||
'parent_type' => ['string', 'in:book,chapter'],
|
|
||||||
'parent_id' => ['int'],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function formatErrors(array $errors): string
|
|
||||||
{
|
|
||||||
$parts = [];
|
|
||||||
foreach ($errors as $key => $error) {
|
|
||||||
if (is_string($key)) {
|
|
||||||
$parts[] = "[{$key}] {$error}";
|
|
||||||
} else {
|
|
||||||
$parts[] = $error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return implode("\n", $parts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ use BookStack\Exceptions\ZipImportException;
|
|||||||
use BookStack\Exceptions\ZipValidationException;
|
use BookStack\Exceptions\ZipValidationException;
|
||||||
use BookStack\Exports\ImportRepo;
|
use BookStack\Exports\ImportRepo;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\Permission;
|
|
||||||
use BookStack\Uploads\AttachmentService;
|
use BookStack\Uploads\AttachmentService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@ class ImportController extends Controller
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
protected ImportRepo $imports,
|
protected ImportRepo $imports,
|
||||||
) {
|
) {
|
||||||
$this->middleware(Permission::ContentImport->middleware());
|
$this->middleware('can:content-import');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,7 +89,7 @@ class ImportController extends Controller
|
|||||||
try {
|
try {
|
||||||
$entity = $this->imports->runImport($import, $parent);
|
$entity = $this->imports->runImport($import, $parent);
|
||||||
} catch (ZipImportException $exception) {
|
} catch (ZipImportException $exception) {
|
||||||
session()->forget(['success', 'warning']);
|
session()->flush();
|
||||||
$this->showErrorNotification(trans('errors.import_zip_failed_notification'));
|
$this->showErrorNotification(trans('errors.import_zip_failed_notification'));
|
||||||
return redirect($import->getUrl())->with('import_errors', $exception->errors);
|
return redirect($import->getUrl())->with('import_errors', $exception->errors);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user