Compare commits

..

395 Commits

Author SHA1 Message Date
Dan Brown
f3efb6441d Updated version and assets for release v24.10.1 2024-11-08 13:53:06 +00:00
Dan Brown
0cf313a21e Merge branch 'development' into release 2024-11-08 13:52:37 +00:00
Dan Brown
26aadffb20 Updated version and assets for release v24.10 2024-10-09 10:48:34 +01:00
Dan Brown
a5f48e3202 Merge branch 'development' into release 2024-10-09 10:46:07 +01:00
Dan Brown
b0dda6e6a7 Updated version and assets for release v24.05.4 2024-08-29 16:04:51 +01:00
Dan Brown
d4025d95e7 Merge branch 'development' into release 2024-08-29 16:04:37 +01:00
Dan Brown
d6021f4d22 Updated version and assets for release v24.05.3 2024-07-14 17:14:21 +01:00
Dan Brown
b9a3290731 Merge branch 'development' into release 2024-07-14 17:13:10 +01:00
Dan Brown
48f235ea5a Updated version and assets for release v24.05.2 2024-06-10 11:44:06 +01:00
Dan Brown
047771b9f4 Merge branch 'development' into release 2024-06-10 11:43:05 +01:00
Dan Brown
b5375114d3 Updated version and assets for release v24.05.1 2024-05-21 11:07:36 +01:00
Dan Brown
fc13e56cea Merge branch 'development' into release 2024-05-21 11:07:10 +01:00
Dan Brown
77fc37ac25 Updated version and assets for release v24.05 2024-05-11 15:49:29 +01:00
Dan Brown
3424351e84 Merge branch 'development' into release 2024-05-11 15:48:49 +01:00
Dan Brown
606f9d92d0 Updated version and assets for release v24.02.3 2024-04-05 15:20:08 +01:00
Dan Brown
a5e25abb9c Merge branch 'v24-02' into release 2024-04-05 15:19:34 +01:00
Dan Brown
b310e87e4c Updated version and assets for release v24.02.2 2024-03-11 14:30:48 +00:00
Dan Brown
425baf9d6e Merge branch 'development' into release 2024-03-10 18:46:05 +00:00
Dan Brown
825c369ad9 Updated version and assets for release v24.02 2024-02-28 13:35:36 +00:00
Dan Brown
10bab70438 Merge branch 'development' into release 2024-02-28 13:35:23 +00:00
Dan Brown
350e0b281b Updated version and assets for release v23.12.3 2024-02-26 12:05:02 +00:00
Dan Brown
08805ea3c8 Merge branch 'v23-12' into release 2024-02-26 12:04:25 +00:00
Dan Brown
9441e32c69 Updated version and assets for release v23.12.2 2024-01-24 10:37:20 +00:00
Dan Brown
530fc37067 Merge branch 'v23-12' into release 2024-01-24 10:36:52 +00:00
Dan Brown
369e499dce Updated version and assets for release v23.12.1 2024-01-16 12:16:06 +00:00
Dan Brown
655815de6d Merge branch 'development' into release 2024-01-16 12:15:50 +00:00
Dan Brown
457adc1fee Updated version and assets for release v23.12 2023-12-29 12:16:07 +00:00
Dan Brown
e86a90967e Merge branch 'development' into release 2023-12-29 12:15:34 +00:00
Dan Brown
5d08f7cf14 Updated version and assets for release v23.10.4 2023-11-20 14:19:46 +00:00
Dan Brown
8744eb2d62 Merge branch 'v23-10' into release 2023-11-20 14:02:23 +00:00
Dan Brown
d8383cfa80 Updated version and assets for release v23.10.2 2023-11-07 15:22:34 +00:00
Dan Brown
4626278447 Merge branch 'development' into release 2023-11-07 15:22:11 +00:00
Dan Brown
c61af9c22b Updated version and assets for release v23.10.1 2023-11-02 14:44:53 +00:00
Dan Brown
72521d0906 Merge branch 'development' into release 2023-11-02 14:35:49 +00:00
Dan Brown
7e44b195c5 Updated version and assets for release v23.10 2023-10-30 12:15:59 +00:00
Dan Brown
5b45eac5e1 Merge branch 'development' into release 2023-10-30 12:14:23 +00:00
Dan Brown
c1d30341e7 Updated version and assets for release v23.08.3 2023-09-15 13:49:40 +01:00
Dan Brown
80d2b4913b Merge branch 'v23-08' into release 2023-09-15 13:49:12 +01:00
Dan Brown
3f473528b1 Updated version and assets for release v23.08.2 2023-09-04 12:06:50 +01:00
Dan Brown
d0dcd4f61b Merge branch 'development' into release 2023-09-04 12:06:15 +01:00
Dan Brown
bde66a1396 Updated version and assets for release v23.08.1 2023-09-03 17:40:19 +01:00
Dan Brown
4de5a2d9bf Merge branch 'development' into release 2023-09-03 17:39:56 +01:00
Dan Brown
27bf4299cf Updated version and assets for release v23.08 2023-08-30 12:38:48 +01:00
Dan Brown
164f01bb25 Merge branch 'development' into release 2023-08-30 12:38:22 +01:00
Dan Brown
f563a005f5 Updated version and assets for release v23.06.2 2023-07-12 22:34:25 +01:00
Dan Brown
a14d8e30cc Merge branch 'development' into release 2023-07-12 22:34:15 +01:00
Dan Brown
a9194ffb63 Updated version and assets for release v23.06.1 2023-07-05 13:04:51 +01:00
Dan Brown
2f9c1b7127 Merge branch 'development' into release 2023-07-05 13:04:30 +01:00
Dan Brown
bbea76668b Updated version and assets for release v23.06 2023-06-30 11:06:19 +01:00
Dan Brown
becc630acf Merge branch 'development' into release 2023-06-30 11:05:57 +01:00
Dan Brown
4ac8ecad6b Updated version and assets for release v23.05.2 2023-05-23 12:36:46 +01:00
Dan Brown
903e88c700 Merge branch 'development' into release 2023-05-23 12:36:29 +01:00
Dan Brown
ed96aa820e Updated version and assets for release v23.05.1 2023-05-08 16:05:50 +01:00
Dan Brown
63ec079b7b Merge branch 'development' into release 2023-05-08 16:04:51 +01:00
Dan Brown
d485fcb3db Updated version and assets for release v23.05 2023-05-03 11:05:33 +01:00
Dan Brown
0f895668a4 Merge branch 'development' into release 2023-05-03 11:03:29 +01:00
Dan Brown
6c577ac3bf Updated version and assets for release v23.02.3 2023-04-07 18:07:32 +01:00
Dan Brown
31cc2423d2 Merge branch 'v23.02-branch' into release 2023-04-07 18:07:09 +01:00
Dan Brown
c9ed32e518 Updated version and assets for release v23.02.2 2023-03-25 12:27:32 +00:00
Dan Brown
6b4c3a0969 Merge branch 'v23.02-branch' into release 2023-03-25 12:27:05 +00:00
Dan Brown
2dad92d1bd Updated version and assets for release v23.02.1 2023-02-27 19:26:13 +00:00
Dan Brown
c1fb7ab7dc Merge branch 'development' into release 2023-02-27 19:23:33 +00:00
Dan Brown
98315f3899 Updated version and assets for release v23.02 2023-02-26 11:03:49 +00:00
Dan Brown
8c82aaabd6 Merge branch 'development' into release 2023-02-26 11:02:56 +00:00
Dan Brown
ce9b536b78 Updated version and assets for release v23.01.1 2023-02-02 12:29:26 +00:00
Dan Brown
d9c50e5bc1 Merge branch 'development' into release 2023-02-02 12:29:07 +00:00
Dan Brown
bf075f7dd8 Updated version and assets for release v23.01 2023-01-31 11:59:51 +00:00
Dan Brown
a4fd673285 Merge branch 'development' into release 2023-01-31 11:59:28 +00:00
Dan Brown
e794c977bc Updated version and assets for release v22.11.1 2022-12-16 23:49:14 +00:00
Dan Brown
0b088ef1d3 Merge branch 'development' into release 2022-12-16 23:48:35 +00:00
Dan Brown
bf6a6af683 Updated version and assets for release v22.11 2022-11-30 12:30:21 +00:00
Dan Brown
914790fd99 Merge branch 'development' into release 2022-11-30 12:29:52 +00:00
Dan Brown
edb0c6a9e8 Updated version and assets for release v22.10.2 2022-11-02 15:22:13 +00:00
Dan Brown
84049de696 Merge branch 'v22-10' into release 2022-11-02 15:19:33 +00:00
Dan Brown
da0531e63b Updated version and assets for release v22.10.1 2022-10-21 21:52:32 +01:00
Dan Brown
421dc75f4e Merge branch 'development' into release 2022-10-21 21:52:16 +01:00
Dan Brown
8ae91df038 Updated version and assets for release v22.10 2022-10-21 11:16:45 +01:00
Dan Brown
64b41dd626 Merge branch 'development' into release 2022-10-21 11:16:25 +01:00
Dan Brown
ebd6e4d3a2 Updated version and assets for release v22.09.1 2022-09-20 13:19:34 +01:00
Dan Brown
80374aea5c Merge branch 'development' into release 2022-09-20 13:19:03 +01:00
Dan Brown
2ac9efae7d Updated version and assets for release v22.09 2022-09-08 12:41:09 +01:00
Dan Brown
a11d565ba4 Merge branch 'development' into release 2022-09-08 12:40:57 +01:00
Dan Brown
1fdf854ea7 Updated version and assets for release v22.07.3 2022-08-11 15:17:06 +01:00
Dan Brown
e9c9792cb9 Merge branch 'development' into release 2022-08-11 15:16:34 +01:00
Dan Brown
5ae524c25a Updated version and assets for release v22.07.2 2022-08-09 13:55:52 +01:00
Dan Brown
0d7287fc8b Merge branch 'development' into release 2022-08-09 13:55:40 +01:00
Dan Brown
e77c96f6b7 Updated version and assets for release v22.07.1 2022-08-02 11:47:25 +01:00
Dan Brown
9b8a10dd3a Merge branch 'development' into release 2022-08-02 11:47:08 +01:00
Dan Brown
49200ca5ce Updated version and assets for release v22.07 2022-07-28 14:53:15 +01:00
Dan Brown
34aa4dbf10 Merge branch 'development' into release 2022-07-28 14:53:01 +01:00
Dan Brown
5ee79d16c9 Updated version and assets for release v22.06.2 2022-06-28 11:57:37 +01:00
Dan Brown
a1ea4006e0 Merge branch 'development' into release 2022-06-28 11:57:24 +01:00
Dan Brown
9078188939 Updated version and assets for release v22.06.1 2022-06-25 14:33:07 +01:00
Dan Brown
ed0aad1a7a Merge branch 'development' into release 2022-06-25 14:32:49 +01:00
Dan Brown
5c59cfb020 Updated version and assets for release v22.06 2022-06-24 11:50:56 +01:00
Dan Brown
3ca15ad68a Merge branch 'development' into release 2022-06-24 11:45:29 +01:00
Dan Brown
60014989f5 Updated version and assets for release v22.04.2 2022-05-09 16:10:16 +01:00
Dan Brown
57b10f195e Merge branch 'development' into release 2022-05-09 16:09:54 +01:00
Dan Brown
b1e95eb39f Updated version and assets for release v22.04.1 2022-05-04 21:26:58 +01:00
Dan Brown
b3da77b8f9 Merge branch 'development' into release 2022-05-04 21:26:31 +01:00
Dan Brown
1a345b74bb Updated version and assets for release v22.04 2022-04-29 15:55:32 +01:00
Dan Brown
8ffc3a4abf Merge branch 'development' into release 2022-04-29 15:55:05 +01:00
Dan Brown
7233c1c7b2 Updated version and assets for release v22.03.1 2022-03-30 19:37:07 +01:00
Dan Brown
1309a01131 Merge branch 'development' into release 2022-03-30 19:36:45 +01:00
Dan Brown
0333185b6d Updated version and assets for release v22.03 2022-03-30 13:49:17 +01:00
Dan Brown
83f89f64e8 Merge branch 'development' into release 2022-03-30 13:49:05 +01:00
Dan Brown
11a1a6fb16 Updated version and assets for release v22.02.3 2022-03-07 15:12:22 +00:00
Dan Brown
882c609296 Merge branch 'development' into release 2022-03-07 15:12:09 +00:00
Dan Brown
176a0dcd59 Updated version and assets for release v22.02.2 2022-03-01 22:45:41 +00:00
Dan Brown
94b0f70bfa Merge branch 'development' into release 2022-03-01 22:45:12 +00:00
Dan Brown
08b2a77d41 Updated version and assets for release v22.02.1 2022-02-27 17:46:06 +00:00
Dan Brown
3e8e9a23cf Merge branch 'development' into release 2022-02-27 17:45:49 +00:00
Dan Brown
58b83b64c8 Updated version and assets for release v22.02 2022-02-26 12:01:44 +00:00
Dan Brown
dfe4cde6ee Merge branch 'development' into release 2022-02-26 12:00:46 +00:00
Dan Brown
d11144d9e2 Updated version and assets for release v21.12.5 2022-02-06 15:49:23 +00:00
Dan Brown
f96b0ea5f3 Merge branch 'development' into release 2022-02-06 15:48:55 +00:00
Dan Brown
815f8d79ed Updated version and assets for release v21.12.4 2022-02-01 11:52:24 +00:00
Dan Brown
b62dab32e0 Merge branch 'development' into release 2022-02-01 11:51:48 +00:00
Dan Brown
262f863981 Updated version and assets for release v21.12.3 2022-01-24 22:49:42 +00:00
Dan Brown
a4c94390a1 Merge branch 'master' into release 2022-01-24 22:49:31 +00:00
Dan Brown
53f3cca85d Updated version and assets for release v21.12.2 2022-01-10 18:23:44 +00:00
Dan Brown
ed08bbcecc Merge branch 'master' into release 2022-01-10 18:23:19 +00:00
Dan Brown
de97ebf9b7 Updated version and assets for release v21.12.1 2022-01-06 12:20:37 +00:00
Dan Brown
f492a660a8 Merge branch 'master' into release 2022-01-06 12:20:26 +00:00
Dan Brown
09436836a5 Updated version and assets for release v21.12 2021-12-22 17:04:18 +00:00
Dan Brown
bb455d7788 Merge branch 'master' into release 2021-12-22 17:03:50 +00:00
Dan Brown
009212ab80 Updated version and assets for release v21.11.3 2021-12-15 14:08:37 +00:00
Dan Brown
ba9cb591c8 Merge branch 'master' into release 2021-12-15 14:08:17 +00:00
Dan Brown
d00ac2f34e Updated version and assets for release v21.11.2 2021-11-30 14:30:19 +00:00
Dan Brown
bd4dc6d463 Merge branch 'master' into release 2021-11-30 14:29:53 +00:00
Dan Brown
d91180a909 Updated version and assets for release v21.11.1 2021-11-23 20:44:36 +00:00
Dan Brown
bc2913a5cb Merge branch 'master' into release 2021-11-23 20:44:12 +00:00
Dan Brown
4802394562 Updated version and assets for release v21.11 2021-11-16 13:22:24 +00:00
Dan Brown
1755556468 Merge branch 'master' into release 2021-11-16 13:21:44 +00:00
Dan Brown
01cdbdb7ae Updated version and assets for release v21.10.3 2021-11-01 13:31:10 +00:00
Dan Brown
fc8bbf3eab Merge branch 'master' into release 2021-11-01 13:30:36 +00:00
Dan Brown
3cdab19319 Updated version and assets for release v21.10.2 2021-10-28 15:57:04 +01:00
Dan Brown
5661d20e87 Merge branch 'master' into release 2021-10-28 15:56:49 +01:00
Dan Brown
91f80123e8 Merge branch 'master' into release 2021-10-27 12:35:00 +01:00
Dan Brown
7a0636d0f8 Updated version and assets for release v21.10.1 2021-10-27 12:31:40 +01:00
Dan Brown
0fe5bdfbac Updated version and assets for release v21.10 2021-10-25 15:59:23 +01:00
Dan Brown
f88687e977 Merge branch 'master' into release 2021-10-25 15:58:59 +01:00
Dan Brown
68d437d05b Updated version and assets for release v21.08.6 2021-10-15 14:34:44 +01:00
Dan Brown
1e56aaea04 Merge branch 'master' into release 2021-10-15 14:34:23 +01:00
Dan Brown
dab170a6fe Updated version and assets for release v21.08.5 2021-10-08 22:25:36 +01:00
Dan Brown
a8de717d9b Merge branch 'master' into release 2021-10-08 22:25:05 +01:00
Dan Brown
78fe95b6fc Updated version and assets for release v21.08.4 2021-10-04 16:25:24 +01:00
Dan Brown
e0c24e41aa Merge branch 'master' into release 2021-10-04 16:24:54 +01:00
Dan Brown
fa8553839b Updated version and assets for release v21.08.3 2021-09-12 16:31:02 +01:00
Dan Brown
b8fcefc794 Merge branch 'master' into release 2021-09-12 16:30:35 +01:00
Dan Brown
88bcb68fcb Updated version and assets for release v21.08.2 2021-09-04 15:07:20 +01:00
Dan Brown
7c000553ae Merge branch 'master' into release 2021-09-04 15:06:33 +01:00
Dan Brown
391fa35c80 Updated version and assets for release v21.08.1 2021-09-02 21:13:09 +01:00
Dan Brown
c6773a8c9f Merge branch 'master' into release 2021-09-02 21:12:06 +01:00
Dan Brown
9b226e7d39 Updated version and assets for release v21.08 2021-08-31 22:07:53 +01:00
Dan Brown
9865446267 Merge branch 'master' into release 2021-08-31 22:07:23 +01:00
Dan Brown
926abbe776 Updated version and assets for release v21.05.4 2021-08-04 21:29:10 +01:00
Dan Brown
4fabef3a57 Merge branch 'v21.05.x' into release 2021-08-04 21:28:45 +01:00
Dan Brown
5ef4cd80c3 Updated version and assets for release v21.05.3 2021-07-03 11:59:52 +01:00
Dan Brown
e01f23583f Merge branch 'v21.05.x' into release 2021-07-03 11:59:21 +01:00
Dan Brown
7792cb3915 Updated version and assets for release v21.05.2 2021-06-13 14:26:34 +01:00
Dan Brown
be26253a18 Merge branch 'master' into release 2021-06-13 14:25:39 +01:00
Dan Brown
1bdd1f8189 Updated version for release v21.05.1 2021-06-04 23:09:42 +01:00
Dan Brown
fa62c79b17 Merge branch 'master' into release 2021-06-04 23:08:59 +01:00
Dan Brown
d7d8fa1e5b Updated version and assets for release v21.05 2021-05-30 16:17:56 +01:00
Dan Brown
18562f1e10 Merge branch 'master' into release 2021-05-30 16:17:44 +01:00
Dan Brown
86090a694f Updated version and assets for release v21.04.6 2021-05-24 13:06:03 +01:00
Dan Brown
1ee8287c73 Merge branch 'v21.04.x' into release 2021-05-24 13:05:34 +01:00
Dan Brown
8eb98cd591 Updated version and assets for release v21.04.5 2021-05-15 17:56:29 +01:00
Dan Brown
0f9ba21b05 Merge branch 'v21.04.x' into release 2021-05-15 17:56:03 +01:00
Dan Brown
834f8e7046 Updated version and assets for release v21.04.4 2021-05-09 14:46:05 +01:00
Dan Brown
32e3399334 Merge branch 'master' into release 2021-05-09 14:45:36 +01:00
Dan Brown
2d8698a218 Updated version and assets for release v21.04.3 2021-04-27 22:01:37 +01:00
Dan Brown
454fb883a2 Merge branch 'master' into release 2021-04-27 22:01:15 +01:00
Dan Brown
6f4a6ab8ea Updated version for release v21.04.2 2021-04-20 22:37:05 +01:00
Dan Brown
9c4b6f36f1 Merge branch 'master' into release 2021-04-20 22:36:35 +01:00
Dan Brown
78886b1e67 Updated version and assets for release v21.04.1 2021-04-19 22:26:19 +01:00
Dan Brown
d9debaf032 Merge branch 'master' into release 2021-04-19 22:25:29 +01:00
Dan Brown
d4360d6347 Updated version and assets for release v21.04 2021-04-09 21:18:32 +01:00
Dan Brown
175b1785c0 Merge branch 'master' into release 2021-04-09 21:18:09 +01:00
Dan Brown
c8740c0171 Updated version for release v0.31.8 2021-03-13 15:32:54 +00:00
Dan Brown
91ee895a74 Merge branch 'v0.31.x' into release 2021-03-13 15:32:06 +00:00
Dan Brown
a045e46571 Updated version for release v0.31.7 2021-03-02 21:19:17 +00:00
Dan Brown
44eaa65c3b Merge branch 'v0.31.x' into release 2021-03-02 21:18:31 +00:00
Dan Brown
0a22af7b14 Updated version for release v0.31.6 2021-02-06 14:41:19 +00:00
Dan Brown
b54702ab08 Merge branch 'v0.31.x' into release 2021-02-06 14:40:47 +00:00
Dan Brown
c4fdcfc5d1 Updated version for release v0.31.5 2021-02-02 20:58:06 +00:00
Dan Brown
cb8117e8df Merge branch 'v0.31.x' into release 2021-02-02 20:57:41 +00:00
Dan Brown
5a218d5056 Updated version and assets for release v0.31.4 2021-01-16 17:50:45 +00:00
Dan Brown
8dbc5cf9c6 Merge branch 'master' into release 2021-01-16 17:50:11 +00:00
Dan Brown
71e81615a3 Updated version for release v0.31.3 2021-01-10 23:29:58 +00:00
Dan Brown
611d37da04 Merge branch 'master' into release 2021-01-10 23:29:11 +00:00
Dan Brown
0e799a3857 Updated version and assets for release v0.31.2 2021-01-10 14:05:16 +00:00
Dan Brown
b91d6e2bfa Merge branch 'master' into release 2021-01-10 14:04:59 +00:00
Dan Brown
ea16ad7e94 Updated version and assets for release v0.31.1 2021-01-04 18:41:55 +00:00
Dan Brown
ba6eb54552 Merge branch 'master' into release 2021-01-04 18:41:26 +00:00
Dan Brown
f705e7683b Updated assets for release v0.31.0 again 2021-01-03 22:33:36 +00:00
Dan Brown
dc996adb20 Merge branch 'master' into release 2021-01-03 22:32:40 +00:00
Dan Brown
a64c638ccc Updated version and assets for release v0.31.0 2021-01-03 21:52:37 +00:00
Dan Brown
359c067279 Merge branch 'master' into release 2021-01-03 21:52:00 +00:00
Dan Brown
66a746e297 Updated version for release v0.30.7 2020-12-18 14:13:40 +00:00
Dan Brown
a4d43ee24b Merge branch 'v0.30.x' into release 2020-12-18 14:13:19 +00:00
Dan Brown
f7793a70a9 Updated version for release v0.30.6 2020-12-17 21:07:06 +00:00
Dan Brown
ceba3d31fb Merge branch 'v0.30.x' into release 2020-12-17 21:03:20 +00:00
Dan Brown
eecc08edde Updated version for release v0.30.5 2020-12-06 21:05:43 +00:00
Dan Brown
eb19aadc75 Merge branch 'v0.30.x' into release 2020-12-06 21:05:11 +00:00
Dan Brown
06c81e69b9 Updated version and assets for release v0.30.4 2020-10-31 16:52:33 +00:00
Dan Brown
3dc3d4a639 Merge branch 'master' into release 2020-10-31 16:51:54 +00:00
Dan Brown
94c59c1e3d Updated version and assets for release v0.30.3 2020-10-13 22:50:52 +01:00
Dan Brown
4d2205853a Merge branch 'master' into release 2020-10-13 22:50:30 +01:00
Dan Brown
751772b87a Updated version and assets for release v0.30.2 2020-09-30 22:44:58 +01:00
Dan Brown
76e30869e1 Merge branch 'master' into release 2020-09-30 22:44:17 +01:00
Dan Brown
3edc9fe9eb Updated version and assets for release v0.30.1 2020-09-26 17:51:37 +01:00
Dan Brown
616c62703e Merge branch 'master' into release 2020-09-26 17:50:25 +01:00
Dan Brown
ecd56917e7 Updated version and assets for release v0.30.0 2020-09-20 10:33:18 +01:00
Dan Brown
e22c9cae91 Merge branch 'master' into release 2020-09-20 10:30:10 +01:00
Dan Brown
29ddb6e1b9 Updated version and assets for release v0.29.3 2020-05-12 22:34:01 +01:00
Dan Brown
2ff90e2ff0 Merge branch 'master' into release 2020-05-12 22:33:27 +01:00
Dan Brown
04ecc128a2 Updated version and assets for release v0.29.2 2020-05-02 11:49:21 +01:00
Dan Brown
87d1d3423b Merge branch 'master' into release 2020-05-02 11:48:48 +01:00
Dan Brown
4818192a2a Updated version and assets for release v0.29.1 2020-04-28 12:30:31 +01:00
Dan Brown
965dd97f54 Merge branch 'master' into release 2020-04-28 12:30:09 +01:00
Dan Brown
195b74926c Updated version and assets for release v0.29.0 2020-04-13 16:10:23 +01:00
Dan Brown
2120db12b2 Merge branch 'master' into release 2020-04-13 16:10:11 +01:00
Dan Brown
ed563fef28 Updated version and assets for release v0.28.3 2020-03-14 22:31:42 +00:00
Dan Brown
0d31a8e3f1 Merge branch 'master' into release 2020-03-14 22:31:11 +00:00
Dan Brown
b8354b974b Updated version and assets for release v0.28.2 2020-02-15 22:36:08 +00:00
Dan Brown
034c1e289d Merge branch 'master' into release 2020-02-15 22:35:46 +00:00
Dan Brown
f31605a3de Updated version and assets for release v0.28.1 2020-02-15 22:08:06 +00:00
Dan Brown
e7cc75c74d Merge branch 'master' into release 2020-02-15 22:07:17 +00:00
Dan Brown
4b79d5e4e8 Updated version and assets for release v0.28.0 2020-02-03 22:44:45 +00:00
Dan Brown
34854915b3 Merge branch 'master' into release 2020-02-03 22:43:58 +00:00
Dan Brown
af6f34b529 Updated version and assets for release v0.27.5 2019-10-16 16:35:50 +01:00
Dan Brown
fb82a2b896 Merge branch 'patching-v0.27' into release 2019-10-16 16:35:10 +01:00
Dan Brown
5b464938b6 Updated version and assets for release v0.27.4 2019-09-07 13:30:08 +01:00
Dan Brown
81f954890d Merge branch 'patching-v0.27' into release 2019-09-07 13:29:53 +01:00
Dan Brown
0e2bbcec62 Updated version and assets for release v0.27.3 2019-09-03 21:50:12 +01:00
Dan Brown
fdd339f525 Merge branch 'master' into release 2019-09-03 21:49:46 +01:00
Dan Brown
8cf7d6a83d Updated version and assets for release v0.27.2 2019-09-01 12:12:23 +01:00
Dan Brown
58a5008718 Merge branch 'master' into release 2019-09-01 12:12:10 +01:00
Dan Brown
c44a8df55d Updated version and assets for release v0.27.1 2019-09-01 11:13:50 +01:00
Dan Brown
ff1494c519 Merge branch 'master' into release 2019-09-01 11:13:18 +01:00
Dan Brown
b8ce8fd852 Updated assets for release v0.27 2019-08-31 14:16:14 +01:00
Dan Brown
75e7454a5f Merge branch 'master' into release and set version 2019-08-31 14:15:18 +01:00
Dan Brown
2558ea8931 Updated version for release v0.26.4 2019-08-06 21:42:09 +01:00
Dan Brown
ac0f47a4b2 Merge branch 'v0.26' into release 2019-08-06 21:41:06 +01:00
Dan Brown
4f16129869 Updated version for release v0.26.3 2019-07-10 20:21:22 +01:00
Dan Brown
64a8037fdd Merge branch 'v0.26' into release 2019-07-10 20:19:54 +01:00
Dan Brown
7502ba1bc8 Updated version and assets for release v0.26.2 2019-05-27 13:48:20 +01:00
Dan Brown
33a04697ef Merge branch 'master' into release 2019-05-27 13:47:47 +01:00
Dan Brown
b70a5c0cdb Updated version and assets for release v0.26.1 2019-05-07 23:05:47 +01:00
Dan Brown
9443ae9f40 Merge branch 'master' into release 2019-05-07 23:05:10 +01:00
Dan Brown
220c2a4102 Updated version and assets for release v0.26.0 2019-05-06 18:58:56 +01:00
Dan Brown
e9914eb301 Merge branch 'master' into release 2019-05-06 18:57:58 +01:00
Dan Brown
934512d09c Updated version and assets for release v0.25.5 2019-03-24 19:45:17 +00:00
Dan Brown
9102c90986 Merge branch 'master' into release 2019-03-24 19:45:00 +00:00
Dan Brown
c3e74219c4 Updated version and assets for release v0.25.4 2019-03-21 19:46:19 +00:00
Dan Brown
13c9d7bc2d Merge branch 'master' into release 2019-03-21 19:43:48 +00:00
Dan Brown
119b539586 Updated version and assets for release v0.25.3 2019-03-21 00:03:26 +00:00
Dan Brown
29a5c180f0 Merge branch 'master' into release 2019-03-21 00:02:33 +00:00
Dan Brown
7906602291 Updated version and assets for release v0.25.2 2019-03-10 13:45:21 +00:00
Dan Brown
6dafe773ff Merge branch 'master' into release 2019-03-10 13:44:29 +00:00
Dan Brown
25bc28a1be Updated version and assets for release v0.25.1 2019-01-20 15:42:32 +00:00
Dan Brown
4c561c7fa0 Merge branch 'master' into release 2019-01-20 15:41:24 +00:00
Dan Brown
95b3e78573 Updated version and assets for release v0.25.0 2019-01-12 22:48:53 +00:00
Dan Brown
63a345bc93 Merge branch 'master' into release 2019-01-12 22:47:07 +00:00
Dan Brown
e093a172cb Updated assets and version for release v0.24.3 2018-11-27 21:52:20 +00:00
Dan Brown
4b01f8934b Merge branch 'master' into release 2018-11-27 21:51:32 +00:00
Dan Brown
bc116b45b5 Re-updated assets for release v0.24.2 2018-11-10 16:10:22 +00:00
Dan Brown
a059960b9e Merge branch 'master' into release 2018-11-10 16:09:14 +00:00
Dan Brown
7770966fed Updated assets for release v0.24.2 2018-11-10 16:01:55 +00:00
Dan Brown
d7adcf6c69 Merge branch 'master' into release 2018-11-10 16:01:01 +00:00
Dan Brown
04a364dcc3 Incremented version for v0.24.1 2018-09-24 16:34:16 +01:00
Dan Brown
db83ac7eaa Merge branch 'master' into release 2018-09-24 16:32:30 +01:00
Dan Brown
3ca9dddf61 Merge branch 'master' into release 2018-09-24 15:59:39 +01:00
Dan Brown
bf74f53ca7 Updated assets for release and incremented version 2018-09-24 12:18:27 +01:00
Dan Brown
9d67efb4a4 Merge branch 'master' into release 2018-09-24 12:08:21 +01:00
Dan Brown
3a39b9f440 Merge pull request #1022 from BookStackApp/revert-983-master
Revert "Update german translation"
2018-09-22 18:33:29 +01:00
Dan Brown
27f7aab375 Revert "Update german translation" 2018-09-22 18:33:15 +01:00
Dan Brown
337da0c467 Merge pull request #983 from vriic/master
Update german translation
2018-09-22 18:27:04 +01:00
Nikolai Nikolajevic
f56b3560c4 Update german translation 2018-08-23 16:17:46 +02:00
Dan Brown
02dfe11ce6 Increment version for release v0.23.2 2018-08-19 15:33:23 +01:00
Dan Brown
83d06beb70 Merge branch 'master' into release 2018-08-19 15:33:10 +01:00
Dan Brown
a8cfc059c8 Updated version for release v0.23.1 2018-08-12 14:22:53 +01:00
Dan Brown
1614b2bab0 Merge branch 'master' into release 2018-08-12 14:22:17 +01:00
Dan Brown
4bdec0d214 Updated version and assets for release v0.23 2018-07-29 20:28:49 +01:00
Dan Brown
6a7d7e7c2b Merge branch 'master' into release 2018-07-29 20:26:00 +01:00
Dan Brown
30d4674657 Updated assets for release v0.22 2018-05-28 14:19:14 +01:00
Dan Brown
9f961f95f8 Merge branch 'master' into release 2018-05-28 14:19:04 +01:00
Dan Brown
bab99a26ec Updated assets and version for v0.21 release 2018-04-22 20:21:22 +01:00
Dan Brown
9a7fecd269 Merge branch 'master' into release 2018-04-22 20:19:02 +01:00
Dan Brown
a8dc0d449b Updated the version because i'm such a plonker
And forgot to do this last release.
I wonder if there's a simple commit hook that could prevent the same two
versions twice in a row?
2018-03-30 15:41:46 +01:00
Dan Brown
a0381f76bf Merge branch 'v0.20' into release 2018-03-30 15:33:23 +01:00
Dan Brown
6102f66daa Updated assets for release v0.20.1 2018-03-25 16:58:14 +01:00
Dan Brown
c6134d162d Merge branch 'master' into release 2018-03-25 16:54:48 +01:00
Dan Brown
2046f9b9de Updated assets for release v0.20.0 2018-02-11 18:20:17 +00:00
Dan Brown
ac3ba594a4 Merge branch 'master' into release and updated version 2018-02-11 18:19:38 +00:00
Dan Brown
22df25a480 Updated assets and version for v0.19.0 2017-12-10 18:21:07 +00:00
Dan Brown
8b30c7f02e Merge branch 'master' into release 2017-12-10 18:19:20 +00:00
Dan Brown
757cdddc7c Updated version and JS for release v0.18.5 2017-11-11 18:33:04 +00:00
Dan Brown
df95e99680 Updated assets and version for release v0.18.4 2017-10-15 19:28:29 +01:00
Dan Brown
5a6d544db7 Merge branch 'master' into release 2017-10-15 19:27:50 +01:00
Dan Brown
16117d329c Merge branch 'master' into release, Updated version 2017-10-06 21:05:45 +01:00
Dan Brown
e90da18ada Updated assets and version for v0.18.2 release 2017-10-01 18:12:59 +01:00
Dan Brown
a08d80e1cc Merge branch 'master' into release 2017-10-01 18:12:07 +01:00
Dan Brown
6258175922 Updated assets and version for v0.18.1 release 2017-09-20 21:36:17 +01:00
Dan Brown
15736777a0 Merge branch 'master' into release 2017-09-20 21:35:33 +01:00
Dan Brown
75915e8a94 Updated assets for release v0.18 2017-09-10 17:07:57 +01:00
Dan Brown
9bde0ae4ea Merge branch 'master' into release 2017-09-10 17:05:05 +01:00
Dan Brown
0c802d1f86 Updated assets and version for release v0.17.4 2017-07-28 13:04:21 +01:00
Dan Brown
b7a96c6466 Merge branch 'master' into release 2017-07-28 13:03:36 +01:00
Dan Brown
4b645a82c7 Updated version for release 2017-07-22 17:27:01 +01:00
Dan Brown
d599b77b6f Merge branch 'master' into release 2017-07-22 17:26:44 +01:00
Dan Brown
26e93dc8c1 Updated assets and version for release v0.17.2 2017-07-22 16:49:07 +01:00
Dan Brown
a4c9a8491b Merge branch 'master' into release 2017-07-22 16:46:57 +01:00
Dan Brown
70ee636d87 Updated css and version for release 2017-07-10 20:52:32 +01:00
Dan Brown
b35f6dbb03 Merge branch 'master' into release 2017-07-10 20:51:25 +01:00
Dan Brown
67d9e24d8f Merge branch 'master' into release
Also updated assets, Version number
2017-07-02 22:52:26 +01:00
Dan Brown
3903fda6ca Incremented version 2017-06-04 15:38:49 +01:00
Dan Brown
441e46ebaa Merge branch 'v0.16' into release 2017-06-04 15:38:29 +01:00
Dan Brown
1f4260f359 Updated version for release v0.16.2 2017-05-07 19:35:51 +01:00
Dan Brown
dc0bf8ad4e Merge branch 'master' into release 2017-05-07 19:35:34 +01:00
Dan Brown
102e326e6a Updated JS and version for release v0.16.1 2017-04-30 19:51:23 +01:00
Dan Brown
2b25bf6f3b Merge branch 'master' into release 2017-04-30 19:50:29 +01:00
Dan Brown
f93280696d Updated assets for release v0.16 2017-04-23 20:42:28 +01:00
Dan Brown
1787391b07 Merge branch 'master' into release 2017-04-23 20:41:45 +01:00
Dan Brown
a74a8ee483 Updated version for v0.15.3 2017-03-23 22:22:16 +00:00
Dan Brown
7fa5405cb7 Merge branch 'master' into release 2017-03-23 22:21:04 +00:00
Dan Brown
6725ddcc41 Updated version for release v0.15.2 2017-03-05 15:50:52 +00:00
Dan Brown
bce941db3f Merge branch 'master' into release 2017-03-05 15:49:47 +00:00
Dan Brown
6d926048ec Updated to version v0.15.1 2017-02-27 16:59:10 +00:00
Dan Brown
5335c973b4 Merge branch 'master' into release 2017-02-27 16:58:20 +00:00
Dan Brown
15c3e5c96e Updated assets for release v0.15 2017-02-27 14:58:02 +00:00
Dan Brown
a5d5904969 Merge branch 'master' into release 2017-02-27 14:57:38 +00:00
Dan Brown
598758b991 Updated version for v0.14.3 2017-02-05 21:23:27 +00:00
Dan Brown
9926e23bc8 Merge branch 'v0.14' into release 2017-02-05 21:21:54 +00:00
Dan Brown
5d3264bc63 Updated assets for release v0.14.2 2017-02-01 22:27:04 +00:00
Dan Brown
d71f819f95 Merge branch 'v0.14' into release 2017-02-01 22:22:38 +00:00
Dan Brown
ee13509760 Updated version number 2017-01-23 22:28:31 +00:00
Dan Brown
82d7bb1f32 Merge branch 'master' into release 2017-01-23 22:28:02 +00:00
Dan Brown
cdfda508d8 Updated assets for release v0.14 2017-01-22 12:36:10 +00:00
Dan Brown
da941e584f Merge branch 'master' into release ready for v0.14 2017-01-22 12:31:27 +00:00
Dan Brown
65874d7b96 Updated assets for release v0.13.1 2016-11-27 19:42:33 +00:00
Dan Brown
ac9b8f405c Merge fixes from master for release v0.13.1 2016-11-27 19:41:12 +00:00
Dan Brown
8d1419a12e Update assets and version for release v0.13 2016-11-13 12:29:52 +00:00
Dan Brown
04f7a7d301 Merge branch 'master' into release 2016-11-13 12:26:56 +00:00
Dan Brown
c10d2a1493 Updated assets for release v0.12.2 2016-10-30 13:19:19 +00:00
Dan Brown
97bbf79ffd Merge branch 'v0.12' into release 2016-10-30 13:18:23 +00:00
Dan Brown
f7b01ae53d Updated assets for release v0.12.1 2016-09-06 20:50:15 +01:00
Dan Brown
d704e1dbba Merge branch 'master' into release 2016-09-06 20:49:15 +01:00
Dan Brown
ef2ff5e093 Updated assets for release v0.12 2016-09-05 19:49:42 +01:00
Dan Brown
7caed3b0db Merge branch 'master' into release 2016-09-05 19:35:21 +01:00
Dan Brown
45641d0754 Updated assets for release v0.11.2 2016-08-21 14:56:29 +01:00
Dan Brown
4b1d08ba99 Merge branch 'v0.11' into release 2016-08-21 14:55:11 +01:00
Dan Brown
160fa99ba4 Updated assets for release v0.11.1 2016-08-14 12:40:55 +01:00
Dan Brown
d2a5ab49ed Merge branch 'v0.11' into release 2016-08-14 12:37:48 +01:00
Dan Brown
c6404d8917 Updated assets for release v0.11 2016-07-03 10:56:16 +01:00
Dan Brown
7113807f12 Merge branch 'master' into release 2016-07-03 10:52:04 +01:00
Dan Brown
be711215e8 Updated assets for release v0.10 2016-05-22 15:12:47 +01:00
Dan Brown
7e3b404240 Merge branch 'master' into release for version v0.10 2016-05-22 15:11:50 +01:00
Dan Brown
e86901ca20 Updated assets for release v0.9.3 2016-05-03 21:13:02 +01:00
Dan Brown
bdfa61c8b2 Merge branch 'v0.9' into release 2016-05-03 21:11:01 +01:00
Dan Brown
2cc36787f5 Updated assets for release 0.9.2 2016-04-15 19:57:02 +01:00
Dan Brown
448ac61b48 Merge branch 'master' into release 2016-04-15 19:52:59 +01:00
Dan Brown
753f6394f7 Merge branch 'master' into release 2016-04-12 20:09:14 +01:00
Dan Brown
b1faf65934 Updated assets for release 0.9.0 2016-04-09 15:49:02 +01:00
Dan Brown
09f478bd74 Merge branch 'master' into release 2016-04-09 15:47:14 +01:00
Dan Brown
a0497feddd Updated assets for release 0.8.2 2016-03-30 21:44:30 +01:00
Dan Brown
789693bde9 Merge branch 'v0.8' into release 2016-03-30 21:32:46 +01:00
Dan Brown
1fe933e4ea Merge branch 'master' into release 2016-03-13 15:38:06 +00:00
Dan Brown
724b4b5a70 Updated assets for release 0.8.0 2016-03-13 15:15:14 +00:00
Dan Brown
1778a56146 Merge branch 'master' into release 2016-03-13 15:13:23 +00:00
Dan Brown
744865fcb2 Updated assets for release 0.7.6 2016-03-06 13:28:44 +00:00
Dan Brown
7f8c8b448d Merged branch master into release 2016-03-06 13:26:29 +00:00
Dan Brown
a67c53826d Updated assets for release 0.7.5 2016-02-25 21:24:09 +00:00
Dan Brown
14b131e850 Merge branch 'master' into release 2016-02-25 21:23:06 +00:00
Dan Brown
9b55a52b85 Updated assets for release 0.7.4 2016-02-11 22:35:01 +00:00
Dan Brown
db1d10e80f Merge branch 'master' into release 2016-02-11 22:29:29 +00:00
Dan Brown
1be576966f Updated assets for release 0.7.3 2016-02-08 20:47:33 +00:00
Dan Brown
b97e792c5f Merge branch 'master' into release 2016-02-08 20:45:48 +00:00
Dan Brown
8dec674cc3 Merge branch 'master' into release 2016-02-02 07:35:20 +00:00
Dan Brown
f784c03746 Merge branch 'master' into release 2016-02-01 18:31:04 +00:00
Dan Brown
148e172fe8 Updated assets for release 0.7 2016-01-31 18:03:55 +00:00
Dan Brown
56ae86646f Merge branch 'master' into release 2016-01-31 18:01:25 +00:00
Dan Brown
1d2b6fdfa2 Add updated assets 2016-01-02 14:50:59 +00:00
Dan Brown
4fc75beed4 Merge branch 'master' into release 2016-01-02 14:49:05 +00:00
Dan Brown
3b3bc0c4bf Updated compiled assets 2015-12-31 17:26:22 +00:00
Dan Brown
910faab88e Merge branch 'master' into release 2015-12-31 17:22:03 +00:00
Dan Brown
f184d763ad Added build folder to release 2015-12-16 17:53:53 +00:00
Dan Brown
a91d42634d Merge branch 'master' into release 2015-12-16 17:29:34 +00:00
Dan Brown
f517ef3616 Added new asset structure 2015-12-16 17:27:53 +00:00
Dan Brown
e99507ddcf Merge branch 'master' into release 2015-12-16 17:21:21 +00:00
Dan Brown
d2cacf1945 Release update 2015-12-01 21:30:21 +00:00
Dan Brown
448ac1405b Merge branch 'master' into release 2015-12-01 21:15:08 +00:00
Dan Brown
6ad21ce885 Added built assets for release 2015-11-30 21:59:34 +00:00
1137 changed files with 16492 additions and 42794 deletions

View File

@@ -36,14 +36,10 @@ APP_LANG=en
# APP_LANG will be used if such a header is not provided.
APP_AUTO_LANG_PUBLIC=true
# Application timezones
# The first option is used to determine what timezone is used for date storage.
# 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.
# Application timezone
# Used where dates are displayed such as on exported content.
# Valid timezone values can be found here: https://www.php.net/manual/en/timezones.php
APP_TIMEZONE=UTC
APP_DISPLAY_TIMEZONE=UTC
# Application theme
# Used to specific a themes/<APP_THEME> folder where BookStack UI
@@ -60,7 +56,6 @@ APP_PROXIES=null
# Database details
# Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
# An ipv6 address can be used via the square bracket format ([::1]).
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=database_database

View File

@@ -42,7 +42,6 @@ body:
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.
placeholder: Be sure to remove any confidential details in your logs
render: text
validations:
required: false
- type: textarea

View File

@@ -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

View File

@@ -438,7 +438,7 @@ javadataherian :: Persian
Ludo-code :: French
hollsten :: Swedish
Ngoc Lan Phung (lanpncz) :: Vietnamese
Worive :: Catalan; French
Worive :: Catalan
Илья Скаба (skabailya) :: Russian
Irjan Olsen (Irch) :: Norwegian Bokmal
Aleksandar Jovanovic (jovanoviczaleksandar) :: Serbian (Cyrillic)
@@ -453,59 +453,3 @@ Mohammed srhiri (m.sghiri20) :: Arabic
YongMin Kim (kym0118) :: Korean
Rivo Zängov (Eraser) :: Estonian
Francisco Rafael Fonseca (chicoraf) :: Portuguese, Brazilian
ИEØ_ΙΙØZ (NEO_IIOZ) :: Chinese Traditional
madnjpn (madnjpn.) :: Georgian
Ásgeir Shiny Ásgeirsson (AsgeirShiny) :: Icelandic
Mohammad Aftab Uddin (chirohorit) :: Bengali
Yannis Karlaftis (meliseus) :: Greek
felixxx :: German Informal
randi (randi65535) :: Korean
test65428 :: Greek
zeronell :: Chinese Simplified
julien Vinber (julienVinber) :: French
Hyunwoo Park (oksure) :: Korean
aram.rafeq.7 (aramrafeq2) :: Kurdish
Raphael Moreno (RaphaelMoreno) :: Portuguese, Brazilian
yn (user99) :: Arabic
Pavel Zlatarov (pzlatarov) :: Bulgarian
ingelres :: French
mabdullah :: Arabic
Skrabák Csaba (kekcsi) :: Hungarian
Evert Meulie (Evert) :: Norwegian Bokmal
Jasper Backer (jasperb) :: Dutch
Alexandar Cavdarovski (ace.200112) :: Swedish
구닥다리TV (yjj8353) :: Korean
Onur Oskay (o.oskay) :: Turkish
Sébastien Merveille (SebastienMerv) :: French
Maxim Kouznetsov (masya.work) :: Hebrew
neodvisnost :: Slovenian
Soubi Agatsuma (bisouya) :: Hebrew
Ilya Shaulov (ishaulov) :: Russian
Konstantin Bobkov (b.konstantv) :: Russian
Ruben Sutter (rubensutter) :: German
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

View File

@@ -11,9 +11,9 @@ on:
jobs:
build:
if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
- name: Setup PHP
uses: shivammathur/setup-php@v2

View File

@@ -11,14 +11,14 @@ on:
jobs:
build:
if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
php-version: 8.1
tools: phpcs
- name: Run formatting check

View File

@@ -13,12 +13,12 @@ on:
jobs:
build:
if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
strategy:
matrix:
php: ['8.2', '8.3', '8.4']
php: ['8.1', '8.2', '8.3']
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
- name: Setup PHP
uses: shivammathur/setup-php@v2

View File

@@ -13,12 +13,12 @@ on:
jobs:
build:
if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
strategy:
matrix:
php: ['8.2', '8.3', '8.4']
php: ['8.1', '8.2', '8.3']
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
- name: Setup PHP
uses: shivammathur/setup-php@v2

7
.gitignore vendored
View File

@@ -8,10 +8,10 @@ Homestead.yaml
.idea
npm-debug.log
yarn-error.log
/public/dist
/public/dist/*.map
/public/plugins
/public/css
/public/js
/public/css/*.map
/public/js/*.map
/public/bower
/public/build/
/public/favicon.ico
@@ -32,4 +32,3 @@ webpack-stats.json
phpstan.neon
esbuild-meta.json
.phpactor.json
/*.zip

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015-2025, Dan Brown and the BookStack project contributors.
Copyright (c) 2015-2024, Dan Brown and the BookStack Project contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -2,26 +2,60 @@
namespace BookStack\Access;
use BookStack\Users\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Database\Eloquent\Model;
class ExternalBaseUserProvider implements UserProvider
{
/**
* Retrieve a user by their unique identifier.
* The user model.
*
* @var string
*/
public function retrieveById(mixed $identifier): ?Authenticatable
protected $model;
/**
* LdapUserProvider constructor.
*/
public function __construct(string $model)
{
return User::query()->find($identifier);
$this->model = $model;
}
/**
* Create a new instance of the model.
*
* @return Model
*/
public function createModel()
{
$class = '\\' . ltrim($this->model, '\\');
return new $class();
}
/**
* Retrieve a user by their unique identifier.
*
* @param mixed $identifier
*
* @return Authenticatable|null
*/
public function retrieveById($identifier)
{
return $this->createModel()->newQuery()->find($identifier);
}
/**
* Retrieve a user by their unique identifier and "remember me" token.
*
* @param mixed $identifier
* @param string $token
*
* @return Authenticatable|null
*/
public function retrieveByToken(mixed $identifier, $token): null
public function retrieveByToken($identifier, $token)
{
return null;
}
@@ -41,25 +75,32 @@ class ExternalBaseUserProvider implements UserProvider
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
*
* @return Authenticatable|null
*/
public function retrieveByCredentials(array $credentials): ?Authenticatable
public function retrieveByCredentials(array $credentials)
{
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'])
->first();
}
/**
* Validate a user against the given credentials.
*
* @param Authenticatable $user
* @param array $credentials
*
* @return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials): bool
public function validateCredentials(Authenticatable $user, array $credentials)
{
// Should be done in the guard.
return false;
}
public function rehashPasswordIfRequired(Authenticatable $user, #[\SensitiveParameter] array $credentials, bool $force = false)
{
// No action to perform, any passwords are external in the auth system
}
}

View File

@@ -3,18 +3,23 @@
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
* into the default laravel 'Guard' auth flow. Instead, most of the logic is done via the relevant
* controller and services. This class provides a safer, thin version of SessionGuard.
* 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 Saml2 controller & Saml2Service. This class provides a safer, thin
* version of SessionGuard.
*/
class AsyncExternalBaseSessionGuard extends ExternalBaseSessionGuard
{
/**
* Validate a user's credentials.
*
* @param array $credentials
*
* @return bool
*/
public function validate(array $credentials = []): bool
public function validate(array $credentials = [])
{
return false;
}
@@ -22,9 +27,12 @@ class AsyncExternalBaseSessionGuard extends ExternalBaseSessionGuard
/**
* Attempt to authenticate a user using the given credentials.
*
* @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;
}

View File

@@ -4,7 +4,7 @@ namespace BookStack\Access\Guards;
use BookStack\Access\RegistrationService;
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\UserProvider;
use Illuminate\Contracts\Session\Session;
@@ -24,31 +24,43 @@ class ExternalBaseSessionGuard implements StatefulGuard
* The name of the Guard. Typically "session".
*
* Corresponds to guard name in authentication configuration.
*
* @var string
*/
protected readonly string $name;
protected $name;
/**
* The user we last attempted to retrieve.
*
* @var \Illuminate\Contracts\Auth\Authenticatable
*/
protected Authenticatable|null $lastAttempted;
protected $lastAttempted;
/**
* The session used by the guard.
*
* @var \Illuminate\Contracts\Session\Session
*/
protected Session $session;
protected $session;
/**
* Indicates if the logout method has been called.
*
* @var bool
*/
protected bool $loggedOut = false;
protected $loggedOut = false;
/**
* Service to handle common registration actions.
*
* @var RegistrationService
*/
protected RegistrationService $registrationService;
protected $registrationService;
/**
* Create a new authentication guard.
*
* @return void
*/
public function __construct(string $name, UserProvider $provider, Session $session, RegistrationService $registrationService)
{
@@ -60,11 +72,13 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Get the currently authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user(): Authenticatable|null
public function user()
{
if ($this->loggedOut) {
return null;
return;
}
// 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.
*
* @return int|null
*/
public function id(): int|null
public function id()
{
if ($this->loggedOut) {
return null;
return;
}
return $this->user()
@@ -101,8 +117,12 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* 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)) {
$this->setUser($this->lastAttempted);
@@ -115,8 +135,12 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* 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))) {
$this->setUser($user);
@@ -129,26 +153,38 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Validate a user's credentials.
*
* @param array $credentials
*
* @return bool
*/
public function validate(array $credentials = []): bool
public function validate(array $credentials = [])
{
return false;
}
/**
* 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;
}
/**
* Log the given user ID into the application.
*
* @param mixed $id
* @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,
// Logins should route through LoginService.
@@ -158,9 +194,12 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* 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());
@@ -169,8 +208,12 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* 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);
@@ -179,8 +222,10 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Log the user out of the application.
*
* @return void
*/
public function logout(): void
public function logout()
{
$this->clearUserDataFromStorage();
@@ -194,48 +239,62 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Remove the user data from the session and cookies.
*
* @return void
*/
protected function clearUserDataFromStorage(): void
protected function clearUserDataFromStorage()
{
$this->session->remove($this->getName());
}
/**
* Get the last user we attempted to authenticate.
*
* @return \Illuminate\Contracts\Auth\Authenticatable
*/
public function getLastAttempted(): Authenticatable
public function getLastAttempted()
{
return $this->lastAttempted;
}
/**
* 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);
}
/**
* Determine if the user was authenticated via "remember me" cookie.
*
* @return bool
*/
public function viaRemember(): bool
public function viaRemember()
{
return false;
}
/**
* Return the currently cached user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function getUser(): Authenticatable|null
public function getUser()
{
return $this->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;

View File

@@ -35,9 +35,13 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
/**
* Validate a user's credentials.
*
* @param array $credentials
*
* @throws LdapException
*
* @return bool
*/
public function validate(array $credentials = []): bool
public function validate(array $credentials = [])
{
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
@@ -53,13 +57,16 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
/**
* Attempt to authenticate a user using the given credentials.
*
* @param array $credentials
* @param bool $remember
*
* @throws LdapException
* @throws LdapException*@throws \BookStack\Exceptions\JsonDebugException
* @throws LoginAttemptException
* @throws JsonDebugException
*
* @return bool
*/
public function attempt(array $credentials = [], $remember = false): bool
public function attempt(array $credentials = [], $remember = false)
{
$username = $credentials['username'];
$userDetails = $this->ldapService->getUserDetails($username);

View File

@@ -54,7 +54,7 @@ class Ldap
*
* @return \LDAP\Result|array|false
*/
public function search($ldapConnection, string $baseDn, string $filter, array $attributes = [])
public function search($ldapConnection, string $baseDn, string $filter, array $attributes = null)
{
return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
}
@@ -66,7 +66,7 @@ class Ldap
*
* @return \LDAP\Result|array|false
*/
public function read($ldapConnection, string $baseDn, string $filter, array $attributes = [])
public function read($ldapConnection, string $baseDn, string $filter, array $attributes = null)
{
return ldap_read($ldapConnection, $baseDn, $filter, $attributes);
}
@@ -87,7 +87,7 @@ class Ldap
*
* @param resource|\LDAP\Connection $ldapConnection
*/
public function searchAndGetEntries($ldapConnection, string $baseDn, string $filter, array $attributes = []): array|false
public function searchAndGetEntries($ldapConnection, string $baseDn, string $filter, array $attributes = null): array|false
{
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
@@ -99,7 +99,7 @@ class Ldap
*
* @param resource|\LDAP\Connection $ldapConnection
*/
public function bind($ldapConnection, ?string $bindRdn = null, ?string $bindPassword = null): bool
public function bind($ldapConnection, string $bindRdn = null, string $bindPassword = null): bool
{
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
}

View File

@@ -71,26 +71,6 @@ class LdapService
return $users[0];
}
/**
* Build the user display name from the (potentially multiple) attributes defined by the configuration.
*/
protected function getUserDisplayName(array $userDetails, array $displayNameAttrs, string $defaultValue): string
{
$displayNameParts = [];
foreach ($displayNameAttrs as $dnAttr) {
$dnComponent = $this->getUserResponseProperty($userDetails, $dnAttr, null);
if ($dnComponent) {
$displayNameParts[] = $dnComponent;
}
}
if (empty($displayNameParts)) {
return $defaultValue;
}
return implode(' ', $displayNameParts);
}
/**
* Get the details of a user from LDAP using the given username.
* User found via configurable user filter.
@@ -101,25 +81,21 @@ class LdapService
{
$idAttr = $this->config['id_attribute'];
$emailAttr = $this->config['email_attribute'];
$displayNameAttrs = explode('|', $this->config['display_name_attribute']);
$displayNameAttr = $this->config['display_name_attribute'];
$thumbnailAttr = $this->config['thumbnail_attribute'];
$user = $this->getUserWithAttributes($userName, array_filter([
'cn', 'dn', $idAttr, $emailAttr, ...$displayNameAttrs, $thumbnailAttr,
'cn', 'dn', $idAttr, $emailAttr, $displayNameAttr, $thumbnailAttr,
]));
if (is_null($user)) {
return null;
}
$nameDefault = $this->getUserResponseProperty($user, 'cn', null);
if (is_null($nameDefault)) {
$nameDefault = ldap_explode_dn($user['dn'], 1)[0] ?? $user['dn'];
}
$userCn = $this->getUserResponseProperty($user, 'cn', null);
$formatted = [
'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']),
'name' => $this->getUserDisplayName($user, $displayNameAttrs, $nameDefault),
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
'dn' => $user['dn'],
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
'avatar' => $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,

View File

@@ -5,11 +5,9 @@ namespace BookStack\Access;
use BookStack\Access\Mfa\MfaSession;
use BookStack\Activity\ActivityType;
use BookStack\Exceptions\LoginAttemptException;
use BookStack\Exceptions\LoginAttemptInvalidUserException;
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Facades\Activity;
use BookStack\Facades\Theme;
use BookStack\Permissions\Permission;
use BookStack\Theming\ThemeEvents;
use BookStack\Users\Models\User;
use Exception;
@@ -31,14 +29,10 @@ class LoginService
* a reason to (MFA or Unconfirmed Email).
* Returns a boolean to indicate the current login result.
*
* @throws StoppedAuthenticationException|LoginAttemptInvalidUserException
* @throws StoppedAuthenticationException
*/
public function login(User $user, string $method, bool $remember = false): void
{
if ($user->isGuest()) {
throw new LoginAttemptInvalidUserException('Login not allowed for guest user');
}
if ($this->awaitingEmailConfirmation($user) || $this->needsMfaVerification($user)) {
$this->setLastLoginAttemptedForUser($user, $method, $remember);
@@ -51,7 +45,7 @@ class LoginService
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $method, $user);
// 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'];
foreach ($guards as $guard) {
auth($guard)->login($user);
@@ -64,7 +58,7 @@ class LoginService
*
* @throws Exception
*/
public function reattemptLoginFor(User $user): void
public function reattemptLoginFor(User $user)
{
if ($user->id !== ($this->getLastLoginAttemptUser()->id ?? null)) {
throw new Exception('Login reattempt user does align with current session state');
@@ -96,7 +90,7 @@ class LoginService
{
$value = session()->get(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
if (!$value) {
return ['user_id' => null, 'method' => null, 'remember' => false];
return ['user_id' => null, 'method' => null];
}
[$id, $method, $remember, $time] = explode(':', $value);
@@ -104,18 +98,18 @@ class LoginService
if ($time < $hourAgo) {
$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)];
}
/**
* 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
* 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(
self::LAST_LOGIN_ATTEMPTED_SESSION_KEY,
@@ -158,40 +152,16 @@ class LoginService
*/
public function attempt(array $credentials, string $method, bool $remember = false): bool
{
if ($this->areCredentialsForGuest($credentials)) {
return false;
}
$result = auth()->attempt($credentials, $remember);
if ($result) {
$user = auth()->user();
auth()->logout();
try {
$this->login($user, $method, $remember);
} catch (LoginAttemptInvalidUserException $e) {
// Catch and return false for non-login accounts
// so it looks like a normal invalid login.
return false;
}
$this->login($user, $method, $remember);
}
return $result;
}
/**
* Check if the given credentials are likely for the system guest account.
*/
protected function areCredentialsForGuest(array $credentials): bool
{
if (isset($credentials['email'])) {
return User::query()->where('email', '=', $credentials['email'])
->where('system_name', '=', 'public')
->exists();
}
return false;
}
/**
* Logs the current user out of the application.
* Returns an app post-redirect path.

View File

@@ -11,7 +11,6 @@ use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Theme;
use BookStack\Http\HttpRequestService;
use BookStack\Theming\ThemeEvents;
use BookStack\Uploads\UserAvatars;
use BookStack\Users\Models\User;
use Illuminate\Support\Facades\Cache;
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
@@ -27,8 +26,7 @@ class OidcService
protected RegistrationService $registrationService,
protected LoginService $loginService,
protected HttpRequestService $http,
protected GroupSyncService $groupService,
protected UserAvatars $userAvatars
protected GroupSyncService $groupService
) {
}
@@ -222,10 +220,6 @@ class OidcService
throw new OidcException($exception->getMessage());
}
if ($this->config()['fetch_avatar'] && !$user->avatar()->exists() && $userDetails->picture) {
$this->userAvatars->assignToUserFromUrl($user, $userDetails->picture);
}
if ($this->shouldSyncGroups()) {
$detachExisting = $this->config()['remove_from_groups'];
$this->groupService->syncUserWithFoundGroups($user, $userDetails->groups ?? [], $detachExisting);

View File

@@ -11,7 +11,6 @@ class OidcUserDetails
public ?string $email = null,
public ?string $name = null,
public ?array $groups = null,
public ?string $picture = null,
) {
}
@@ -41,16 +40,15 @@ class OidcUserDetails
$this->email = $claims->getClaim('email') ?? $this->email;
$this->name = static::getUserDisplayName($displayNameClaims, $claims) ?? $this->name;
$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);
$displayName = [];
foreach ($displayNameClaimParts as $claim) {
$component = $claims->getClaim(trim($claim)) ?? '';
$component = $token->getClaim(trim($claim)) ?? '';
if ($component !== '') {
$displayName[] = $component;
}
@@ -59,13 +57,13 @@ class OidcUserDetails
return implode(' ', $displayName);
}
protected static function getUserGroups(string $groupsClaim, ProvidesClaims $claims): ?array
protected static function getUserGroups(string $groupsClaim, ProvidesClaims $token): ?array
{
if (empty($groupsClaim)) {
return null;
}
$groupsList = Arr::get($claims->getAllClaims(), $groupsClaim);
$groupsList = Arr::get($token->getAllClaims(), $groupsClaim);
if (!is_array($groupsList)) {
return null;
}
@@ -74,14 +72,4 @@ class OidcUserDetails
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;
}
}

View File

@@ -11,9 +11,7 @@ class OidcUserinfoResponse implements ProvidesClaims
public function __construct(ResponseInterface $response, string $issuer, array $keys)
{
$contentTypeHeaderValue = $response->getHeader('Content-Type')[0] ?? '';
$contentType = strtolower(trim(explode(';', $contentTypeHeaderValue, 2)[0]));
$contentType = $response->getHeader('Content-Type')[0];
if ($contentType === 'application/json') {
$this->claims = json_decode($response->getBody()->getContents(), true);
}

View File

@@ -51,7 +51,7 @@ class Saml2Service
* Returns the SAML2 request ID, and the URL to redirect the user to.
*
* @throws Error
* @return array{url: string, id: ?string}
* @returns array{url: string, id: ?string}
*/
public function logout(User $user): array
{

View File

@@ -55,7 +55,7 @@ class SocialDriverManager
/**
* Gets the names of the active social drivers, keyed by driver id.
* @return array<string, string>
* @returns array<string, string>
*/
public function getActive(): array
{
@@ -92,7 +92,7 @@ class SocialDriverManager
string $driverName,
array $config,
string $socialiteHandler,
?callable $configureForRedirect = null
callable $configureForRedirect = null
) {
$this->validDrivers[] = $driverName;
config()->set('services.' . $driverName, $config);

View File

@@ -11,7 +11,6 @@ use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\Relation;
class ActivityQueries
@@ -68,7 +67,6 @@ class ActivityQueries
$activity = $query->orderBy('created_at', 'desc')
->with(['loggable' => function (Relation $query) {
/** @var MorphTo<Entity, Activity> $query */
$query->withTrashed();
}, 'user.avatar'])
->skip($count * ($page - 1))

View File

@@ -67,14 +67,6 @@ class ActivityType
const WEBHOOK_UPDATE = 'webhook_update';
const WEBHOOK_DELETE = 'webhook_delete';
const IMPORT_CREATE = 'import_create';
const IMPORT_RUN = 'import_run';
const IMPORT_DELETE = 'import_delete';
const SORT_RULE_CREATE = 'sort_rule_create';
const SORT_RULE_UPDATE = 'sort_rule_update';
const SORT_RULE_DELETE = 'sort_rule_delete';
/**
* Get all the possible values.
*/

View File

@@ -4,8 +4,6 @@ namespace BookStack\Activity;
use BookStack\Activity\Models\Comment;
use BookStack\Entities\Models\Entity;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PrettyException;
use BookStack\Facades\Activity as ActivityService;
use BookStack\Util\HtmlDescriptionFilter;
@@ -22,7 +20,7 @@ class CommentRepo
/**
* 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;
$comment = new Comment();
@@ -31,8 +29,7 @@ class CommentRepo
$comment->created_by = $userId;
$comment->updated_by = $userId;
$comment->local_id = $this->getNextLocalId($entity);
$comment->parent_id = $parentId;
$comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $contentRef) === 1 ? $contentRef : '';
$comment->parent_id = $parent_id;
$entity->comments()->save($comment);
ActivityService::add(ActivityType::COMMENT_CREATE, $comment);
@@ -55,41 +52,6 @@ class CommentRepo
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.
*/

View File

@@ -4,7 +4,6 @@ namespace BookStack\Activity\Controllers;
use BookStack\Activity\Models\Activity;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
class AuditLogApiController extends ApiController
{
@@ -17,8 +16,8 @@ class AuditLogApiController extends ApiController
*/
public function list()
{
$this->checkPermission(Permission::SettingsManage);
$this->checkPermission(Permission::UsersManage);
$this->checkPermission('settings-manage');
$this->checkPermission('users-manage');
$query = Activity::query()->with(['user']);

View File

@@ -5,8 +5,6 @@ namespace BookStack\Activity\Controllers;
use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\Activity;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\Sorting\SortUrl;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
@@ -14,8 +12,8 @@ class AuditLogController extends Controller
{
public function index(Request $request)
{
$this->checkPermission(Permission::SettingsManage);
$this->checkPermission(Permission::UsersManage);
$this->checkPermission('settings-manage');
$this->checkPermission('users-manage');
$sort = $request->get('sort', 'activity_date');
$order = $request->get('order', 'desc');
@@ -67,7 +65,6 @@ class AuditLogController extends Controller
'filters' => $filters,
'listOptions' => $listOptions,
'activityTypes' => $types,
'filterSortUrl' => new SortUrl('settings/audit', array_filter($request->except('page')))
]);
}
}

View File

@@ -3,11 +3,8 @@
namespace BookStack\Activity\Controllers;
use BookStack\Activity\CommentRepo;
use BookStack\Activity\Tools\CommentTree;
use BookStack\Activity\Tools\CommentTreeNode;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
@@ -29,7 +26,6 @@ class CommentController extends Controller
$input = $this->validate($request, [
'html' => ['required', 'string'],
'parent_id' => ['nullable', 'integer'],
'content_ref' => ['string'],
]);
$page = $this->pageQueries->findVisibleById($pageId);
@@ -43,13 +39,15 @@ class CommentController extends Controller
}
// Create a new comment.
$this->checkPermission(Permission::CommentCreateAll);
$contentRef = $input['content_ref'] ?? '';
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $contentRef);
$this->checkPermission('comment-create-all');
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null);
return view('comments.comment-branch', [
'readOnly' => false,
'branch' => new CommentTreeNode($comment, 0, []),
'branch' => [
'comment' => $comment,
'children' => [],
]
]);
}
@@ -65,8 +63,8 @@ class CommentController extends Controller
]);
$comment = $this->commentRepo->getById($commentId);
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
$this->checkOwnablePermission(Permission::CommentUpdate, $comment);
$this->checkOwnablePermission('page-view', $comment->entity);
$this->checkOwnablePermission('comment-update', $comment);
$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.
*/
public function destroy(int $id)
{
$comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission(Permission::CommentDelete, $comment);
$this->checkOwnablePermission('comment-delete', $comment);
$this->commentRepo->delete($comment);

View File

@@ -5,14 +5,13 @@ namespace BookStack\Activity\Controllers;
use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Tools\MixedEntityRequestHelper;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use Illuminate\Http\Request;
class WatchController extends Controller
{
public function update(Request $request, MixedEntityRequestHelper $entityHelper)
{
$this->checkPermission(Permission::ReceiveNotifications);
$this->checkPermission('receive-notifications');
$this->preventGuestAccess();
$requestData = $this->validate($request, array_merge([

View File

@@ -6,7 +6,6 @@ use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\Webhook;
use BookStack\Activity\Queries\WebhooksAllPaginatedAndSorted;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
@@ -15,7 +14,7 @@ class WebhookController extends Controller
public function __construct()
{
$this->middleware([
Permission::SettingsManage->middleware()
'can:settings-manage',
]);
}

View File

@@ -4,8 +4,6 @@ namespace BookStack\Activity\Models;
use BookStack\App\Model;
use BookStack\Users\Models\HasCreatorAndUpdater;
use BookStack\Users\Models\OwnableInterface;
use BookStack\Users\Models\User;
use BookStack\Util\HtmlContentFilter;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -19,15 +17,16 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
* @property int $local_id
* @property string $entity_type
* @property int $entity_id
* @property string $content_ref
* @property bool $archived
* @property int $created_by
* @property int $updated_by
*/
class Comment extends Model implements Loggable, OwnableInterface
class Comment extends Model implements Loggable
{
use HasFactory;
use HasCreatorAndUpdater;
protected $fillable = ['parent_id'];
protected $appends = ['created', 'updated'];
/**
* Get the entity that this comment belongs to.
@@ -39,7 +38,6 @@ class Comment extends Model implements Loggable, OwnableInterface
/**
* Get the parent comment this is in reply to (if existing).
* @return BelongsTo<Comment, $this>
*/
public function parent(): BelongsTo
{
@@ -56,6 +54,22 @@ class Comment extends Model implements Loggable, OwnableInterface
return $this->updated_at->timestamp > $this->created_at->timestamp;
}
/**
* Get created date as a relative diff.
*/
public function getCreatedAttribute(): string
{
return $this->created_at->diffForHumans();
}
/**
* Get updated date as a relative diff.
*/
public function getUpdatedAttribute(): string
{
return $this->updated_at->diffForHumans();
}
public function logDescriptor(): string
{
return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->entity_type} (ID: {$this->entity_id})";

View File

@@ -12,8 +12,6 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
* @property int $id
* @property string $name
* @property string $value
* @property int $entity_id
* @property string $entity_type
* @property int $order
*/
class Tag extends Model

View File

@@ -5,10 +5,8 @@ namespace BookStack\Activity\Notifications\Handlers;
use BookStack\Activity\Models\Loggable;
use BookStack\Activity\Notifications\Messages\BaseActivityNotification;
use BookStack\Entities\Models\Entity;
use BookStack\Permissions\Permission;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User;
use Illuminate\Support\Facades\Log;
abstract class BaseNotificationHandler implements NotificationHandler
{
@@ -27,7 +25,7 @@ abstract class BaseNotificationHandler implements NotificationHandler
}
// Prevent sending of the user does not have notification permissions
if (!$user->can(Permission::ReceiveNotifications)) {
if (!$user->can('receive-notifications')) {
continue;
}
@@ -38,11 +36,7 @@ abstract class BaseNotificationHandler implements NotificationHandler
}
// Send the notification
try {
$user->notify(new $notification($detail, $initiator));
} catch (\Exception $exception) {
Log::error("Failed to send email notification to user [id:{$user->id}] with error: {$exception->getMessage()}");
}
$user->notify(new $notification($detail, $initiator));
}
}
}

View File

@@ -20,8 +20,7 @@ class PageUpdateNotificationHandler extends BaseNotificationHandler
throw new \InvalidArgumentException("Detail for page update notifications must be a page");
}
// Get the last update from activity
/** @var ?Activity $lastUpdate */
// Get last update from activity
$lastUpdate = $detail->activity()
->where('type', '=', ActivityType::PAGE_UPDATE)
->where('id', '!=', $activity->id)

View File

@@ -4,13 +4,12 @@ namespace BookStack\Activity\Tools;
use BookStack\Activity\Models\Comment;
use BookStack\Entities\Models\Page;
use BookStack\Permissions\Permission;
class CommentTree
{
/**
* The built nested tree structure array.
* @var CommentTreeNode[]
* @var array{comment: Comment, depth: int, children: array}[]
*/
protected array $tree;
protected array $comments;
@@ -29,7 +28,7 @@ class CommentTree
public function empty(): bool
{
return count($this->getActive()) === 0;
return count($this->tree) === 0;
}
public function count(): int
@@ -37,41 +36,15 @@ class CommentTree
return count($this->comments);
}
public function getActive(): array
public function get(): array
{
return array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived);
}
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;
return $this->tree;
}
public function canUpdateAny(): bool
{
foreach ($this->comments as $comment) {
if (userCan(Permission::CommentUpdate, $comment)) {
if (userCan('comment-update', $comment)) {
return true;
}
}
@@ -81,7 +54,6 @@ class CommentTree
/**
* @param Comment[] $comments
* @return CommentTreeNode[]
*/
protected function createTree(array $comments): array
{
@@ -105,22 +77,26 @@ class CommentTree
$tree = [];
foreach ($childMap[0] ?? [] as $childId) {
$tree[] = $this->createTreeNodeForId($childId, 0, $byId, $childMap);
$tree[] = $this->createTreeForId($childId, 0, $byId, $childMap);
}
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] ?? [];
$children = [];
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

View File

@@ -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;
}
}

View File

@@ -3,16 +3,17 @@
namespace BookStack\Activity\Tools;
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
{
public function __construct(
protected Entity $entity
) {
protected array $tags;
/**
* @param Tag[] $tags
*/
public function __construct(array $tags)
{
$this->tags = $tags;
}
/**
@@ -21,23 +22,14 @@ class TagClassGenerator
public function generate(): array
{
$classes = [];
$tags = $this->entity->tags->all();
foreach ($tags as $tag) {
array_push($classes, ...$this->generateClassesForTag($tag));
}
if ($this->entity instanceof BookChild && userCan(Permission::BookView, $this->entity->book)) {
$bookTags = $this->entity->book->tags;
foreach ($bookTags as $bookTag) {
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-'));
foreach ($this->tags as $tag) {
$name = $this->normalizeTagClassString($tag->name);
$value = $this->normalizeTagClassString($tag->value);
$classes[] = 'tag-name-' . $name;
if ($value) {
$classes[] = 'tag-value-' . $value;
$classes[] = 'tag-pair-' . $name . '-' . $value;
}
}
@@ -49,22 +41,6 @@ class TagClassGenerator
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
{
$value = str_replace(' ', '', strtolower($value));

View File

@@ -7,7 +7,6 @@ use BookStack\Activity\WatchLevels;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Permissions\Permission;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;
@@ -23,7 +22,7 @@ class UserEntityWatchOptions
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

View File

@@ -50,7 +50,7 @@ class WebhookFormatter
}
if ($this->detail instanceof Model) {
$data['related_item'] = $this->formatModel($this->detail);
$data['related_item'] = $this->formatModel();
}
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();
foreach ($this->modelFormatters as $formatter) {

View File

@@ -36,7 +36,7 @@ class WatchLevels
/**
* Get all the possible values as an option_name => value array.
* @return array<string, int>
* @returns array<string, int>
*/
public static function all(): array
{
@@ -50,7 +50,7 @@ class WatchLevels
/**
* Get the watch options suited for the given entity.
* @return array<string, int>
* @returns array<string, int>
*/
public static function allSuitedFor(Entity $entity): array
{

View File

@@ -2,7 +2,6 @@
namespace BookStack\Api;
use BookStack\App\AppVersion;
use BookStack\Http\ApiController;
use Exception;
use Illuminate\Contracts\Container\BindingResolutionException;
@@ -26,7 +25,7 @@ class ApiDocsGenerator
*/
public static function generateConsideringCache(): Collection
{
$appVersion = AppVersion::get();
$appVersion = trim(file_get_contents(base_path('version')));
$cacheKey = 'api-docs::' . $appVersion;
$isProduction = config('app.env') === 'production';
$cacheVal = $isProduction ? Cache::get($cacheKey) : null;

View File

@@ -2,9 +2,7 @@
namespace BookStack\Api;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
class ApiEntityListFormatter
{
@@ -22,16 +20,8 @@ class ApiEntityListFormatter
* @var array<string|int, string|callable>
*/
protected array $fields = [
'id',
'name',
'slug',
'book_id',
'chapter_id',
'draft',
'template',
'priority',
'created_at',
'updated_at',
'id', 'name', 'slug', 'book_id', 'chapter_id', 'draft',
'template', 'priority', 'created_at', 'updated_at',
];
public function __construct(array $list)
@@ -72,28 +62,6 @@ class ApiEntityListFormatter
return $this;
}
/**
* Include parent book/chapter info in the formatted data.
*/
public function withParents(): self
{
$this->withField('book', function (Entity $entity) {
if ($entity instanceof BookChild && $entity->book) {
return $entity->book->only(['id', 'name', 'slug']);
}
return null;
});
$this->withField('chapter', function (Entity $entity) {
if ($entity instanceof Page && $entity->chapter) {
return $entity->chapter->only(['id', 'name', 'slug']);
}
return null;
});
return $this;
}
/**
* Format the data and return an array of formatted content.
* @return array[]

View File

@@ -4,7 +4,6 @@ namespace BookStack\Api;
use BookStack\Access\LoginService;
use BookStack\Exceptions\ApiAuthException;
use BookStack\Permissions\Permission;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
@@ -147,7 +146,7 @@ class ApiTokenGuard implements Guard
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);
}
}

View File

@@ -4,7 +4,6 @@ namespace BookStack\Api;
use BookStack\Activity\ActivityType;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\Users\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
@@ -17,8 +16,8 @@ class UserApiTokenController extends Controller
*/
public function create(Request $request, int $userId)
{
$this->checkPermission(Permission::AccessApi);
$this->checkPermissionOrCurrentUser(Permission::UsersManage, $userId);
$this->checkPermission('access-api');
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$this->updateContext($request);
$user = User::query()->findOrFail($userId);
@@ -36,8 +35,8 @@ class UserApiTokenController extends Controller
*/
public function store(Request $request, int $userId)
{
$this->checkPermission(Permission::AccessApi);
$this->checkPermissionOrCurrentUser(Permission::UsersManage, $userId);
$this->checkPermission('access-api');
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$this->validate($request, [
'name' => ['required', 'max:250'],
@@ -144,8 +143,8 @@ class UserApiTokenController extends Controller
*/
protected function checkPermissionAndFetchUserToken(int $userId, int $tokenId): array
{
$this->checkPermissionOr(Permission::UsersManage, function () use ($userId) {
return $userId === user()->id && userCan(Permission::AccessApi);
$this->checkPermissionOr('users-manage', function () use ($userId) {
return $userId === user()->id && userCan('access-api');
});
$user = User::query()->findOrFail($userId);

View File

@@ -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;
}
}

View File

@@ -8,7 +8,7 @@ class Model extends EloquentModel
{
/**
* Provides public access to get the raw attribute value from the model.
* Used in areas where no mutations are required, but performance is critical.
* Used in areas where no mutations are required but performance is critical.
*
* @return mixed
*/

View File

@@ -59,8 +59,8 @@ class AuthServiceProvider extends ServiceProvider
*/
public function register(): void
{
Auth::provider('external-users', function () {
return new ExternalBaseUserProvider();
Auth::provider('external-users', function ($app, array $config) {
return new ExternalBaseUserProvider($config['model']);
});
// Bind and provide the default system user as a singleton to the app instance when needed.

View File

@@ -15,7 +15,7 @@ class EventServiceProvider extends ServiceProvider
/**
* The event listener mappings for the application.
*
* @var array<class-string, array<int, string>>
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
SocialiteWasCalled::class => [
@@ -42,12 +42,4 @@ class EventServiceProvider extends ServiceProvider
{
return false;
}
/**
* Overrides the registration of Laravel's default email verification system
*/
protected function configureEmailVerification(): void
{
//
}
}

View File

@@ -85,12 +85,5 @@ class RouteServiceProvider extends ServiceProvider
RateLimiter::for('public', function (Request $request) {
return Limit::perMinute(10)->by($request->ip());
});
RateLimiter::for('exports', function (Request $request) {
$user = user();
$attempts = $user->isGuest() ? 4 : 10;
$key = $user->isGuest() ? $request->ip() : $user->id;
return Limit::perMinute($attempts)->by($key);
});
}
}

View File

@@ -3,7 +3,6 @@
namespace BookStack\App\Providers;
use BookStack\Entities\BreadcrumbsViewComposer;
use BookStack\Util\DateFormatter;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\View;
@@ -11,15 +10,6 @@ use Illuminate\Support\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.
*/
@@ -31,9 +21,6 @@ class ViewTweaksServiceProvider extends ServiceProvider
// View Composers
View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class);
// View Globals
View::share('dates', $this->app->make(DateFormatter::class));
// Custom blade view directives
Blade::directive('icon', function ($expression) {
return "<?php echo (new \BookStack\Util\SvgIcon($expression))->toHtml(); ?>";

View File

@@ -5,8 +5,11 @@ namespace BookStack\App;
/**
* Assigned to models that can have slugs.
* Must have the below properties.
*
* @property int $id
* @property string $name
*/
interface SluggableInterface
interface Sluggable
{
/**
* Regenerate the slug for this model.

View File

@@ -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('/'),
]);
}
}

View File

@@ -1,9 +1,6 @@
<?php
use BookStack\App\AppVersion;
use BookStack\App\Model;
use BookStack\Facades\Theme;
use BookStack\Permissions\Permission;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Settings\SettingService;
use BookStack\Users\Models\User;
@@ -15,7 +12,12 @@ use BookStack\Users\Models\User;
*/
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 = '';
if (config('app.env') === 'development') {
@@ -40,9 +42,9 @@ function user(): User
* Check if the current user has a permission. If an ownable element
* 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 ($ownable === null) {
return user()->can($permission);
}
@@ -56,7 +58,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.
* 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);
@@ -68,7 +70,7 @@ function userCanOnAny(string|Permission $action, string $entityClass = ''): bool
*
* @return mixed|SettingService
*/
function setting(?string $key = null, mixed $default = null): mixed
function setting(string $key = null, $default = null)
{
$settingService = app()->make(SettingService::class);
@@ -86,10 +88,43 @@ function setting(?string $key = null, mixed $default = null): mixed
*/
function theme_path(string $path = ''): ?string
{
$theme = Theme::getTheme();
$theme = config('view.theme');
if (!$theme) {
return null;
}
return base_path('themes/' . $theme . ($path ? DIRECTORY_SEPARATOR . $path : $path));
}
/**
* Generate a URL with multiple parameters for sorting purposes.
* Works out the logic to set the correct sorting direction
* Discards empty parameters and allows overriding.
*/
function sortUrl(string $path, array $data, array $overrideData = []): string
{
$queryStringSections = [];
$queryData = array_merge($data, $overrideData);
// Change sorting direction is already sorted on current attribute
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
} elseif (isset($overrideData['sort'])) {
$queryData['order'] = 'asc';
}
foreach ($queryData as $name => $value) {
$trimmedVal = trim($value);
if ($trimmedVal === '') {
continue;
}
$queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
}
if (count($queryStringSections) === 0) {
return url($path);
}
return url($path . '?' . implode('&', $queryStringSections));
}

View File

@@ -70,8 +70,8 @@ return [
// 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.
// Host-specific functionality (usually controlled via other options) like auth
// or user avatars, for example, won't use this list.
// Space separated if multiple. Can use '*' as a wildcard.
// or user avatars for example, won't use this list.
// Space seperated if multiple. Can use '*' as a wildcard.
// Values will be compared prefix-matched, case-insensitive, against called SSR urls.
// Defaults to allow all hosts.
'ssr_hosts' => env('ALLOWED_SSR_HOSTS', '*'),
@@ -80,10 +80,8 @@ return [
// Integer value between 0 (IP hidden) to 4 (Full IP usage)
'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'),
// Application timezone for displayed date/time values in the UI.
'display_timezone' => env('APP_DISPLAY_TIMEZONE', env('APP_TIMEZONE', 'UTC')),
// Default locale to use
// A default variant is also stored since Laravel can overwrite

View File

@@ -0,0 +1,37 @@
<?php
/**
* Broadcasting configuration options.
*
* Changes to these config files are not supported by BookStack and may break upon updates.
* Configuration should be altered via the `.env` file or environment variables.
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
return [
// Default Broadcaster
// This option controls the default broadcaster that will be used by the
// framework when an event needs to be broadcast. This can be set to
// any of the connections defined in the "connections" array below.
'default' => 'null',
// Broadcast Connections
// Here you may define all of the broadcast connections that will be used
// to broadcast events to other systems or over websockets. Samples of
// each available type of connection are provided inside this array.
'connections' => [
// Default options removed since we don't use broadcasting.
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];

View File

@@ -35,6 +35,10 @@ return [
// Available caches stores
'stores' => [
'apc' => [
'driver' => 'apc',
],
'array' => [
'driver' => 'array',
'serialize' => false,
@@ -45,7 +49,6 @@ return [
'table' => 'cache',
'connection' => null,
'lock_connection' => null,
'lock_table' => null,
],
'file' => [
@@ -85,6 +88,6 @@ return [
|
*/
'prefix' => env('CACHE_PREFIX', 'bookstack_cache_'),
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache_'),
];

View File

@@ -40,16 +40,12 @@ if (env('REDIS_SERVERS', false)) {
// MYSQL
// Split out port from host if set
$mysqlHost = env('DB_HOST', 'localhost');
$mysqlHostExploded = explode(':', $mysqlHost);
$mysqlPort = env('DB_PORT', 3306);
$mysqlHostIpv6 = str_starts_with($mysqlHost, '[');
if ($mysqlHostIpv6 && str_contains($mysqlHost, ']:')) {
$mysqlHost = implode(':', array_slice($mysqlHostExploded, 0, -1));
$mysqlPort = intval(end($mysqlHostExploded));
} else if (!$mysqlHostIpv6 && count($mysqlHostExploded) > 1) {
$mysqlHost = $mysqlHostExploded[0];
$mysqlPort = intval($mysqlHostExploded[1]);
$mysql_host = env('DB_HOST', 'localhost');
$mysql_host_exploded = explode(':', $mysql_host);
$mysql_port = env('DB_PORT', 3306);
if (count($mysql_host_exploded) > 1) {
$mysql_host = $mysql_host_exploded[0];
$mysql_port = intval($mysql_host_exploded[1]);
}
return [
@@ -65,17 +61,17 @@ return [
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => $mysqlHost,
'host' => $mysql_host,
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'port' => $mysqlPort,
'port' => $mysql_port,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
// Prefixes are only semi-supported and may be unstable
// 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_indexes' => true,
'strict' => false,
@@ -92,7 +88,7 @@ return [
'database' => 'bookstack-test',
'username' => env('MYSQL_USER', 'bookstack-test'),
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
'port' => $mysqlPort,
'port' => $mysql_port,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
@@ -103,7 +99,9 @@ return [
],
// 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',
// Redis configuration to use if set

View File

@@ -114,7 +114,6 @@ return [
* @var array
*/
'allowed_protocols' => [
"data://" => ["rules" => []],
'file://' => ['rules' => []],
'http://' => ['rules' => []],
'https://' => ['rules' => []],

View File

@@ -32,22 +32,20 @@ return [
'local' => [
'driver' => 'local',
'root' => public_path(),
'serve' => false,
'visibility' => 'public',
'throw' => true,
'directory_visibility' => 'public',
],
'local_secure_attachments' => [
'driver' => 'local',
'root' => storage_path('uploads/files/'),
'serve' => false,
'throw' => true,
],
'local_secure_images' => [
'driver' => 'local',
'root' => storage_path('uploads/images/'),
'serve' => false,
'visibility' => 'public',
'throw' => true,
],

View File

@@ -11,7 +11,6 @@
// Configured mail encryption method.
// STARTTLS should still be attempted, but tls/ssl forces TLS usage.
$mailEncryption = env('MAIL_ENCRYPTION', null);
$mailPort = intval(env('MAIL_PORT', 587));
return [
@@ -34,13 +33,13 @@ return [
'transport' => 'smtp',
'scheme' => null,
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'port' => $mailPort,
'port' => env('MAIL_PORT', 587),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'verify_peer' => env('MAIL_VERIFY_SSL', true),
'timeout' => null,
'local_domain' => null,
'require_tls' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl' || $mailPort === 465),
'local_domain' => env('MAIL_EHLO_DOMAIN'),
'tls_required' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl'),
],
'sendmail' => [
@@ -65,4 +64,12 @@ return [
],
],
],
// Email markdown configuration
'markdown' => [
'theme' => 'default',
'paths' => [
resource_path('views/vendor/mail'),
],
],
];

View File

@@ -47,12 +47,6 @@ return [
// Multiple values can be provided comma seperated.
'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
// Enable syncing, upon login, of OIDC groups to BookStack roles
'user_to_groups' => env('OIDC_USER_TO_GROUPS', false),

View File

@@ -23,7 +23,6 @@ return [
'database' => [
'driver' => 'database',
'connection' => null,
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,

View File

@@ -1,99 +0,0 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Entities\Models\Book;
use BookStack\Sorting\BookSorter;
use BookStack\Sorting\SortRule;
use Illuminate\Console\Command;
class AssignSortRuleCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:assign-sort-rule
{sort-rule=0: ID of the sort rule to apply}
{--all-books : Apply to all books in the system}
{--books-without-sort : Apply to only books without a sort rule already assigned}
{--books-with-sort= : Apply to only books with the sort rule of given id}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Assign a sort rule to content in the system';
/**
* Execute the console command.
*/
public function handle(BookSorter $sorter): int
{
$sortRuleId = intval($this->argument('sort-rule')) ?? 0;
if ($sortRuleId === 0) {
return $this->listSortRules();
}
$rule = SortRule::query()->find($sortRuleId);
if ($this->option('all-books')) {
$query = Book::query();
} else if ($this->option('books-without-sort')) {
$query = Book::query()->whereNull('sort_rule_id');
} else if ($this->option('books-with-sort')) {
$sortId = intval($this->option('books-with-sort')) ?: 0;
if (!$sortId) {
$this->error("Provided --books-with-sort option value is invalid");
return 1;
}
$query = Book::query()->where('sort_rule_id', $sortId);
} else {
$this->error("No option provided to specify target. Run with the -h option to see all available options.");
return 1;
}
if (!$rule) {
$this->error("Sort rule of provided id {$sortRuleId} not found!");
return 1;
}
$count = $query->clone()->count();
$this->warn("This will apply sort rule [{$rule->id}: {$rule->name}] to {$count} book(s) and run the sort on each.");
$confirmed = $this->confirm("Are you sure you want to continue?");
if (!$confirmed) {
return 1;
}
$processed = 0;
$query->chunkById(10, function ($books) use ($rule, $sorter, $count, &$processed) {
$max = min($count, ($processed + 10));
$this->info("Applying to {$processed}-{$max} of {$count} books");
foreach ($books as $book) {
$book->sort_rule_id = $rule->id;
$book->save();
$sorter->runBookAutoSort($book);
}
$processed = $max;
});
$this->info("Sort applied to {$processed} book(s)!");
return 0;
}
protected function listSortRules(): int
{
$rules = SortRule::query()->orderBy('id', 'asc')->get();
$this->error("Sort rule ID required!");
$this->warn("\nAvailable sort rules:");
foreach ($rules as $rule) {
$this->info("{$rule->id}: {$rule->name}");
}
return 1;
}
}

View File

@@ -8,6 +8,7 @@ use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\Rules\Unique;
class CreateAdminCommand extends Command
{
@@ -20,9 +21,7 @@ class CreateAdminCommand extends Command
{--email= : The email address for the new admin user}
{--name= : The name of the new admin user}
{--password= : The password to assign to the new admin user}
{--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}';
{--external-auth-id= : The external authentication system id for the new admin user (SAML2/LDAP/OIDC)}';
/**
* The console command description.
@@ -36,12 +35,26 @@ class CreateAdminCommand extends Command
*/
public function handle(UserRepo $userRepo): int
{
$initialAdminOnly = $this->option('initial');
$shouldGeneratePassword = $this->option('generate-password');
$details = $this->gatherDetails($shouldGeneratePassword, $initialAdminOnly);
$details = $this->snakeCaseOptions();
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, [
'email' => ['required', 'email', 'min:5'],
'email' => ['required', 'email', 'min:5', new Unique('users', 'email')],
'name' => ['required', 'min:2'],
'password' => ['required_without:external_auth_id', Password::default()],
'external_auth_id' => ['required_without:password'],
@@ -55,101 +68,16 @@ class CreateAdminCommand extends Command
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->attachRole($adminRole);
$user->attachRole(Role::getSystemRole('admin'));
$user->email_confirmed = true;
$user->save();
if ($shouldGeneratePassword) {
$this->line($details['password']);
} else {
$this->info("Admin account with email \"{$user->email}\" successfully created!");
}
$this->info("Admin account with email \"{$user->email}\" successfully created!");
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
{
$returnOpts = [];

View File

@@ -52,7 +52,7 @@ class UpdateUrlCommand extends Command
'page_revisions' => ['html', 'text', 'markdown'],
'images' => ['url'],
'settings' => ['value'],
'comments' => ['html'],
'comments' => ['html', 'text'],
];
foreach ($columnsToUpdateByTable as $table => $columns) {

View File

@@ -11,7 +11,6 @@ use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
@@ -31,7 +30,6 @@ class BookApiController extends ApiController
{
$books = $this->queries
->visibleForList()
->with(['cover:id,name,url'])
->addSelect(['created_by', 'updated_by']);
return $this->apiListingResponse($books, [
@@ -48,7 +46,7 @@ class BookApiController extends ApiController
*/
public function create(Request $request)
{
$this->checkPermission(Permission::BookCreateAll);
$this->checkPermission('book-create-all');
$requestData = $this->validate($request, $this->rules()['create']);
$book = $this->bookRepo->create($requestData);
@@ -93,7 +91,7 @@ class BookApiController extends ApiController
public function update(Request $request, string $id)
{
$book = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission(Permission::BookUpdate, $book);
$this->checkOwnablePermission('book-update', $book);
$requestData = $this->validate($request, $this->rules()['update']);
$book = $this->bookRepo->update($book, $requestData);
@@ -110,7 +108,7 @@ class BookApiController extends ApiController
public function delete(string $id)
{
$book = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission(Permission::BookDelete, $book);
$this->checkOwnablePermission('book-delete', $book);
$this->bookRepo->destroy($book);

View File

@@ -17,9 +17,7 @@ use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\DatabaseTransaction;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
@@ -72,14 +70,14 @@ class BookController extends Controller
/**
* Show the form for creating a new book.
*/
public function create(?string $shelfSlug = null)
public function create(string $shelfSlug = null)
{
$this->checkPermission(Permission::BookCreateAll);
$this->checkPermission('book-create-all');
$bookshelf = null;
if ($shelfSlug !== null) {
$bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
$this->checkOwnablePermission(Permission::BookshelfUpdate, $bookshelf);
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
$this->setPageTitle(trans('entities.books_create'));
@@ -95,9 +93,9 @@ class BookController extends Controller
* @throws ImageUploadException
* @throws ValidationException
*/
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, [
'name' => ['required', 'string', 'max:255'],
'description_html' => ['string', 'max:2000'],
@@ -109,7 +107,7 @@ class BookController extends Controller
$bookshelf = null;
if ($shelfSlug !== null) {
$bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
$this->checkOwnablePermission(Permission::BookshelfUpdate, $bookshelf);
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
$book = $this->bookRepo->create($validated);
@@ -155,7 +153,7 @@ class BookController extends Controller
public function edit(string $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()]));
return view('books.edit', ['book' => $book, 'current' => $book]);
@@ -171,7 +169,7 @@ class BookController extends Controller
public function update(Request $request, string $slug)
{
$book = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission(Permission::BookUpdate, $book);
$this->checkOwnablePermission('book-update', $book);
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'],
@@ -198,7 +196,7 @@ class BookController extends Controller
public function showDelete(string $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()]));
return view('books.delete', ['book' => $book, 'current' => $book]);
@@ -212,7 +210,7 @@ class BookController extends Controller
public function destroy(string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission(Permission::BookDelete, $book);
$this->checkOwnablePermission('book-delete', $book);
$this->bookRepo->destroy($book);
@@ -227,7 +225,7 @@ class BookController extends Controller
public function showCopy(string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission(Permission::BookView, $book);
$this->checkOwnablePermission('book-view', $book);
session()->flashInput(['name' => $book->name]);
@@ -244,8 +242,8 @@ class BookController extends Controller
public function copy(Request $request, Cloner $cloner, string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission(Permission::BookView, $book);
$this->checkPermission(Permission::BookCreateAll);
$this->checkOwnablePermission('book-view', $book);
$this->checkPermission('book-create-all');
$newName = $request->get('name') ?: $book->name;
$bookCopy = $cloner->cloneBook($book, $newName);
@@ -260,14 +258,12 @@ class BookController extends Controller
public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission(Permission::BookUpdate, $book);
$this->checkOwnablePermission(Permission::BookDelete, $book);
$this->checkPermission(Permission::BookshelfCreateAll);
$this->checkPermission(Permission::BookCreateAll);
$this->checkOwnablePermission('book-update', $book);
$this->checkOwnablePermission('book-delete', $book);
$this->checkPermission('bookshelf-create-all');
$this->checkPermission('book-create-all');
$shelf = (new DatabaseTransaction(function () use ($book, $transformer) {
return $transformer->transformBookToShelf($book);
}))->run();
$shelf = $transformer->transformBookToShelf($book);
return redirect($shelf->getUrl());
}

View File

@@ -1,12 +1,10 @@
<?php
namespace BookStack\Exports\Controllers;
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Queries\BookQueries;
use BookStack\Exports\ExportFormatter;
use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Throwable;
class BookExportApiController extends ApiController
@@ -15,7 +13,7 @@ class BookExportApiController extends ApiController
protected ExportFormatter $exportFormatter,
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');
}
/**
* 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);
}
}

View File

@@ -1,13 +1,10 @@
<?php
namespace BookStack\Exports\Controllers;
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Queries\BookQueries;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exports\ExportFormatter;
use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use Throwable;
class BookExportController extends Controller
@@ -16,8 +13,7 @@ class BookExportController extends Controller
protected BookQueries $queries,
protected ExportFormatter $exportFormatter,
) {
$this->middleware(Permission::ContentExport->middleware());
$this->middleware('throttle:exports');
$this->middleware('can:content-export');
}
/**
@@ -67,16 +63,4 @@ class BookExportController extends Controller
return $this->download()->directly($textContent, $bookSlug . '.md');
}
/**
* Export a book to a contained ZIP export file.
* @throws NotFoundException
*/
public function zip(string $bookSlug, ZipExportBuilder $builder)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$zip = $builder->buildForBook($book);
return $this->download()->streamedFileDirectly($zip, $bookSlug . '.zip', true);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Activity\ActivityType;
use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\BookSortMap;
use BookStack\Facades\Activity;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
class BookSortController extends Controller
{
public function __construct(
protected BookQueries $queries,
) {
}
/**
* Shows the view which allows pages to be re-ordered and sorted.
*/
public function show(string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = (new BookContents($book))->getTree(false);
$this->setPageTitle(trans('entities.books_sort_named', ['bookName' => $book->getShortName()]));
return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
}
/**
* Shows the sort box for a single book.
* Used via AJAX when loading in extra books to a sort.
*/
public function showItem(string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$bookChildren = (new BookContents($book))->getTree();
return view('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
}
/**
* Sorts a book using a given mapping array.
*/
public function update(Request $request, string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-update', $book);
// Return if no map sent
if (!$request->filled('sort-tree')) {
return redirect($book->getUrl());
}
$sortMap = BookSortMap::fromJson($request->get('sort-tree'));
$bookContents = new BookContents($book);
$booksInvolved = $bookContents->sortUsingMap($sortMap);
// Rebuild permissions and add activity for involved books.
foreach ($booksInvolved as $bookInvolved) {
Activity::add(ActivityType::BOOK_SORT, $bookInvolved);
}
return redirect($book->getUrl());
}
}

View File

@@ -6,7 +6,6 @@ use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Http\Request;
@@ -27,7 +26,6 @@ class BookshelfApiController extends ApiController
{
$shelves = $this->queries
->visibleForList()
->with(['cover:id,name,url'])
->addSelect(['created_by', 'updated_by']);
return $this->apiListingResponse($shelves, [
@@ -46,7 +44,7 @@ class BookshelfApiController extends ApiController
*/
public function create(Request $request)
{
$this->checkPermission(Permission::BookshelfCreateAll);
$this->checkPermission('bookshelf-create-all');
$requestData = $this->validate($request, $this->rules()['create']);
$bookIds = $request->get('books', []);
@@ -85,7 +83,7 @@ class BookshelfApiController extends ApiController
public function update(Request $request, string $id)
{
$shelf = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$requestData = $this->validate($request, $this->rules()['update']);
$bookIds = $request->get('books', null);
@@ -104,7 +102,7 @@ class BookshelfApiController extends ApiController
public function delete(string $id)
{
$shelf = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission(Permission::BookshelfDelete, $shelf);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->bookshelfRepo->destroy($shelf);

View File

@@ -11,7 +11,6 @@ use BookStack\Entities\Tools\ShelfContext;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\SimpleListOptions;
use Exception;
@@ -69,7 +68,7 @@ class BookshelfController extends Controller
*/
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']);
$this->setPageTitle(trans('entities.shelves_create'));
@@ -84,7 +83,7 @@ class BookshelfController extends Controller
*/
public function store(Request $request)
{
$this->checkPermission(Permission::BookshelfCreateAll);
$this->checkPermission('bookshelf-create-all');
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'],
'description_html' => ['string', 'max:2000'],
@@ -106,7 +105,7 @@ class BookshelfController extends Controller
public function show(Request $request, ActivityQueries $activities, string $slug)
{
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission(Permission::BookshelfView, $shelf);
$this->checkOwnablePermission('bookshelf-view', $shelf);
$listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([
'default' => trans('common.sort_default'),
@@ -144,7 +143,7 @@ class BookshelfController extends Controller
public function edit(string $slug)
{
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
$books = $this->bookQueries->visibleForList()
@@ -170,7 +169,7 @@ class BookshelfController extends Controller
public function update(Request $request, string $slug)
{
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'],
'description_html' => ['string', 'max:2000'],
@@ -196,7 +195,7 @@ class BookshelfController extends Controller
public function showDelete(string $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()]));
@@ -211,7 +210,7 @@ class BookshelfController extends Controller
public function destroy(string $slug)
{
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission(Permission::BookshelfDelete, $shelf);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->shelfRepo->destroy($shelf);

View File

@@ -2,20 +2,19 @@
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Request;
class ChapterApiController extends ApiController
{
protected array $rules = [
protected $rules = [
'create' => [
'book_id' => ['required', 'integer'],
'name' => ['required', 'string', 'max:255'],
@@ -66,7 +65,7 @@ class ChapterApiController extends ApiController
$bookId = $request->get('book_id');
$book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId));
$this->checkOwnablePermission(Permission::ChapterCreate, $book);
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($requestData, $book);
@@ -102,10 +101,10 @@ class ChapterApiController extends ApiController
{
$requestData = $this->validate($request, $this->rules()['update']);
$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'])) {
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
try {
$this->chapterRepo->move($chapter, "book:{$requestData['book_id']}");
@@ -130,7 +129,7 @@ class ChapterApiController extends ApiController
public function delete(string $id)
{
$chapter = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter);
@@ -145,10 +144,7 @@ class ChapterApiController extends ApiController
$chapter->load(['tags']);
$chapter->makeVisible('description_html');
$chapter->setAttribute('description_html', $chapter->descriptionHtml());
/** @var Book $book */
$book = $chapter->book()->first();
$chapter->setAttribute('book_slug', $book->slug);
$chapter->setAttribute('book_slug', $chapter->book()->first()->slug);
return $chapter;
}

View File

@@ -17,9 +17,7 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\DatabaseTransaction;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
@@ -40,7 +38,7 @@ class ChapterController extends Controller
public function create(string $bookSlug)
{
$book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission(Permission::ChapterCreate, $book);
$this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle(trans('entities.chapters_create'));
@@ -65,7 +63,7 @@ class ChapterController extends Controller
]);
$book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission(Permission::ChapterCreate, $book);
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($validated, $book);
@@ -78,6 +76,7 @@ class ChapterController extends Controller
public function show(string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
$sidebarTree = (new BookContents($chapter->book))->getTree();
$pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get();
@@ -106,7 +105,7 @@ class ChapterController extends Controller
public function edit(string $bookSlug, string $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()]));
@@ -128,7 +127,7 @@ class ChapterController extends Controller
]);
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->chapterRepo->update($chapter, $validated);
@@ -143,7 +142,7 @@ class ChapterController extends Controller
public function showDelete(string $bookSlug, string $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()]));
@@ -159,7 +158,7 @@ class ChapterController extends Controller
public function destroy(string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter);
@@ -175,8 +174,8 @@ class ChapterController extends Controller
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
return view('chapters.move', [
'chapter' => $chapter,
@@ -192,8 +191,8 @@ class ChapterController extends Controller
public function move(Request $request, string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
@@ -221,6 +220,7 @@ class ChapterController extends Controller
public function showCopy(string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
session()->flashInput(['name' => $chapter->name]);
@@ -239,6 +239,7 @@ class ChapterController extends Controller
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
$entitySelection = $request->get('entity_selection') ?: null;
$newParentBook = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $chapter->getParent();
@@ -249,7 +250,7 @@ class ChapterController extends Controller
return redirect($chapter->getUrl('/copy'));
}
$this->checkOwnablePermission(Permission::ChapterCreate, $newParentBook);
$this->checkOwnablePermission('chapter-create', $newParentBook);
$newName = $request->get('name') ?: $chapter->name;
$chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName);
@@ -264,13 +265,11 @@ class ChapterController extends Controller
public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
$this->checkPermission(Permission::BookCreateAll);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->checkPermission('book-create-all');
$book = (new DatabaseTransaction(function () use ($chapter, $transformer) {
return $transformer->transformChapterToBook($chapter);
}))->run();
$book = $transformer->transformChapterToBook($chapter);
return redirect($book->getUrl());
}

View File

@@ -1,12 +1,10 @@
<?php
namespace BookStack\Exports\Controllers;
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Exports\ExportFormatter;
use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Throwable;
class ChapterExportApiController extends ApiController
@@ -15,7 +13,7 @@ class ChapterExportApiController extends ApiController
protected ExportFormatter $exportFormatter,
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');
}
/**
* 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);
}
}

View File

@@ -1,13 +1,11 @@
<?php
namespace BookStack\Exports\Controllers;
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exports\ExportFormatter;
use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use Throwable;
class ChapterExportController extends Controller
@@ -16,8 +14,7 @@ class ChapterExportController extends Controller
protected ChapterQueries $queries,
protected ExportFormatter $exportFormatter,
) {
$this->middleware(Permission::ContentExport->middleware());
$this->middleware('throttle:exports');
$this->middleware('can:content-export');
}
/**
@@ -73,16 +70,4 @@ class ChapterExportController extends Controller
return $this->download()->directly($chapterText, $chapterSlug . '.md');
}
/**
* Export a book to a contained ZIP export file.
* @throws NotFoundException
*/
public function zip(string $bookSlug, string $chapterSlug, ZipExportBuilder $builder)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$zip = $builder->buildForChapter($chapter);
return $this->download()->streamedFileDirectly($zip, $chapterSlug . '.zip', true);
}
}

View File

@@ -7,13 +7,12 @@ use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Exception;
use Illuminate\Http\Request;
class PageApiController extends ApiController
{
protected array $rules = [
protected $rules = [
'create' => [
'book_id' => ['required_without:chapter_id', 'integer'],
'chapter_id' => ['required_without:book_id', 'integer'],
@@ -77,7 +76,7 @@ class PageApiController extends ApiController
} else {
$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);
$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']);
$page = $this->queries->findVisibleByIdOrFail($id);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->checkOwnablePermission('page-update', $page);
$parent = null;
if ($request->has('chapter_id')) {
@@ -127,7 +126,7 @@ class PageApiController extends ApiController
}
if ($parent && !$parent->matches($page->getParent())) {
$this->checkOwnablePermission(Permission::PageDelete, $page);
$this->checkOwnablePermission('page-delete', $page);
try {
$this->pageRepo->move($page, $parent->getType() . ':' . $parent->id);
@@ -152,7 +151,7 @@ class PageApiController extends ApiController
public function delete(string $id)
{
$page = $this->queries->findVisibleByIdOrFail($id);
$this->checkOwnablePermission(Permission::PageDelete, $page);
$this->checkOwnablePermission('page-delete', $page);
$this->pageRepo->destroy($page);

View File

@@ -17,10 +17,8 @@ use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditActivity;
use BookStack\Entities\Tools\PageEditorData;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\References\ReferenceFetcher;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -43,7 +41,7 @@ class PageController extends Controller
*
* @throws Throwable
*/
public function create(string $bookSlug, ?string $chapterSlug = null)
public function create(string $bookSlug, string $chapterSlug = null)
{
if ($chapterSlug) {
$parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
@@ -51,7 +49,7 @@ class PageController extends Controller
$parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
}
$this->checkOwnablePermission(Permission::PageCreate, $parent);
$this->checkOwnablePermission('page-create', $parent);
// Redirect to draft edit screen if signed in
if ($this->isSignedIn()) {
@@ -71,7 +69,7 @@ class PageController extends Controller
*
* @throws ValidationException
*/
public function createAsGuest(Request $request, string $bookSlug, ?string $chapterSlug = null)
public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
{
$this->validate($request, [
'name' => ['required', 'string', 'max:255'],
@@ -83,7 +81,7 @@ class PageController extends Controller
$parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
}
$this->checkOwnablePermission(Permission::PageCreate, $parent);
$this->checkOwnablePermission('page-create', $parent);
$page = $this->pageRepo->getNewDraftPage($parent);
$this->pageRepo->publishDraft($page, [
@@ -101,7 +99,7 @@ class PageController extends Controller
public function editDraft(Request $request, string $bookSlug, int $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', ''));
$this->setPageTitle(trans('entities.pages_edit_draft'));
@@ -121,7 +119,7 @@ class PageController extends Controller
'name' => ['required', 'string', 'max:255'],
]);
$draftPage = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission(Permission::PageCreate, $draftPage->getParent());
$this->checkOwnablePermission('page-create', $draftPage->getParent());
$page = $this->pageRepo->publishDraft($draftPage, $request->all());
@@ -149,6 +147,8 @@ class PageController extends Controller
return redirect($page->getUrl());
}
$this->checkOwnablePermission('page-view', $page);
$pageContent = (new PageContent($page));
$page->html = $pageContent->render();
$pageNav = $pageContent->getNavigation($page->html);
@@ -196,7 +196,7 @@ class PageController extends Controller
public function edit(Request $request, string $bookSlug, string $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', ''));
if ($editorData->getWarnings()) {
@@ -220,7 +220,7 @@ class PageController extends Controller
'name' => ['required', 'string', 'max:255'],
]);
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->checkOwnablePermission('page-update', $page);
$this->pageRepo->update($page, $request->all());
@@ -235,7 +235,7 @@ class PageController extends Controller
public function saveDraft(Request $request, int $pageId)
{
$page = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->checkOwnablePermission('page-update', $page);
if (!$this->isSignedIn()) {
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)
{
$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()]));
$usedAsTemplate =
$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)
{
$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()]));
$usedAsTemplate =
$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)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission(Permission::PageDelete, $page);
$this->checkOwnablePermission('page-delete', $page);
$parent = $page->getParent();
$this->pageRepo->destroy($page);
@@ -336,13 +336,13 @@ class PageController extends Controller
$page = $this->queries->findVisibleByIdOrFail($pageId);
$book = $page->book;
$chapter = $page->chapter;
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->checkOwnablePermission('page-update', $page);
$this->pageRepo->destroy($page);
$this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
if ($chapter && userCan(Permission::ChapterView, $chapter)) {
if ($chapter && userCan('view', $chapter)) {
return redirect($chapter->getUrl());
}
@@ -383,8 +383,8 @@ class PageController extends Controller
public function showMove(string $bookSlug, string $pageSlug)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->checkOwnablePermission(Permission::PageDelete, $page);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
return view('pages.move', [
'book' => $page->book,
@@ -401,8 +401,8 @@ class PageController extends Controller
public function move(Request $request, string $bookSlug, string $pageSlug)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->checkOwnablePermission(Permission::PageDelete, $page);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
@@ -430,6 +430,7 @@ class PageController extends Controller
public function showCopy(string $bookSlug, string $pageSlug)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]);
return view('pages.copy', [
@@ -447,7 +448,7 @@ class PageController extends Controller
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission(Permission::PageView, $page);
$this->checkOwnablePermission('page-view', $page);
$entitySelection = $request->get('entity_selection') ?: null;
$newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent();
@@ -458,7 +459,7 @@ class PageController extends Controller
return redirect($page->getUrl('/copy'));
}
$this->checkOwnablePermission(Permission::PageCreate, $newParent);
$this->checkOwnablePermission('page-create', $newParent);
$newName = $request->get('name') ?: $page->name;
$pageCopy = $cloner->clonePage($page, $newParent, $newName);

View File

@@ -1,12 +1,10 @@
<?php
namespace BookStack\Exports\Controllers;
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Exports\ExportFormatter;
use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Throwable;
class PageExportApiController extends ApiController
@@ -15,7 +13,7 @@ class PageExportApiController extends ApiController
protected ExportFormatter $exportFormatter,
protected PageQueries $queries,
) {
$this->middleware(Permission::ContentExport->middleware());
$this->middleware('can:content-export');
}
/**
@@ -65,15 +63,4 @@ class PageExportApiController extends ApiController
return $this->download()->directly($markdown, $page->slug . '.md');
}
/**
* Export a page as a contained ZIP file.
*/
public function exportZip(int $id, ZipExportBuilder $builder)
{
$page = $this->queries->findVisibleByIdOrFail($id);
$zip = $builder->buildForPage($page);
return $this->download()->streamedFileDirectly($zip, $page->slug . '.zip', true);
}
}

View File

@@ -1,14 +1,12 @@
<?php
namespace BookStack\Exports\Controllers;
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Tools\PageContent;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exports\ExportFormatter;
use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use Throwable;
class PageExportController extends Controller
@@ -17,8 +15,7 @@ class PageExportController extends Controller
protected PageQueries $queries,
protected ExportFormatter $exportFormatter,
) {
$this->middleware(Permission::ContentExport->middleware());
$this->middleware('throttle:exports');
$this->middleware('can:content-export');
}
/**
@@ -77,16 +74,4 @@ class PageExportController extends Controller
return $this->download()->directly($pageText, $pageSlug . '.md');
}
/**
* Export a page to a contained ZIP export file.
* @throws NotFoundException
*/
public function zip(string $bookSlug, string $pageSlug, ZipExportBuilder $builder)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$zip = $builder->buildForPage($page);
return $this->download()->streamedFileDirectly($zip, $pageSlug . '.zip', true);
}
}

View File

@@ -11,7 +11,6 @@ use BookStack\Entities\Tools\PageContent;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Ssddanbrown\HtmlDiff\Diff;
@@ -44,6 +43,7 @@ class PageRevisionController extends Controller
->selectRaw("IF(markdown = '', false, true) as is_markdown")
->with(['page.book', 'createdBy'])
->reorder('id', $listOptions->getOrder())
->reorder('created_at', $listOptions->getOrder())
->paginate(50);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName' => $page->getShortName()]));
@@ -52,7 +52,6 @@ class PageRevisionController extends Controller
'revisions' => $revisions,
'page' => $page,
'listOptions' => $listOptions,
'oldestRevisionId' => $page->revisions()->min('id'),
]);
}
@@ -99,7 +98,7 @@ class PageRevisionController extends Controller
throw new NotFoundException();
}
$prev = $revision->getPreviousRevision();
$prev = $revision->getPrevious();
$prevContent = $prev->html ?? '';
$diff = Diff::excecute($prevContent, $revision->html);
@@ -125,7 +124,7 @@ class PageRevisionController extends Controller
public function restore(string $bookSlug, string $pageSlug, int $revisionId)
{
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->checkOwnablePermission('page-update', $page);
$page = $this->pageRepo->restoreRevision($page, $revisionId);
@@ -140,7 +139,7 @@ class PageRevisionController extends Controller
public function destroy(string $bookSlug, string $pageSlug, int $revId)
{
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission(Permission::PageDelete, $page);
$this->checkOwnablePermission('page-delete', $page);
$revision = $page->revisions()->where('id', '=', $revId)->first();
if ($revision === null) {

View File

@@ -6,20 +6,18 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Deletion;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\DeletionRepo;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
class RecycleBinApiController extends ApiController
{
public function __construct()
{
$this->middleware(function ($request, $next) {
$this->checkPermission(Permission::SettingsManage);
$this->checkPermission(Permission::RestrictionsManageAll);
$this->checkPermission('settings-manage');
$this->checkPermission('restrictions-manage-all');
return $next($request);
});
@@ -42,7 +40,7 @@ class RecycleBinApiController extends ApiController
'updated_at',
'deletable_type',
'deletable_id',
], [$this->listFormatter(...)]);
], [Closure::fromCallable([$this, 'listFormatter'])]);
}
/**
@@ -71,9 +69,10 @@ class RecycleBinApiController extends ApiController
/**
* Load some related details for the deletion listing.
*/
protected function listFormatter(Deletion $deletion): void
protected function listFormatter(Deletion $deletion)
{
$deletable = $deletion->deletable;
$withTrashedQuery = fn (Builder $query) => $query->withTrashed();
if ($deletable instanceof BookChild) {
$parent = $deletable->getParent();
@@ -82,19 +81,11 @@ class RecycleBinApiController extends ApiController
}
if ($deletable instanceof Book || $deletable instanceof Chapter) {
$countsToLoad = ['pages' => static::withTrashedQuery(...)];
$countsToLoad = ['pages' => $withTrashedQuery];
if ($deletable instanceof Book) {
$countsToLoad['chapters'] = static::withTrashedQuery(...);
$countsToLoad['chapters'] = $withTrashedQuery;
}
$deletable->loadCount($countsToLoad);
}
}
/**
* @param Builder<Chapter|Page> $query
*/
protected static function withTrashedQuery(Builder $query): void
{
$query->withTrashed();
}
}

View File

@@ -8,7 +8,6 @@ use BookStack\Entities\Models\Entity;
use BookStack\Entities\Repos\DeletionRepo;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
class RecycleBinController extends Controller
{
@@ -21,8 +20,8 @@ class RecycleBinController extends Controller
public function __construct()
{
$this->middleware(function ($request, $next) {
$this->checkPermission(Permission::SettingsManage);
$this->checkPermission(Permission::RestrictionsManageAll);
$this->checkPermission('settings-manage');
$this->checkPermission('restrictions-manage-all');
return $next($request);
});

View File

@@ -2,7 +2,6 @@
namespace BookStack\Entities\Models;
use BookStack\Sorting\SortRule;
use BookStack\Uploads\Image;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -17,19 +16,17 @@ use Illuminate\Support\Collection;
* @property string $description
* @property int $image_id
* @property ?int $default_template_id
* @property ?int $sort_rule_id
* @property Image|null $cover
* @property \Illuminate\Database\Eloquent\Collection $chapters
* @property \Illuminate\Database\Eloquent\Collection $pages
* @property \Illuminate\Database\Eloquent\Collection $directPages
* @property \Illuminate\Database\Eloquent\Collection $shelves
* @property ?Page $defaultTemplate
* @property ?SortRule $sortRule
*/
class Book extends Entity implements CoverImageInterface, HtmlDescriptionInterface
class Book extends Entity implements HasCoverImage
{
use HasFactory;
use HtmlDescriptionTrait;
use HasHtmlDescription;
public float $searchFactor = 1.2;
@@ -85,17 +82,8 @@ class Book extends Entity implements CoverImageInterface, HtmlDescriptionInterfa
return $this->belongsTo(Page::class, 'default_template_id');
}
/**
* Get the sort set assigned to this book, if existing.
*/
public function sortRule(): BelongsTo
{
return $this->belongsTo(SortRule::class);
}
/**
* Get all pages within this book.
* @return HasMany<Page, $this>
*/
public function pages(): HasMany
{
@@ -112,7 +100,6 @@ class Book extends Entity implements CoverImageInterface, HtmlDescriptionInterfa
/**
* Get all chapters within this book.
* @return HasMany<Chapter, $this>
*/
public function chapters(): HasMany
{

View File

@@ -8,10 +8,10 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Bookshelf extends Entity implements CoverImageInterface, HtmlDescriptionInterface
class Bookshelf extends Entity implements HasCoverImage
{
use HasFactory;
use HtmlDescriptionTrait;
use HasHtmlDescription;
protected $table = 'bookshelves';
@@ -70,7 +70,6 @@ class Bookshelf extends Entity implements CoverImageInterface, HtmlDescriptionIn
/**
* Get the cover image of the shelf.
* @return BelongsTo<Image, $this>
*/
public function cover(): BelongsTo
{

View File

@@ -14,10 +14,10 @@ use Illuminate\Support\Collection;
* @property ?int $default_template_id
* @property ?Page $defaultTemplate
*/
class Chapter extends BookChild implements HtmlDescriptionInterface
class Chapter extends BookChild
{
use HasFactory;
use HtmlDescriptionTrait;
use HasHtmlDescription;
public float $searchFactor = 1.2;
@@ -27,7 +27,7 @@ class Chapter extends BookChild implements HtmlDescriptionInterface
/**
* Get the pages that this chapter contains.
*
* @return HasMany<Page, $this>
* @return HasMany<Page>
*/
public function pages(string $dir = 'ASC'): HasMany
{
@@ -60,7 +60,6 @@ class Chapter extends BookChild implements HtmlDescriptionInterface
/**
* Get the visible pages in this chapter.
* @return Collection<Page>
*/
public function getVisiblePages(): Collection
{

View File

@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
* A model that can be deleted in a manner that deletions
* are tracked to be part of the recycle bin system.
*/
interface DeletableInterface
interface Deletable
{
public function deletions(): MorphMany;
}

View File

@@ -13,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
* @property int $deleted_by
* @property string $deletable_type
* @property int $deletable_id
* @property DeletableInterface $deletable
* @property Deletable $deletable
*/
class Deletion extends Model implements Loggable
{

View File

@@ -12,7 +12,7 @@ use BookStack\Activity\Models\View;
use BookStack\Activity\Models\Viewable;
use BookStack\Activity\Models\Watch;
use BookStack\App\Model;
use BookStack\App\SluggableInterface;
use BookStack\App\Sluggable;
use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Permissions\JointPermissionBuilder;
use BookStack\Permissions\Models\EntityPermission;
@@ -22,12 +22,10 @@ use BookStack\References\Reference;
use BookStack\Search\SearchIndex;
use BookStack\Search\SearchTerm;
use BookStack\Users\Models\HasCreatorAndUpdater;
use BookStack\Users\Models\OwnableInterface;
use BookStack\Users\Models\User;
use BookStack\Users\Models\HasOwner;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -44,23 +42,17 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property Carbon $deleted_at
* @property int $created_by
* @property int $updated_by
* @property int $owned_by
* @property Collection $tags
*
* @method static Entity|Builder visible()
* @method static Builder withLastView()
* @method static Builder withViewCount()
*/
abstract class Entity extends Model implements
SluggableInterface,
Favouritable,
Viewable,
DeletableInterface,
OwnableInterface,
Loggable
abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, Deletable, Loggable
{
use SoftDeletes;
use HasCreatorAndUpdater;
use HasOwner;
/**
* @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');
}
/**
* 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.
*/
@@ -305,14 +283,10 @@ abstract class Entity extends Model implements
public function getParent(): ?self
{
if ($this instanceof Page) {
/** @var BelongsTo<Chapter|Book, Page> $builder */
$builder = $this->chapter_id ? $this->chapter() : $this->book();
return $builder->withTrashed()->first();
return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
}
if ($this instanceof Chapter) {
/** @var BelongsTo<Book, Page> $builder */
$builder = $this->book();
return $builder->withTrashed()->first();
return $this->book()->withTrashed()->first();
}
return null;
@@ -321,7 +295,7 @@ abstract class Entity extends Model implements
/**
* Rebuild the permissions for this entity.
*/
public function rebuildPermissions(): void
public function rebuildPermissions()
{
app()->make(JointPermissionBuilder::class)->rebuildForEntity(clone $this);
}
@@ -329,7 +303,7 @@ abstract class Entity extends Model implements
/**
* Index the current entity for search.
*/
public function indexForSearch(): void
public function indexForSearch()
{
app()->make(SearchIndex::class)->indexEntity(clone $this);
}
@@ -339,7 +313,7 @@ abstract class Entity extends Model implements
*/
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;
}

View File

@@ -4,7 +4,7 @@ namespace BookStack\Entities\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
interface CoverImageInterface
interface HasCoverImage
{
/**
* Get the cover image for this item.

View 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);
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}
}

View File

@@ -60,7 +60,7 @@ class PageRevision extends Model implements Loggable
/**
* 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)
->where('id', '<', $this->id)

View File

@@ -6,9 +6,6 @@ use BookStack\Entities\Models\Book;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Database\Eloquent\Builder;
/**
* @implements ProvidesEntityQueries<Book>
*/
class BookQueries implements ProvidesEntityQueries
{
protected static array $listAttributes = [
@@ -16,9 +13,6 @@ class BookQueries implements ProvidesEntityQueries
'created_at', 'updated_at', 'image_id', 'owned_by',
];
/**
* @return Builder<Book>
*/
public function start(): Builder
{
return Book::query();

View File

@@ -6,9 +6,6 @@ use BookStack\Entities\Models\Bookshelf;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Database\Eloquent\Builder;
/**
* @implements ProvidesEntityQueries<Bookshelf>
*/
class BookshelfQueries implements ProvidesEntityQueries
{
protected static array $listAttributes = [
@@ -16,9 +13,6 @@ class BookshelfQueries implements ProvidesEntityQueries
'created_at', 'updated_at', 'image_id', 'owned_by',
];
/**
* @return Builder<Bookshelf>
*/
public function start(): Builder
{
return Bookshelf::query();

View File

@@ -6,9 +6,6 @@ use BookStack\Entities\Models\Chapter;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Database\Eloquent\Builder;
/**
* @implements ProvidesEntityQueries<Chapter>
*/
class ChapterQueries implements ProvidesEntityQueries
{
protected static array $listAttributes = [

View File

@@ -35,7 +35,6 @@ class EntityQueries
/**
* Start a query of visible entities of the given type,
* suitable for listing display.
* @return Builder<Entity>
*/
public function visibleForList(string $entityType): Builder
{
@@ -45,6 +44,7 @@ class EntityQueries
protected function getQueriesForType(string $type): ProvidesEntityQueries
{
/** @var ?ProvidesEntityQueries $queries */
$queries = match ($type) {
'page' => $this->pages,
'chapter' => $this->chapters,

View File

@@ -6,9 +6,6 @@ use BookStack\Entities\Models\Page;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Database\Eloquent\Builder;
/**
* @implements ProvidesEntityQueries<Page>
*/
class PageQueries implements ProvidesEntityQueries
{
protected static array $contentAttributes = [
@@ -21,9 +18,6 @@ class PageQueries implements ProvidesEntityQueries
'template', 'text', 'created_at', 'updated_at', 'priority', 'owned_by',
];
/**
* @return Builder<Page>
*/
public function start(): Builder
{
return Page::query();
@@ -72,9 +66,6 @@ class PageQueries implements ProvidesEntityQueries
});
}
/**
* @return Builder<Page>
*/
public function visibleForList(): Builder
{
return $this->start()

View File

@@ -7,32 +7,28 @@ use Illuminate\Database\Eloquent\Builder;
/**
* 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.
* Any added methods should return a builder instances to allow extension
* via building on the query, unless the method starts with 'find'
* in which case an entity object should be returned.
* (nullable unless it's a *OrFail method).
*
* @template TModel of Entity
*/
interface ProvidesEntityQueries
{
/**
* Start a new query for this entity type.
* @return Builder<TModel>
*/
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;
/**
* Start a query for items that are visible, with selection
* configured for list display of this item.
* @return Builder<TModel>
*/
public function visibleForList(): Builder;
}

Some files were not shown because too many files have changed in this diff Show More