mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-11 11:19:38 +03:00
Compare commits
466 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70ee636d87 | ||
|
|
b35f6dbb03 | ||
|
|
2ea7e10923 | ||
|
|
e7f8188ee5 | ||
|
|
67d9e24d8f | ||
|
|
314d98abc3 | ||
|
|
b2dfc069c5 | ||
|
|
69c50b9b48 | ||
|
|
f101e4f010 | ||
|
|
005f0eb4fc | ||
|
|
7293ad7b24 | ||
|
|
c5b58d8fd2 | ||
|
|
4db2c274e2 | ||
|
|
cbff801aec | ||
|
|
d5d83da766 | ||
|
|
de6d8a811c | ||
|
|
a94844b6b7 | ||
|
|
968e7b8b72 | ||
|
|
5f461c9796 | ||
|
|
b2b8706d05 | ||
|
|
2d345fe454 | ||
|
|
7bec70d429 | ||
|
|
60710e7fb4 | ||
|
|
f10d3a8564 | ||
|
|
b020520366 | ||
|
|
52010e5820 | ||
|
|
75eca03b05 | ||
|
|
b8ebefd803 | ||
|
|
168c66815e | ||
|
|
26e703a1cd | ||
|
|
c8be214ee0 | ||
|
|
2060cdb931 | ||
|
|
0528e98d9c | ||
|
|
3903fda6ca | ||
|
|
441e46ebaa | ||
|
|
f99c8ff99a | ||
|
|
88f93f76dd | ||
|
|
e5fc6bf5fa | ||
|
|
8990a9817f | ||
|
|
1f4260f359 | ||
|
|
dc0bf8ad4e | ||
|
|
75981c2412 | ||
|
|
102e326e6a | ||
|
|
2b25bf6f3b | ||
|
|
f8ae4c335e | ||
|
|
5570e858e5 | ||
|
|
1859a4d356 | ||
|
|
ad4642c2c4 | ||
|
|
92108d710d | ||
|
|
f93280696d | ||
|
|
1787391b07 | ||
|
|
44347ee353 | ||
|
|
9e704fcae4 | ||
|
|
fdd816b17d | ||
|
|
82e2c523e6 | ||
|
|
a323b0d49c | ||
|
|
4c985aac7e | ||
|
|
87e18b8068 | ||
|
|
607a2c91fc | ||
|
|
fde970ba59 | ||
|
|
ec7be1b08b | ||
|
|
746a760a23 | ||
|
|
1a09d88891 | ||
|
|
46c01ecba2 | ||
|
|
544ece03a5 | ||
|
|
5fee7c4db1 | ||
|
|
8ed9f75d57 | ||
|
|
a15b179676 | ||
|
|
73844b9eeb | ||
|
|
dcde599709 | ||
|
|
0e0945ef84 | ||
|
|
ad125327c0 | ||
|
|
dfaf20dd83 | ||
|
|
786262db3b | ||
|
|
29a4110d8f | ||
|
|
9b639f715f | ||
|
|
46f3d78c8a | ||
|
|
1338ae2fc3 | ||
|
|
37813a223a | ||
|
|
b488e969bb | ||
|
|
1377296ef4 | ||
|
|
01cb22af37 | ||
|
|
331305333d | ||
|
|
0651eae7ec | ||
|
|
1552417598 | ||
|
|
a74a8ee483 | ||
|
|
7fa5405cb7 | ||
|
|
cc0ce7c630 | ||
|
|
070d4aeb6c | ||
|
|
668ce26269 | ||
|
|
6725ddcc41 | ||
|
|
bce941db3f | ||
|
|
4499ae84bb | ||
|
|
d4e790d3cf | ||
|
|
9b35aa42a2 | ||
|
|
7163997367 | ||
|
|
2385a6c29b | ||
|
|
36173eb47d | ||
|
|
f7645824d9 | ||
|
|
6d926048ec | ||
|
|
5335c973b4 | ||
|
|
bcafa73faf | ||
|
|
15c3e5c96e | ||
|
|
a5d5904969 | ||
|
|
e3eefba745 | ||
|
|
a90f564980 | ||
|
|
253132afdf | ||
|
|
eded8abded | ||
|
|
0abed1afe5 | ||
|
|
22077d4181 | ||
|
|
b0e849f413 | ||
|
|
af3c0e43a5 | ||
|
|
387047f262 | ||
|
|
4a2a539c08 | ||
|
|
4214fcd2fa | ||
|
|
b2b64fb853 | ||
|
|
a6128a1df1 | ||
|
|
598758b991 | ||
|
|
9926e23bc8 | ||
|
|
6638ee47d3 | ||
|
|
65899a3e91 | ||
|
|
86625a7642 | ||
|
|
ee495450cc | ||
|
|
d369d315a7 | ||
|
|
7c9937e924 | ||
|
|
33a2999a57 | ||
|
|
076693efc9 | ||
|
|
54d1fcde5b | ||
|
|
1c656d6556 | ||
|
|
2431ce9f86 | ||
|
|
5d3264bc63 | ||
|
|
d71f819f95 | ||
|
|
80f844139c | ||
|
|
9eecaea31a | ||
|
|
3ccfa0e7fc | ||
|
|
6669998c10 | ||
|
|
ee13509760 | ||
|
|
82d7bb1f32 | ||
|
|
492e2f173e | ||
|
|
cdfda508d8 | ||
|
|
da941e584f | ||
|
|
380f0f2042 | ||
|
|
33d4844f17 | ||
|
|
989de47f22 | ||
|
|
8f19231ed5 | ||
|
|
5c60f27a7d | ||
|
|
2d4034f3b7 | ||
|
|
56d58ad8e5 | ||
|
|
a4f6bc63f0 | ||
|
|
26da81a3b0 | ||
|
|
ec9410b510 | ||
|
|
a0035e4de2 | ||
|
|
e4e3b25c22 | ||
|
|
d8c5f72258 | ||
|
|
d67ad47b2c | ||
|
|
b08e49b59d | ||
|
|
d3dc73ca06 | ||
|
|
d5ea15e6dd | ||
|
|
5d45286646 | ||
|
|
dabf149411 | ||
|
|
ee5ded6e1e | ||
|
|
598b07b53d | ||
|
|
e211f31370 | ||
|
|
0bcf608e0b | ||
|
|
969ad8911c | ||
|
|
581c382f65 | ||
|
|
f7f86ff821 | ||
|
|
212cd710aa | ||
|
|
33c44d3c0f | ||
|
|
0faa130cfd | ||
|
|
af76580b98 | ||
|
|
b526d172d6 | ||
|
|
f2917fc462 | ||
|
|
7c8c4c2a05 | ||
|
|
3595ac2551 | ||
|
|
8453191dfb | ||
|
|
65796cfc7b | ||
|
|
bab27462ab | ||
|
|
241278226f | ||
|
|
7f9de2c8ab | ||
|
|
f91f33c236 | ||
|
|
3f0ef57d31 | ||
|
|
0eb90cb3b6 | ||
|
|
9fe158b78a | ||
|
|
b14222dabd | ||
|
|
a24f3d7d47 | ||
|
|
c9700e38e2 | ||
|
|
05316c90ba | ||
|
|
242dc21876 | ||
|
|
08c4b9ac7c | ||
|
|
f30f4579e9 | ||
|
|
573357a08c | ||
|
|
0775cd09a1 | ||
|
|
96075dee7b | ||
|
|
066adf3cea | ||
|
|
65874d7b96 | ||
|
|
ac9b8f405c | ||
|
|
286f9b0c7d | ||
|
|
c403d05755 | ||
|
|
57dc53ceff | ||
|
|
340d3f833b | ||
|
|
694a9459c1 | ||
|
|
8d1419a12e | ||
|
|
04f7a7d301 | ||
|
|
0fb1fc87c8 | ||
|
|
d3c7aada89 | ||
|
|
e639600ba5 | ||
|
|
d439fb459b | ||
|
|
9d8a176182 | ||
|
|
600055bc73 | ||
|
|
e691b401c8 | ||
|
|
672b15d36c | ||
|
|
ac80723058 | ||
|
|
ab468bac3c | ||
|
|
c10d2a1493 | ||
|
|
97bbf79ffd | ||
|
|
2af0021c2b | ||
|
|
0f2eaccb39 | ||
|
|
b251671e3f | ||
|
|
c4eed37d8e | ||
|
|
8b43b91057 | ||
|
|
91fe7f0bee | ||
|
|
5cfb7b8de4 | ||
|
|
6329a1842a | ||
|
|
a6c6c6e300 | ||
|
|
30458405ce | ||
|
|
91220239e5 | ||
|
|
7ee695d74a | ||
|
|
867fc8be64 | ||
|
|
89509b487a | ||
|
|
ac0b29fb6d | ||
|
|
673c74ddfc | ||
|
|
3b7d223b0c | ||
|
|
b662670efc | ||
|
|
771626b6ec | ||
|
|
f15cc5bdfa | ||
|
|
fff5bbcee4 | ||
|
|
42d8e9e5bd | ||
|
|
da10c50945 | ||
|
|
24523cf31d | ||
|
|
1d681e53e4 | ||
|
|
e0235fda8b | ||
|
|
9dc9724e15 | ||
|
|
f7b01ae53d | ||
|
|
d704e1dbba | ||
|
|
393f6047f2 | ||
|
|
d94fc5b694 | ||
|
|
f06770d91d | ||
|
|
ef2ff5e093 | ||
|
|
7caed3b0db | ||
|
|
11960d9d3a | ||
|
|
bbd8fff021 | ||
|
|
ec17bd8608 | ||
|
|
0dbb8babee | ||
|
|
b14e9fc619 | ||
|
|
63c6d3478d | ||
|
|
781f0e7887 | ||
|
|
23e014cb25 | ||
|
|
5b64358ef1 | ||
|
|
56df64063d | ||
|
|
3f81eba13b | ||
|
|
7973412c29 | ||
|
|
f83de5f834 | ||
|
|
f2ceba978a | ||
|
|
96c074bb56 | ||
|
|
45641d0754 | ||
|
|
4b1d08ba99 | ||
|
|
f8a299caee | ||
|
|
437dce7756 | ||
|
|
f8ad820281 | ||
|
|
757f16a3b5 | ||
|
|
632ecc668f | ||
|
|
92d393537c | ||
|
|
160fa99ba4 | ||
|
|
d2a5ab49ed | ||
|
|
43d9d2eba7 | ||
|
|
baa260a03d | ||
|
|
b157a9927a | ||
|
|
b246a67e8a | ||
|
|
2d958e88bf | ||
|
|
07c7d5af17 | ||
|
|
42976ca48c | ||
|
|
d05e85efa9 | ||
|
|
547e117760 | ||
|
|
50a5d3c546 | ||
|
|
3fd82200cc | ||
|
|
7215392784 | ||
|
|
c279c6e2af | ||
|
|
8a9a8dfae5 | ||
|
|
c44314def3 | ||
|
|
8b899a9cf0 | ||
|
|
0ebdfa4825 | ||
|
|
32a06f119b | ||
|
|
ec30864ce5 | ||
|
|
6bc72e157a | ||
|
|
9537e2ae95 | ||
|
|
c6404d8917 | ||
|
|
7113807f12 | ||
|
|
10418323ef | ||
|
|
a11d5245ec | ||
|
|
565033e0d4 | ||
|
|
c25ef18900 | ||
|
|
b0a63ba0cc | ||
|
|
7b6c88f17c | ||
|
|
361ba8b244 | ||
|
|
9baa96d41c | ||
|
|
e584b4926f | ||
|
|
bcd9c2044e | ||
|
|
d582e76fed | ||
|
|
991dd8a558 | ||
|
|
bc49784797 | ||
|
|
7f99903fdb | ||
|
|
97d011ac8e | ||
|
|
61596a8e21 | ||
|
|
44e337cef6 | ||
|
|
1bec3eaa1e | ||
|
|
5942d796b5 | ||
|
|
eec9c05518 | ||
|
|
246d1621f5 | ||
|
|
6c1e06bf86 | ||
|
|
3c1e165134 | ||
|
|
80d1c594cc | ||
|
|
947db95d16 | ||
|
|
5b9362ab0b | ||
|
|
f602b088ac | ||
|
|
4acf0c4ee0 | ||
|
|
be711215e8 | ||
|
|
7e3b404240 | ||
|
|
9d3f329bc9 | ||
|
|
bd00a03e7b | ||
|
|
be517de7dc | ||
|
|
23ab1f0c81 | ||
|
|
49621e7b15 | ||
|
|
1ab6f017c9 | ||
|
|
0b364fd72f | ||
|
|
e80ae76856 | ||
|
|
eebad3e2a0 | ||
|
|
db2af47286 | ||
|
|
7ad28aeab4 | ||
|
|
8d80e7311c | ||
|
|
7932069535 | ||
|
|
78564ec61d | ||
|
|
b80184cd93 | ||
|
|
1fa079b466 | ||
|
|
fcfb9470c9 | ||
|
|
c99653f0f2 | ||
|
|
5080b4996e | ||
|
|
1903422113 | ||
|
|
e86901ca20 | ||
|
|
bdfa61c8b2 | ||
|
|
4e8722661f | ||
|
|
6f9d2939e7 | ||
|
|
3234510d31 | ||
|
|
a40af08018 | ||
|
|
223a6b546c | ||
|
|
0d5da2d9d4 | ||
|
|
37337b8b1d | ||
|
|
ffcc058927 | ||
|
|
3a1cda5802 | ||
|
|
5c1015d6fc | ||
|
|
3385ec3f78 | ||
|
|
1b46c19849 | ||
|
|
75a4fc905b | ||
|
|
05666efda9 | ||
|
|
59367b3417 | ||
|
|
9a31b83b2a | ||
|
|
a81a56706e | ||
|
|
9dc1b35e82 | ||
|
|
2dc1180a41 | ||
|
|
ada7c83e96 | ||
|
|
ea287ebf86 | ||
|
|
043cdeafb3 | ||
|
|
8b36ca95a3 | ||
|
|
2cc36787f5 | ||
|
|
448ac61b48 | ||
|
|
8933179017 | ||
|
|
792e786880 | ||
|
|
753f6394f7 | ||
|
|
0a8030306e | ||
|
|
b1faf65934 | ||
|
|
09f478bd74 | ||
|
|
d6bad01130 | ||
|
|
a33deed26b | ||
|
|
2e7345f4f0 | ||
|
|
6e03078de3 | ||
|
|
1a7de4c2d6 | ||
|
|
66ba773367 | ||
|
|
afc3583be8 | ||
|
|
cbff2c6035 | ||
|
|
8e614ecb6e | ||
|
|
d099885fd1 | ||
|
|
2bb8c3d914 | ||
|
|
4caa61fe96 | ||
|
|
c5960f9b6a | ||
|
|
412eed19c3 | ||
|
|
e9b596d3bc | ||
|
|
a0497feddd | ||
|
|
789693bde9 | ||
|
|
8b109bac13 | ||
|
|
097d9c9f3c | ||
|
|
e7d8a041a8 | ||
|
|
dc2978824e | ||
|
|
e1994ef2cf | ||
|
|
efb49019d4 | ||
|
|
ef874712bb | ||
|
|
26965fa08f | ||
|
|
1fe933e4ea | ||
|
|
491f73e0cd | ||
|
|
724b4b5a70 | ||
|
|
1778a56146 | ||
|
|
4656c12f6d | ||
|
|
a06321675a | ||
|
|
dbe11c1360 | ||
|
|
75ecf1c44d | ||
|
|
5283919d24 | ||
|
|
ced8c8e497 | ||
|
|
bf7852ce85 | ||
|
|
30214fde74 | ||
|
|
e9c213f803 | ||
|
|
9f11e045a5 | ||
|
|
93ebdf724b | ||
|
|
59ce228c2e | ||
|
|
744865fcb2 | ||
|
|
7f8c8b448d | ||
|
|
1d6137f7e2 | ||
|
|
66c56e9d02 | ||
|
|
e744d4c82c | ||
|
|
0774ecc89c | ||
|
|
5e7a4c7fb5 | ||
|
|
76eaf64f94 | ||
|
|
80865b30a5 | ||
|
|
8e6248f57f | ||
|
|
268db6b1d0 | ||
|
|
479dd80a8c | ||
|
|
069431db72 | ||
|
|
bc2b310638 | ||
|
|
33bf20cfc8 | ||
|
|
e3bdc391cd | ||
|
|
5681f4dd69 | ||
|
|
38d822e04c | ||
|
|
8e274a5a84 | ||
|
|
985d2f1c2c | ||
|
|
7f5872372d | ||
|
|
201f788806 | ||
|
|
a14b5c33fd | ||
|
|
473261be35 | ||
|
|
a54be85185 | ||
|
|
a67c53826d | ||
|
|
14b131e850 | ||
|
|
54e3122540 | ||
|
|
d339ab1125 | ||
|
|
3ab09ef708 | ||
|
|
c86a122d80 | ||
|
|
3a58e37838 | ||
|
|
6bd49bcd4b | ||
|
|
61577cf6bf | ||
|
|
b4dec2a99c | ||
|
|
fe0b122aca | ||
|
|
8eb2960950 | ||
|
|
c2369a740d | ||
|
|
bab6fd1f2f | ||
|
|
86fbc9a936 | ||
|
|
4d9726dbdd | ||
|
|
4442a2e6d1 | ||
|
|
293be7093c | ||
|
|
990acbb9ac |
17
.env.example
17
.env.example
@@ -3,6 +3,10 @@ APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_KEY=SomeRandomString
|
||||
|
||||
# The below url has to be set if using social auth options
|
||||
# or if you are not using BookStack at the root path of your domain.
|
||||
# APP_URL=http://bookstack.dev
|
||||
|
||||
# Database details
|
||||
DB_HOST=localhost
|
||||
DB_DATABASE=database_database
|
||||
@@ -12,8 +16,17 @@ DB_PASSWORD=database_user_password
|
||||
# Cache and session
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
# If using Memcached, comment the above and uncomment these
|
||||
#CACHE_DRIVER=memcached
|
||||
#SESSION_DRIVER=memcached
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
# Memcached settings
|
||||
# If using a UNIX socket path for the host, set the port to 0
|
||||
# This follows the following format: HOST:PORT:WEIGHT
|
||||
# For multiple servers separate with a comma
|
||||
MEMCACHED_SERVERS=127.0.0.1:11211:100
|
||||
|
||||
# Storage
|
||||
STORAGE_TYPE=local
|
||||
# Amazon S3 Config
|
||||
@@ -33,8 +46,6 @@ GITHUB_APP_ID=false
|
||||
GITHUB_APP_SECRET=false
|
||||
GOOGLE_APP_ID=false
|
||||
GOOGLE_APP_SECRET=false
|
||||
# URL used for social login redirects, NO TRAILING SLASH
|
||||
APP_URL=http://bookstack.dev
|
||||
|
||||
# External services such as Gravatar
|
||||
DISABLE_EXTERNAL_SERVICES=false
|
||||
@@ -53,4 +64,4 @@ MAIL_HOST=localhost
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_ENCRYPTION=null
|
||||
13
.github/ISSUE_TEMPLATE.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
### For Feature Requests
|
||||
|
||||
Desired Feature:
|
||||
|
||||
### For Bug Reports
|
||||
|
||||
* BookStack Version:
|
||||
* PHP Version:
|
||||
* MySQL Version:
|
||||
|
||||
##### Expected Behavior
|
||||
|
||||
##### Actual Behavior
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -10,4 +10,14 @@ Homestead.yaml
|
||||
/public/bower
|
||||
/storage/images
|
||||
_ide_helper.php
|
||||
/storage/debugbar
|
||||
/storage/debugbar
|
||||
.phpstorm.meta.php
|
||||
yarn.lock
|
||||
/bin
|
||||
.buildpath
|
||||
|
||||
.project
|
||||
|
||||
.settings/org.eclipse.wst.common.project.facet.core.xml
|
||||
|
||||
.settings/org.eclipse.php.core.prefs
|
||||
|
||||
28
.travis.yml
Normal file
28
.travis.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
dist: trusty
|
||||
sudo: false
|
||||
language: php
|
||||
php:
|
||||
- 7.0
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
before_script:
|
||||
- mysql -u root -e 'create database `bookstack-test`;'
|
||||
- mysql -u root -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
|
||||
- mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
||||
- mysql -u root -e "FLUSH PRIVILEGES;"
|
||||
- phpenv config-rm xdebug.ini
|
||||
- composer dump-autoload --no-interaction
|
||||
- composer install --prefer-dist --no-interaction
|
||||
- php artisan clear-compiled -n
|
||||
- php artisan optimize -n
|
||||
- php artisan migrate --force -n --database=mysql_testing
|
||||
- php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
|
||||
|
||||
after_failure:
|
||||
- cat storage/logs/laravel.log
|
||||
|
||||
script:
|
||||
- phpunit
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Dan Brown
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property string key
|
||||
* @property \User user
|
||||
@@ -15,15 +13,11 @@ class Activity extends Model
|
||||
|
||||
/**
|
||||
* Get the entity for this activity.
|
||||
* @return bool
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
if ($this->entity_id) {
|
||||
return $this->morphTo('entity')->first();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if ($this->entity_type === '') $this->entity_type = null;
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +26,7 @@ class Activity extends Model
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User');
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +44,7 @@ class Activity extends Model
|
||||
* @return bool
|
||||
*/
|
||||
public function isSimilarTo($activityB) {
|
||||
return [$this->key, $this->entitiy_type, $this->entitiy_id] === [$activityB->key, $activityB->entitiy_type, $activityB->entitiy_id];
|
||||
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
36
app/Attachment.php
Normal file
36
app/Attachment.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class Attachment extends Ownable
|
||||
{
|
||||
protected $fillable = ['name', 'order'];
|
||||
|
||||
/**
|
||||
* Get the downloadable file name for this upload.
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function getFileName()
|
||||
{
|
||||
if (str_contains($this->name, '.')) return $this->name;
|
||||
return $this->name . '.' . $this->extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page this file was uploaded to.
|
||||
* @return Page
|
||||
*/
|
||||
public function page()
|
||||
{
|
||||
return $this->belongsTo(Page::class, 'uploaded_to');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url of this file.
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return baseUrl('/attachments/' . $this->id);
|
||||
}
|
||||
|
||||
}
|
||||
49
app/Book.php
49
app/Book.php
@@ -1,35 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class Book extends Entity
|
||||
{
|
||||
|
||||
protected $fillable = ['name', 'description'];
|
||||
|
||||
public function getUrl()
|
||||
/**
|
||||
* Get the url for this book.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
return '/books/' . $this->slug;
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||
}
|
||||
return baseUrl('/books/' . urlencode($this->slug));
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the edit url for this book.
|
||||
* @return string
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return $this->getUrl() . '/edit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages within this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function pages()
|
||||
{
|
||||
return $this->hasMany('BookStack\Page');
|
||||
return $this->hasMany(Page::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all chapters within this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function chapters()
|
||||
{
|
||||
return $this->hasMany('BookStack\Chapter');
|
||||
return $this->hasMany(Chapter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description;
|
||||
$description = $this->description;
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,25 +5,59 @@ class Chapter extends Entity
|
||||
{
|
||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||
|
||||
protected $with = ['book'];
|
||||
|
||||
/**
|
||||
* Get the book this chapter is within.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function book()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Book');
|
||||
return $this->belongsTo(Book::class);
|
||||
}
|
||||
|
||||
public function pages()
|
||||
/**
|
||||
* Get the pages that this chapter contains.
|
||||
* @param string $dir
|
||||
* @return mixed
|
||||
*/
|
||||
public function pages($dir = 'ASC')
|
||||
{
|
||||
return $this->hasMany('BookStack\Page')->orderBy('priority', 'ASC');
|
||||
return $this->hasMany(Page::class)->orderBy('priority', $dir);
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
/**
|
||||
* Get the url of this chapter.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
||||
return '/books/' . $bookSlug. '/chapter/' . $this->slug;
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||
}
|
||||
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this chapter's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description;
|
||||
$description = $this->description;
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
47
app/Console/Commands/ClearActivity.php
Normal file
47
app/Console/Commands/ClearActivity.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Activity;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearActivity extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:clear-activity';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clear user activity from the system';
|
||||
|
||||
protected $activity;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param Activity $activity
|
||||
*/
|
||||
public function __construct(Activity $activity)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->activity->newQuery()->truncate();
|
||||
$this->comment('System activity cleared');
|
||||
}
|
||||
}
|
||||
50
app/Console/Commands/ClearRevisions.php
Normal file
50
app/Console/Commands/ClearRevisions.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\PageRevision;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearRevisions extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:clear-revisions
|
||||
{--a|all : Include active update drafts in deletion}
|
||||
';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clear page revisions';
|
||||
|
||||
protected $pageRevision;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param PageRevision $pageRevision
|
||||
*/
|
||||
public function __construct(PageRevision $pageRevision)
|
||||
{
|
||||
$this->pageRevision = $pageRevision;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$deleteTypes = $this->option('all') ? ['version', 'update_draft'] : ['version'];
|
||||
$this->pageRevision->newQuery()->whereIn('type', $deleteTypes)->delete();
|
||||
$this->comment('Revisions deleted');
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,21 @@ namespace BookStack\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ResetViews extends Command
|
||||
class ClearViews extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'views:reset';
|
||||
protected $signature = 'bookstack:clear-views';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Reset all view-counts for all entities.';
|
||||
protected $description = 'Clear all view-counts for all entities.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
@@ -37,5 +37,6 @@ class ResetViews extends Command
|
||||
public function handle()
|
||||
{
|
||||
\Views::resetAll();
|
||||
$this->comment('Views cleared');
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
|
||||
class Inspire extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'inspire';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Display an inspiring quote';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->comment(PHP_EOL.Inspiring::quote().PHP_EOL);
|
||||
}
|
||||
}
|
||||
60
app/Console/Commands/RegeneratePermissions.php
Normal file
60
app/Console/Commands/RegeneratePermissions.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\PermissionService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegeneratePermissions extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:regenerate-permissions {--database= : The database connection to use.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Regenerate all system permissions';
|
||||
|
||||
/**
|
||||
* The service to handle the permission system.
|
||||
*
|
||||
* @var PermissionService
|
||||
*/
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(PermissionService $permissionService)
|
||||
{
|
||||
$this->permissionService = $permissionService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = \DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
\DB::setDefaultConnection($this->option('database'));
|
||||
$this->permissionService->setConnection(\DB::connection($this->option('database')));
|
||||
}
|
||||
|
||||
$this->permissionService->buildJointPermissions();
|
||||
|
||||
\DB::setDefaultConnection($connection);
|
||||
$this->comment('Permissions regenerated');
|
||||
}
|
||||
}
|
||||
54
app/Console/Commands/RegenerateSearch.php
Normal file
54
app/Console/Commands/RegenerateSearch.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\SearchService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegenerateSearch extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:regenerate-search {--database= : The database connection to use.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(SearchService $searchService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->searchService = $searchService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = \DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
\DB::setDefaultConnection($this->option('database'));
|
||||
$this->searchService->setConnection(\DB::connection($this->option('database')));
|
||||
}
|
||||
|
||||
$this->searchService->indexAllEntities();
|
||||
\DB::setDefaultConnection($connection);
|
||||
$this->comment('Search index regenerated');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console;
|
||||
<?php namespace BookStack\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
@@ -13,8 +11,11 @@ class Kernel extends ConsoleKernel
|
||||
* @var array
|
||||
*/
|
||||
protected $commands = [
|
||||
\BookStack\Console\Commands\Inspire::class,
|
||||
\BookStack\Console\Commands\ResetViews::class,
|
||||
Commands\ClearViews::class,
|
||||
Commands\ClearActivity::class,
|
||||
Commands\ClearRevisions::class,
|
||||
Commands\RegeneratePermissions::class,
|
||||
Commands\RegenerateSearch::class
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -25,7 +26,6 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->command('inspire')
|
||||
->hourly();
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EmailConfirmation extends Model
|
||||
{
|
||||
protected $fillable = ['user_id', 'token'];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User');
|
||||
}
|
||||
}
|
||||
131
app/Entity.php
131
app/Entity.php
@@ -1,13 +1,10 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
abstract class Entity extends Model
|
||||
class Entity extends Ownable
|
||||
{
|
||||
|
||||
use Ownable;
|
||||
public $textField = 'description';
|
||||
|
||||
/**
|
||||
* Compares this entity to another given entity.
|
||||
@@ -48,16 +45,62 @@ abstract class Entity extends Model
|
||||
*/
|
||||
public function activity()
|
||||
{
|
||||
return $this->morphMany('BookStack\Activity', 'entity')->orderBy('created_at', 'desc');
|
||||
return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get View objects for this entity.
|
||||
* @return mixed
|
||||
*/
|
||||
public function views()
|
||||
{
|
||||
return $this->morphMany('BookStack\View', 'viewable');
|
||||
return $this->morphMany(View::class, 'viewable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Tag models that have been user assigned to this entity.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related search terms.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function searchTerms()
|
||||
{
|
||||
return $this->morphMany(SearchTerm::class, 'entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this entities restrictions.
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return $this->morphMany(EntityPermission::class, 'restrictable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this entity has a specific restriction set against it.
|
||||
* @param $role_id
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRestriction($role_id, $action)
|
||||
{
|
||||
return $this->permissions()->where('role_id', '=', $role_id)
|
||||
->where('action', '=', $action)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity jointPermissions this is connected to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function jointPermissions()
|
||||
{
|
||||
return $this->morphMany(JointPermission::class, 'entity');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,63 +111,65 @@ abstract class Entity extends Model
|
||||
*/
|
||||
public static function isA($type)
|
||||
{
|
||||
return static::getClassName() === strtolower($type);
|
||||
return static::getType() === strtolower($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the class name.
|
||||
* @return string
|
||||
* Get entity type.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getClassName()
|
||||
public static function getType()
|
||||
{
|
||||
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
|
||||
return strtolower(static::getClassName());
|
||||
}
|
||||
|
||||
/**
|
||||
*Gets a limited-length version of the entities name.
|
||||
* Get an instance of an entity of the given type.
|
||||
* @param $type
|
||||
* @return Entity
|
||||
*/
|
||||
public static function getEntityInstance($type)
|
||||
{
|
||||
$types = ['Page', 'Book', 'Chapter'];
|
||||
$className = str_replace([' ', '-', '_'], '', ucwords($type));
|
||||
if (!in_array($className, $types)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return app('BookStack\\' . $className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a limited-length version of the entities name.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getShortName($length = 25)
|
||||
{
|
||||
if(strlen($this->name) <= $length) return $this->name;
|
||||
return substr($this->name, 0, $length-3) . '...';
|
||||
if (strlen($this->name) <= $length) return $this->name;
|
||||
return substr($this->name, 0, $length - 3) . '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a full-text search on this entity.
|
||||
* @param string[] $fieldsToSearch
|
||||
* @param string[] $terms
|
||||
* @param string[] array $wheres
|
||||
* Get the body text of this entity.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function fullTextSearch($fieldsToSearch, $terms, $wheres = [])
|
||||
public function getText()
|
||||
{
|
||||
$termString = '';
|
||||
foreach ($terms as $term) {
|
||||
$termString .= htmlentities($term) . '* ';
|
||||
}
|
||||
$fields = implode(',', $fieldsToSearch);
|
||||
$termStringEscaped = \DB::connection()->getPdo()->quote($termString);
|
||||
$search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance'));
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termStringEscaped]);
|
||||
|
||||
// Add additional where terms
|
||||
foreach ($wheres as $whereTerm) {
|
||||
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
|
||||
}
|
||||
|
||||
// Load in relations
|
||||
if (!static::isA('book')) $search = $search->with('book');
|
||||
if (static::isA('page')) $search = $search->with('chapter');
|
||||
|
||||
return $search->orderBy('title_relevance', 'desc')->get();
|
||||
return $this->{$this->textField};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this item.
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getUrl();
|
||||
public function entityRawQuery(){return '';}
|
||||
|
||||
/**
|
||||
* Get the url of this entity
|
||||
* @param $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path){return '/';}
|
||||
|
||||
}
|
||||
|
||||
18
app/EntityPermission.php
Normal file
18
app/EntityPermission.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class EntityPermission extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['role_id', 'action'];
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get all this restriction's attached entity.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function restrictable()
|
||||
{
|
||||
return $this->morphTo('restrictable');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Events;
|
||||
|
||||
abstract class Event
|
||||
{
|
||||
//
|
||||
}
|
||||
4
app/Exceptions/AuthException.php
Normal file
4
app/Exceptions/AuthException.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class AuthException extends PrettyException {}
|
||||
4
app/Exceptions/FileUploadException.php
Normal file
4
app/Exceptions/FileUploadException.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class FileUploadException extends PrettyException {}
|
||||
@@ -3,9 +3,9 @@
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Validation\ValidationException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use PhpSpec\Exception\Example\ErrorException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
@@ -47,18 +47,60 @@ class Handler extends ExceptionHandler
|
||||
{
|
||||
// Handle notify exceptions which will redirect to the
|
||||
// specified location then show a notification message.
|
||||
if ($e instanceof NotifyException) {
|
||||
\Session::flash('error', $e->message);
|
||||
return response()->redirectTo($e->redirectLocation);
|
||||
if ($this->isExceptionType($e, NotifyException::class)) {
|
||||
session()->flash('error', $this->getOriginalMessage($e));
|
||||
return redirect($e->redirectLocation);
|
||||
}
|
||||
|
||||
// Handle pretty exceptions which will show a friendly application-fitting page
|
||||
// Which will include the basic message to point the user roughly to the cause.
|
||||
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
|
||||
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
|
||||
return response()->view('errors/500', ['message' => $message], 500);
|
||||
if ($this->isExceptionType($e, PrettyException::class) && !config('app.debug')) {
|
||||
$message = $this->getOriginalMessage($e);
|
||||
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
|
||||
return response()->view('errors/' . $code, ['message' => $message], $code);
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the exception chain to compare against the original exception type.
|
||||
* @param Exception $e
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
protected function isExceptionType(Exception $e, $type) {
|
||||
do {
|
||||
if (is_a($e, $type)) return true;
|
||||
} while ($e = $e->getPrevious());
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get original exception message.
|
||||
* @param Exception $e
|
||||
* @return string
|
||||
*/
|
||||
protected function getOriginalMessage(Exception $e) {
|
||||
do {
|
||||
$message = $e->getMessage();
|
||||
} while ($e = $e->getPrevious());
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an authentication exception into an unauthenticated response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Auth\AuthenticationException $exception
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
protected function unauthenticated($request, AuthenticationException $exception)
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['error' => 'Unauthenticated.'], 401);
|
||||
}
|
||||
|
||||
return redirect()->guest('login');
|
||||
}
|
||||
}
|
||||
|
||||
14
app/Exceptions/NotFoundException.php
Normal file
14
app/Exceptions/NotFoundException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class NotFoundException extends PrettyException {
|
||||
|
||||
/**
|
||||
* NotFoundException constructor.
|
||||
* @param string $message
|
||||
*/
|
||||
public function __construct($message = 'Item not found')
|
||||
{
|
||||
parent::__construct($message, 404);
|
||||
}
|
||||
}
|
||||
6
app/Exceptions/PermissionsException.php
Normal file
6
app/Exceptions/PermissionsException.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
class PermissionsException extends Exception {}
|
||||
@@ -1,5 +1,3 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PrettyException extends Exception {}
|
||||
class PrettyException extends \Exception {}
|
||||
215
app/Http/Controllers/AttachmentController.php
Normal file
215
app/Http/Controllers/AttachmentController.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
use BookStack\Attachment;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Services\AttachmentService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AttachmentController extends Controller
|
||||
{
|
||||
protected $attachmentService;
|
||||
protected $attachment;
|
||||
protected $entityRepo;
|
||||
|
||||
/**
|
||||
* AttachmentController constructor.
|
||||
* @param AttachmentService $attachmentService
|
||||
* @param Attachment $attachment
|
||||
* @param EntityRepo $entityRepo
|
||||
*/
|
||||
public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo)
|
||||
{
|
||||
$this->attachmentService = $attachmentService;
|
||||
$this->attachment = $attachment;
|
||||
$this->entityRepo = $entityRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Endpoint at which attachments are uploaded to.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function upload(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||
'file' => 'required|file'
|
||||
]);
|
||||
|
||||
$pageId = $request->get('uploaded_to');
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
|
||||
$this->checkPermission('attachment-create-all');
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$uploadedFile = $request->file('file');
|
||||
|
||||
try {
|
||||
$attachment = $this->attachmentService->saveNewUpload($uploadedFile, $pageId);
|
||||
} catch (FileUploadException $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return response()->json($attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an uploaded attachment.
|
||||
* @param int $attachmentId
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function uploadUpdate($attachmentId, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||
'file' => 'required|file'
|
||||
]);
|
||||
|
||||
$pageId = $request->get('uploaded_to');
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||
|
||||
if (intval($pageId) !== intval($attachment->uploaded_to)) {
|
||||
return $this->jsonError(trans('errors.attachment_page_mismatch'));
|
||||
}
|
||||
|
||||
$uploadedFile = $request->file('file');
|
||||
|
||||
try {
|
||||
$attachment = $this->attachmentService->saveUpdatedUpload($uploadedFile, $attachment);
|
||||
} catch (FileUploadException $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return response()->json($attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the details of an existing file.
|
||||
* @param $attachmentId
|
||||
* @param Request $request
|
||||
* @return Attachment|mixed
|
||||
*/
|
||||
public function update($attachmentId, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||
'name' => 'required|string|min:1|max:255',
|
||||
'link' => 'url|min:1|max:255'
|
||||
]);
|
||||
|
||||
$pageId = $request->get('uploaded_to');
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||
|
||||
if (intval($pageId) !== intval($attachment->uploaded_to)) {
|
||||
return $this->jsonError(trans('errors.attachment_page_mismatch'));
|
||||
}
|
||||
|
||||
$attachment = $this->attachmentService->updateFile($attachment, $request->all());
|
||||
return response()->json($attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a link to a page.
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function attachLink(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||
'name' => 'required|string|min:1|max:255',
|
||||
'link' => 'required|url|min:1|max:255'
|
||||
]);
|
||||
|
||||
$pageId = $request->get('uploaded_to');
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
|
||||
$this->checkPermission('attachment-create-all');
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$attachmentName = $request->get('name');
|
||||
$link = $request->get('link');
|
||||
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
|
||||
|
||||
return response()->json($attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for a specific page.
|
||||
* @param $pageId
|
||||
* @return mixed
|
||||
*/
|
||||
public function listForPage($pageId)
|
||||
{
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
return response()->json($page->attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the attachment sorting.
|
||||
* @param $pageId
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function sortForPage($pageId, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'files' => 'required|array',
|
||||
'files.*.id' => 'required|integer',
|
||||
]);
|
||||
$page = $this->entityRepo->getById('page', $pageId);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$attachments = $request->get('files');
|
||||
$this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
|
||||
return response()->json(['message' => trans('entities.attachments_order_updated')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an attachment from storage.
|
||||
* @param $attachmentId
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function get($attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
$page = $this->entityRepo->getById('page', $attachment->uploaded_to);
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
if ($attachment->external) {
|
||||
return redirect($attachment->path);
|
||||
}
|
||||
|
||||
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
|
||||
return response($attachmentContents, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific attachment in the system.
|
||||
* @param $attachmentId
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete($attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
$this->checkOwnablePermission('attachment-delete', $attachment);
|
||||
$this->attachmentService->deleteFile($attachment);
|
||||
return response()->json(['message' => trans('entities.attachments_deleted')]);
|
||||
}
|
||||
}
|
||||
68
app/Http/Controllers/Auth/ForgotPasswordController.php
Normal file
68
app/Http/Controllers/Auth/ForgotPasswordController.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
||||
use Illuminate\Http\Request;
|
||||
use Password;
|
||||
|
||||
class ForgotPasswordController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller is responsible for handling password reset emails and
|
||||
| includes a trait which assists in sending these notifications from
|
||||
| your application to your users. Feel free to explore this trait.
|
||||
|
|
||||
*/
|
||||
|
||||
use SendsPasswordResetEmails;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send a reset link to the given user.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function sendResetLinkEmail(Request $request)
|
||||
{
|
||||
$this->validate($request, ['email' => 'required|email']);
|
||||
|
||||
// We will send the password reset link to this user. Once we have attempted
|
||||
// to send the link, we will examine the response then see the message we
|
||||
// need to show to the user. Finally, we'll send out a proper response.
|
||||
$response = $this->broker()->sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
if ($response === Password::RESET_LINK_SENT) {
|
||||
$message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
|
||||
session()->flash('success', $message);
|
||||
return back()->with('status', trans($response));
|
||||
}
|
||||
|
||||
// If an error was returned by the password broker, we will get this message
|
||||
// translated so we can notify a user of the problem. We'll redirect back
|
||||
// to where the users came from so they can attempt this process again.
|
||||
return back()->withErrors(
|
||||
['email' => trans($response)]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
124
app/Http/Controllers/Auth/LoginController.php
Normal file
124
app/Http/Controllers/Auth/LoginController.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Exceptions\AuthException;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Login Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller handles authenticating users for the application and
|
||||
| redirecting them to your home screen. The controller uses a trait
|
||||
| to conveniently provide its functionality to your applications.
|
||||
|
|
||||
*/
|
||||
|
||||
use AuthenticatesUsers;
|
||||
|
||||
/**
|
||||
* Where to redirect users after login.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/';
|
||||
|
||||
protected $redirectPath = '/';
|
||||
protected $redirectAfterLogout = '/login';
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
|
||||
{
|
||||
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->redirectPath = baseUrl('/');
|
||||
$this->redirectAfterLogout = baseUrl('/login');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function username()
|
||||
{
|
||||
return config('auth.method') === 'standard' ? 'email' : 'username';
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the action when a user is authenticated.
|
||||
* If the user authenticated but does not exist in the user table we create them.
|
||||
* @param Request $request
|
||||
* @param Authenticatable $user
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws AuthException
|
||||
*/
|
||||
protected function authenticated(Request $request, Authenticatable $user)
|
||||
{
|
||||
// Explicitly log them out for now if they do no exist.
|
||||
if (!$user->exists) auth()->logout($user);
|
||||
|
||||
if (!$user->exists && $user->email === null && !$request->has('email')) {
|
||||
$request->flash();
|
||||
session()->flash('request-email', true);
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
if (!$user->exists && $user->email === null && $request->has('email')) {
|
||||
$user->email = $request->get('email');
|
||||
}
|
||||
|
||||
if (!$user->exists) {
|
||||
|
||||
// Check for users with same email already
|
||||
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
||||
if ($alreadyUser) {
|
||||
throw new AuthException(trans('errors.error_user_exists_different_creds', ['email' => $user->email]));
|
||||
}
|
||||
|
||||
$user->save();
|
||||
$this->userRepo->attachDefaultRole($user);
|
||||
auth()->login($user);
|
||||
}
|
||||
|
||||
$path = session()->pull('url.intended', '/');
|
||||
$path = baseUrl($path, true);
|
||||
return redirect($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the application login form.
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function getLogin()
|
||||
{
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
$authMethod = config('auth.method');
|
||||
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the relevant social site.
|
||||
* @param $socialDriver
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
public function getSocialLogin($socialDriver)
|
||||
{
|
||||
session()->put('social-callback', 'login');
|
||||
return $this->socialAuthService->startLogIn($socialDriver);
|
||||
}
|
||||
}
|
||||
@@ -2,83 +2,94 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\EmailConfirmationService;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use BookStack\SocialAccount;
|
||||
use BookStack\User;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Validator;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ThrottlesLogins;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
|
||||
class AuthController extends Controller
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Registration & Login Controller
|
||||
| Register Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller handles the registration of new users, as well as the
|
||||
| authentication of existing users. By default, this controller uses
|
||||
| a simple trait to add these behaviors. Why don't you explore it?
|
||||
| This controller handles the registration of new users as well as their
|
||||
| validation and creation. By default this controller uses a trait to
|
||||
| provide this functionality without requiring any additional code.
|
||||
|
|
||||
*/
|
||||
|
||||
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
|
||||
|
||||
protected $redirectPath = '/';
|
||||
protected $redirectAfterLogout = '/login';
|
||||
protected $username = 'email';
|
||||
|
||||
use RegistersUsers;
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $emailConfirmationService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new authentication controller instance.
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* Where to redirect users after login / registration.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/';
|
||||
protected $redirectPath = '/';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* @param EmailConfirmationService $emailConfirmationService
|
||||
* @param UserRepo $userRepo
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
||||
{
|
||||
$this->middleware('guest', ['only' => ['getLogin', 'postLogin', 'getRegister', 'postRegister']]);
|
||||
$this->middleware('guest')->except(['socialCallback', 'detachSocialAccount']);
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->emailConfirmationService = $emailConfirmationService;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->username = config('auth.method') === 'standard' ? 'email' : 'username';
|
||||
$this->redirectTo = baseUrl('/');
|
||||
$this->redirectPath = baseUrl('/');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a validator for an incoming registration request.
|
||||
*
|
||||
* @param array $data
|
||||
* @return \Illuminate\Contracts\Validation\Validator
|
||||
*/
|
||||
protected function validator(array $data)
|
||||
{
|
||||
return Validator::make($data, [
|
||||
'name' => 'required|max:255',
|
||||
'email' => 'required|email|max:255|unique:users',
|
||||
'name' => 'required|max:255',
|
||||
'email' => 'required|email|max:255|unique:users',
|
||||
'password' => 'required|min:6',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not registrations are allowed in the app settings.
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function checkRegistrationAllowed()
|
||||
{
|
||||
if (!\Setting::get('registration-enabled')) {
|
||||
throw new UserRegistrationException('Registrations are currently disabled.', '/login');
|
||||
if (!setting('registration-enabled')) {
|
||||
throw new UserRegistrationException(trans('auth.registrations_disabled'), '/login');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the application registration form.
|
||||
* @return \Illuminate\Http\Response
|
||||
* @return Response
|
||||
*/
|
||||
public function getRegister()
|
||||
{
|
||||
@@ -89,9 +100,10 @@ class AuthController extends Controller
|
||||
|
||||
/**
|
||||
* Handle a registration request for the application.
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
* @param Request|\Illuminate\Http\Request $request
|
||||
* @return Response
|
||||
* @throws UserRegistrationException
|
||||
* @throws \Illuminate\Foundation\Validation\ValidationException
|
||||
*/
|
||||
public function postRegister(Request $request)
|
||||
{
|
||||
@@ -108,73 +120,35 @@ class AuthController extends Controller
|
||||
return $this->registerUser($userData);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overrides the action when a user is authenticated.
|
||||
* If the user authenticated but does not exist in the user table we create them.
|
||||
* @param Request $request
|
||||
* @param Authenticatable $user
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* Create a new user instance after a valid registration.
|
||||
* @param array $data
|
||||
* @return User
|
||||
*/
|
||||
protected function authenticated(Request $request, Authenticatable $user)
|
||||
protected function create(array $data)
|
||||
{
|
||||
// Explicitly log them out for now if they do no exist.
|
||||
if (!$user->exists) auth()->logout($user);
|
||||
|
||||
if (!$user->exists && $user->email === null && !$request->has('email')) {
|
||||
$request->flash();
|
||||
session()->flash('request-email', true);
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
if (!$user->exists && $user->email === null && $request->has('email')) {
|
||||
$user->email = $request->get('email');
|
||||
}
|
||||
|
||||
if (!$user->exists) {
|
||||
$user->save();
|
||||
$this->userRepo->attachDefaultRole($user);
|
||||
auth()->login($user);
|
||||
}
|
||||
|
||||
return redirect()->intended($this->redirectPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user after a registration callback.
|
||||
* @param $socialDriver
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function socialRegisterCallback($socialDriver)
|
||||
{
|
||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
|
||||
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
|
||||
|
||||
// Create an array of the user data to create a new user instance
|
||||
$userData = [
|
||||
'name' => $socialUser->getName(),
|
||||
'email' => $socialUser->getEmail(),
|
||||
'password' => str_random(30)
|
||||
];
|
||||
return $this->registerUser($userData, $socialAccount);
|
||||
return User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password']),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The registrations flow for all users.
|
||||
* @param array $userData
|
||||
* @param array $userData
|
||||
* @param bool|false|SocialAccount $socialAccount
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
* @throws \BookStack\Exceptions\ConfirmationEmailException
|
||||
* @throws ConfirmationEmailException
|
||||
*/
|
||||
protected function registerUser(array $userData, $socialAccount = false)
|
||||
{
|
||||
if (\Setting::get('registration-restrict')) {
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', \Setting::get('registration-restrict')));
|
||||
if (setting('registration-restrict')) {
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
|
||||
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
|
||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||
throw new UserRegistrationException('That email domain does not have access to this application', '/register');
|
||||
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,17 +157,20 @@ class AuthController extends Controller
|
||||
$newUser->socialAccounts()->save($socialAccount);
|
||||
}
|
||||
|
||||
if (\Setting::get('registration-confirmation') || \Setting::get('registration-restrict')) {
|
||||
$newUser->email_confirmed = false;
|
||||
if (setting('registration-confirmation') || setting('registration-restrict')) {
|
||||
$newUser->save();
|
||||
$this->emailConfirmationService->sendConfirmation($newUser);
|
||||
|
||||
try {
|
||||
$this->emailConfirmationService->sendConfirmation($newUser);
|
||||
} catch (Exception $e) {
|
||||
session()->flash('error', trans('auth.email_confirm_send_error'));
|
||||
}
|
||||
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
$newUser->email_confirmed = true;
|
||||
|
||||
auth()->login($newUser);
|
||||
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
|
||||
session()->flash('success', trans('auth.register_success'));
|
||||
return redirect($this->redirectPath());
|
||||
}
|
||||
|
||||
@@ -206,18 +183,6 @@ class AuthController extends Controller
|
||||
return view('auth/register-confirm');
|
||||
}
|
||||
|
||||
/**
|
||||
* View the confirmation email as a standard web page.
|
||||
* @param $token
|
||||
* @return \Illuminate\View\View
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function viewConfirmEmail($token)
|
||||
{
|
||||
$confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
|
||||
return view('emails/email-confirmation', ['token' => $confirmation->token]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms an email via a token and logs the user into the system.
|
||||
* @param $token
|
||||
@@ -230,8 +195,8 @@ class AuthController extends Controller
|
||||
$user = $confirmation->user;
|
||||
$user->email_confirmed = true;
|
||||
$user->save();
|
||||
auth()->login($confirmation->user);
|
||||
session()->flash('success', 'Your email has been confirmed!');
|
||||
auth()->login($user);
|
||||
session()->flash('success', trans('auth.email_confirm_success'));
|
||||
$this->emailConfirmationService->deleteConfirmationsByUser($user);
|
||||
return redirect($this->redirectPath);
|
||||
}
|
||||
@@ -257,33 +222,19 @@ class AuthController extends Controller
|
||||
'email' => 'required|email|exists:users,email'
|
||||
]);
|
||||
$user = $this->userRepo->getByEmail($request->get('email'));
|
||||
|
||||
try {
|
||||
$this->emailConfirmationService->sendConfirmation($user);
|
||||
} catch (Exception $e) {
|
||||
session()->flash('error', trans('auth.email_confirm_send_error'));
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
$this->emailConfirmationService->sendConfirmation($user);
|
||||
session()->flash('success', 'Confirmation email resent, Please check your inbox.');
|
||||
session()->flash('success', trans('auth.email_confirm_resent'));
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the application login form.
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function getLogin()
|
||||
{
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
$authMethod = config('auth.method');
|
||||
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the relevant social site.
|
||||
* @param $socialDriver
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
public function getSocialLogin($socialDriver)
|
||||
{
|
||||
session()->put('social-callback', 'login');
|
||||
return $this->socialAuthService->startLogIn($socialDriver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the social site for authentication intended to register.
|
||||
* @param $socialDriver
|
||||
@@ -312,7 +263,7 @@ class AuthController extends Controller
|
||||
return $this->socialRegisterCallback($socialDriver);
|
||||
}
|
||||
} else {
|
||||
throw new SocialSignInException('No action defined', '/login');
|
||||
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
|
||||
}
|
||||
return redirect()->back();
|
||||
}
|
||||
@@ -327,4 +278,24 @@ class AuthController extends Controller
|
||||
return $this->socialAuthService->detachSocialAccount($socialDriver);
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Register a new user after a registration callback.
|
||||
* @param $socialDriver
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function socialRegisterCallback($socialDriver)
|
||||
{
|
||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
|
||||
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
|
||||
|
||||
// Create an array of the user data to create a new user instance
|
||||
$userData = [
|
||||
'name' => $socialUser->getName(),
|
||||
'email' => $socialUser->getEmail(),
|
||||
'password' => str_random(30)
|
||||
];
|
||||
return $this->registerUser($userData, $socialAccount);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace BookStack\Http\Controllers\Auth;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
|
||||
class PasswordController extends Controller
|
||||
class ResetPasswordController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -23,10 +23,27 @@ class PasswordController extends Controller
|
||||
protected $redirectTo = '/';
|
||||
|
||||
/**
|
||||
* Create a new password controller instance.
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response for a successful password reset.
|
||||
*
|
||||
* @param string $response
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
protected function sendResetResponse($response)
|
||||
{
|
||||
$message = trans('auth.reset_password_success');
|
||||
session()->flash('success', $message);
|
||||
return redirect($this->redirectPath())
|
||||
->with('status', trans($response));
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use BookStack\Repos\PageRepo;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
||||
class BookController extends Controller
|
||||
{
|
||||
|
||||
protected $bookRepo;
|
||||
protected $pageRepo;
|
||||
protected $chapterRepo;
|
||||
protected $entityRepo;
|
||||
protected $userRepo;
|
||||
protected $exportService;
|
||||
|
||||
/**
|
||||
* BookController constructor.
|
||||
* @param BookRepo $bookRepo
|
||||
* @param PageRepo $pageRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param UserRepo $userRepo
|
||||
* @param ExportService $exportService
|
||||
*/
|
||||
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->exportService = $exportService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the book.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$books = $this->bookRepo->getAllPaginated(10);
|
||||
$recents = $this->signedIn ? $this->bookRepo->getRecentlyViewed(4, 0) : false;
|
||||
$popular = $this->bookRepo->getPopular(4, 0);
|
||||
$books = $this->entityRepo->getAllPaginated('book', 10);
|
||||
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
|
||||
$popular = $this->entityRepo->getPopular('book', 4, 0);
|
||||
$this->setPageTitle('Books');
|
||||
return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new book.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$this->checkPermission('book-create');
|
||||
$this->setPageTitle('Create New Book');
|
||||
$this->checkPermission('book-create-all');
|
||||
$this->setPageTitle(trans('entities.books_create'));
|
||||
return view('books/create');
|
||||
}
|
||||
|
||||
@@ -68,30 +62,26 @@ class BookController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('book-create');
|
||||
$this->checkPermission('book-create-all');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
]);
|
||||
$book = $this->bookRepo->newFromInput($request->all());
|
||||
$book->slug = $this->bookRepo->findSuitableSlug($book->name);
|
||||
$book->created_by = Auth::user()->id;
|
||||
$book->updated_by = Auth::user()->id;
|
||||
$book->save();
|
||||
$book = $this->entityRepo->createFromInput('book', $request->all());
|
||||
Activity::add($book, 'book_create', $book->id);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified book.
|
||||
*
|
||||
* @param $slug
|
||||
* @return Response
|
||||
*/
|
||||
public function show($slug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$bookChildren = $this->bookRepo->getChildren($book);
|
||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||
$this->checkOwnablePermission('book-view', $book);
|
||||
$bookChildren = $this->entityRepo->getBookChildren($book);
|
||||
Views::add($book);
|
||||
$this->setPageTitle($book->getShortName());
|
||||
return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
|
||||
@@ -99,37 +89,32 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified book.
|
||||
*
|
||||
* @param $slug
|
||||
* @return Response
|
||||
*/
|
||||
public function edit($slug)
|
||||
{
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$this->setPageTitle('Edit Book ' . $book->getShortName());
|
||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()]));
|
||||
return view('books/edit', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified book in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param $slug
|
||||
* @return Response
|
||||
*/
|
||||
public function update(Request $request, $slug)
|
||||
{
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
]);
|
||||
$book->fill($request->all());
|
||||
$book->slug = $this->bookRepo->findSuitableSlug($book->name, $book->id);
|
||||
$book->updated_by = Auth::user()->id;
|
||||
$book->save();
|
||||
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
|
||||
Activity::add($book, 'book_update', $book->id);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
@@ -141,9 +126,9 @@ class BookController extends Controller
|
||||
*/
|
||||
public function showDelete($bookSlug)
|
||||
{
|
||||
$this->checkPermission('book-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->setPageTitle('Delete Book ' . $book->getShortName());
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
$this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
|
||||
return view('books/delete', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
|
||||
@@ -154,11 +139,11 @@ class BookController extends Controller
|
||||
*/
|
||||
public function sort($bookSlug)
|
||||
{
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$bookChildren = $this->bookRepo->getChildren($book);
|
||||
$books = $this->bookRepo->getAll();
|
||||
$this->setPageTitle('Sort Book ' . $book->getShortName());
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$bookChildren = $this->entityRepo->getBookChildren($book, true);
|
||||
$books = $this->entityRepo->getAll('book', false);
|
||||
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
|
||||
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
|
||||
}
|
||||
|
||||
@@ -170,43 +155,52 @@ class BookController extends Controller
|
||||
*/
|
||||
public function getSortItem($bookSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$bookChildren = $this->bookRepo->getChildren($book);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$bookChildren = $this->entityRepo->getBookChildren($book);
|
||||
return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an array of sort mapping to pages and chapters.
|
||||
*
|
||||
* @param string $bookSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function saveSort($bookSlug, Request $request)
|
||||
{
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
|
||||
// Return if no map sent
|
||||
if (!$request->has('sort-tree')) {
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
$sortedBooks = [];
|
||||
// Sort pages and chapters
|
||||
$sortedBooks = [];
|
||||
$updatedModels = collect();
|
||||
$sortMap = json_decode($request->get('sort-tree'));
|
||||
$defaultBookId = $book->id;
|
||||
foreach ($sortMap as $index => $bookChild) {
|
||||
$id = $bookChild->id;
|
||||
|
||||
// Loop through contents of provided map and update entities accordingly
|
||||
foreach ($sortMap as $bookChild) {
|
||||
$priority = $bookChild->sort;
|
||||
$id = intval($bookChild->id);
|
||||
$isPage = $bookChild->type == 'page';
|
||||
$bookId = $this->bookRepo->exists($bookChild->book) ? $bookChild->book : $defaultBookId;
|
||||
$model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
|
||||
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
|
||||
$model->priority = $index;
|
||||
if ($isPage) {
|
||||
$model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter;
|
||||
$bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId;
|
||||
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
|
||||
$model = $this->entityRepo->getById($isPage?'page':'chapter', $id);
|
||||
|
||||
// Update models only if there's a change in parent chain or ordering.
|
||||
if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
|
||||
$this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model);
|
||||
$model->priority = $priority;
|
||||
if ($isPage) $model->chapter_id = $chapterId;
|
||||
$model->save();
|
||||
$updatedModels->push($model);
|
||||
}
|
||||
$model->save();
|
||||
|
||||
// Store involved books to be sorted later
|
||||
if (!in_array($bookId, $sortedBooks)) {
|
||||
$sortedBooks[] = $bookId;
|
||||
}
|
||||
@@ -214,7 +208,9 @@ class BookController extends Controller
|
||||
|
||||
// Add activity for books
|
||||
foreach ($sortedBooks as $bookId) {
|
||||
$updatedBook = $this->bookRepo->getById($bookId);
|
||||
/** @var Book $updatedBook */
|
||||
$updatedBook = $this->entityRepo->getById('book', $bookId);
|
||||
$this->entityRepo->buildJointPermissionsForBook($updatedBook);
|
||||
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
|
||||
}
|
||||
|
||||
@@ -223,17 +219,92 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Remove the specified book from storage.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function destroy($bookSlug)
|
||||
{
|
||||
$this->checkPermission('book-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
Activity::addMessage('book_delete', 0, $book->name);
|
||||
Activity::removeEntity($book);
|
||||
$this->bookRepo->destroyBySlug($bookSlug);
|
||||
$this->entityRepo->destroyBook($book);
|
||||
return redirect('/books');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $bookSlug
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRestrict($bookSlug)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('books/restrictions', [
|
||||
'book' => $book,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the restrictions for this book.
|
||||
* @param $bookSlug
|
||||
* @param $bookSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function restrict($bookSlug, Request $request)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $book);
|
||||
session()->flash('success', trans('entities.books_permissions_updated'));
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a book as a PDF file.
|
||||
* @param string $bookSlug
|
||||
* @return mixed
|
||||
*/
|
||||
public function exportPdf($bookSlug)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$pdfContent = $this->exportService->bookToPdf($book);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a book as a contained HTML file.
|
||||
* @param string $bookSlug
|
||||
* @return mixed
|
||||
*/
|
||||
public function exportHtml($bookSlug)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$htmlContent = $this->exportService->bookToContainedHtml($book);
|
||||
return response()->make($htmlContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a book as a plain text file.
|
||||
* @param $bookSlug
|
||||
* @return mixed
|
||||
*/
|
||||
public function exportPlainText($bookSlug)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$htmlContent = $this->exportService->bookToPlainText($book);
|
||||
return response()->make($htmlContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
||||
class ChapterController extends Controller
|
||||
{
|
||||
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $userRepo;
|
||||
protected $entityRepo;
|
||||
protected $exportService;
|
||||
|
||||
/**
|
||||
* ChapterController constructor.
|
||||
* @param $bookRepo
|
||||
* @param $chapterRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param UserRepo $userRepo
|
||||
* @param ExportService $exportService
|
||||
*/
|
||||
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->exportService = $exportService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the form for creating a new chapter.
|
||||
* @param $bookSlug
|
||||
@@ -38,9 +36,9 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function create($bookSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-create');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->setPageTitle('Create New Chapter');
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-create', $book);
|
||||
$this->setPageTitle(trans('entities.chapters_create'));
|
||||
return view('chapters/create', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
|
||||
@@ -52,18 +50,16 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function store($bookSlug, Request $request)
|
||||
{
|
||||
$this->checkPermission('chapter-create');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->newFromInput($request->all());
|
||||
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id);
|
||||
$chapter->priority = $this->bookRepo->getNewPriority($book);
|
||||
$chapter->created_by = auth()->user()->id;
|
||||
$chapter->updated_by = auth()->user()->id;
|
||||
$book->chapters()->save($chapter);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-create', $book);
|
||||
|
||||
$input = $request->all();
|
||||
$input['priority'] = $this->entityRepo->getNewBookPriority($book);
|
||||
$chapter = $this->entityRepo->createFromInput('chapter', $input, $book);
|
||||
Activity::add($chapter, 'chapter_create', $book->id);
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
@@ -76,12 +72,19 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function show($bookSlug, $chapterSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-view', $chapter);
|
||||
$sidebarTree = $this->entityRepo->getBookChildren($chapter->book);
|
||||
Views::add($chapter);
|
||||
$this->setPageTitle($chapter->getShortName());
|
||||
return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]);
|
||||
$pages = $this->entityRepo->getChapterChildren($chapter);
|
||||
return view('chapters/show', [
|
||||
'book' => $chapter->book,
|
||||
'chapter' => $chapter,
|
||||
'current' => $chapter,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'pages' => $pages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,11 +95,10 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function edit($bookSlug, $chapterSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->setPageTitle('Edit Chapter' . $chapter->getShortName());
|
||||
return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
|
||||
return view('chapters/edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,14 +110,15 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function update(Request $request, $bookSlug, $chapterSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
if ($chapter->name !== $request->get('name')) {
|
||||
$chapter->slug = $this->entityRepo->findSuitableSlug('chapter', $request->get('name'), $chapter->id, $chapter->book->id);
|
||||
}
|
||||
$chapter->fill($request->all());
|
||||
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
|
||||
$chapter->updated_by = auth()->user()->id;
|
||||
$chapter->updated_by = user()->id;
|
||||
$chapter->save();
|
||||
Activity::add($chapter, 'chapter_update', $book->id);
|
||||
Activity::add($chapter, 'chapter_update', $chapter->book->id);
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
@@ -127,11 +130,10 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function showDelete($bookSlug, $chapterSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$this->setPageTitle('Delete Chapter' . $chapter->getShortName());
|
||||
return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
|
||||
return view('chapters/delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,11 +144,148 @@ class ChapterController extends Controller
|
||||
*/
|
||||
public function destroy($bookSlug, $chapterSlug)
|
||||
{
|
||||
$this->checkPermission('chapter-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$book = $chapter->book;
|
||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||
Activity::addMessage('chapter_delete', $book->id, $chapter->name);
|
||||
$this->chapterRepo->destroy($chapter);
|
||||
$this->entityRepo->destroyChapter($chapter);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page for moving a chapter.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function showMove($bookSlug, $chapterSlug) {
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
return view('chapters/move', [
|
||||
'chapter' => $chapter,
|
||||
'book' => $chapter->book
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the move action for a chapter.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function move($bookSlug, $chapterSlug, Request $request) {
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
|
||||
$entitySelection = $request->get('entity_selection', null);
|
||||
if ($entitySelection === null || $entitySelection === '') {
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
$stringExploded = explode(':', $entitySelection);
|
||||
$entityType = $stringExploded[0];
|
||||
$entityId = intval($stringExploded[1]);
|
||||
|
||||
$parent = false;
|
||||
|
||||
if ($entityType == 'book') {
|
||||
$parent = $this->entityRepo->getById('book', $entityId);
|
||||
}
|
||||
|
||||
if ($parent === false || $parent === null) {
|
||||
session()->flash('error', trans('errors.selected_book_not_found'));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
$this->entityRepo->changeBook('chapter', $parent->id, $chapter, true);
|
||||
Activity::add($chapter, 'chapter_move', $chapter->book->id);
|
||||
session()->flash('success', trans('entities.chapter_move_success', ['bookName' => $parent->name]));
|
||||
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRestrict($bookSlug, $chapterSlug)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('chapters/restrictions', [
|
||||
'chapter' => $chapter,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the restrictions for this chapter.
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function restrict($bookSlug, $chapterSlug, Request $request)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter);
|
||||
session()->flash('success', trans('entities.chapters_permissions_success'));
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a chapter to pdf .
|
||||
* @param string $bookSlug
|
||||
* @param string $chapterSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportPdf($bookSlug, $chapterSlug)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$pdfContent = $this->exportService->chapterToPdf($chapter);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a chapter to a self-contained HTML file.
|
||||
* @param string $bookSlug
|
||||
* @param string $chapterSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportHtml($bookSlug, $chapterSlug)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$containedHtml = $this->exportService->chapterToContainedHtml($chapter);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a chapter to a simple plaintext .txt file.
|
||||
* @param string $bookSlug
|
||||
* @param string $chapterSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportPlainText($bookSlug, $chapterSlug)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$containedHtml = $this->exportService->chapterToPlainText($chapter);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use HttpRequestException;
|
||||
use BookStack\Ownable;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Http\Exception\HttpResponseException;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use BookStack\User;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
@@ -29,17 +28,21 @@ abstract class Controller extends BaseController
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// Get a user instance for the current user
|
||||
$user = auth()->user();
|
||||
if (!$user) $user = User::getDefault();
|
||||
$this->middleware(function ($request, $next) {
|
||||
|
||||
// Share variables with views
|
||||
view()->share('signedIn', auth()->check());
|
||||
view()->share('currentUser', $user);
|
||||
// Get a user instance for the current user
|
||||
$user = user();
|
||||
|
||||
// Share variables with controllers
|
||||
$this->currentUser = $user;
|
||||
$this->signedIn = auth()->check();
|
||||
// Share variables with controllers
|
||||
$this->currentUser = $user;
|
||||
$this->signedIn = auth()->check();
|
||||
|
||||
// Share variables with views
|
||||
view()->share('signedIn', $this->signedIn);
|
||||
view()->share('currentUser', $user);
|
||||
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,32 +64,46 @@ abstract class Controller extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* On a permission error redirect to home and display
|
||||
* On a permission error redirect to home and display.
|
||||
* the error as a notification.
|
||||
*/
|
||||
protected function showPermissionError()
|
||||
{
|
||||
Session::flash('error', trans('errors.permission'));
|
||||
throw new HttpResponseException(
|
||||
redirect('/')
|
||||
);
|
||||
if (request()->wantsJson()) {
|
||||
$response = response()->json(['error' => trans('errors.permissionJson')], 403);
|
||||
} else {
|
||||
$response = redirect('/');
|
||||
session()->flash('error', trans('errors.permission'));
|
||||
}
|
||||
|
||||
throw new HttpResponseException($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for a permission.
|
||||
*
|
||||
* @param $permissionName
|
||||
* @param string $permissionName
|
||||
* @return bool|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
protected function checkPermission($permissionName)
|
||||
{
|
||||
if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
|
||||
if (!user() || !user()->can($permissionName)) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the current user's permissions against an ownable item.
|
||||
* @param $permission
|
||||
* @param Ownable $ownable
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkOwnablePermission($permission, Ownable $ownable)
|
||||
{
|
||||
if (userCan($permission, $ownable)) return true;
|
||||
return $this->showPermissionError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has a permission or bypass if the callback is true.
|
||||
* @param $permissionName
|
||||
@@ -100,4 +117,33 @@ abstract class Controller extends BaseController
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send back a json error message.
|
||||
* @param string $messageText
|
||||
* @param int $statusCode
|
||||
* @return mixed
|
||||
*/
|
||||
protected function jsonError($messageText = "", $statusCode = 500)
|
||||
{
|
||||
return response()->json(['message' => $messageText], $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the response for when a request fails validation.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param array $errors
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function buildFailedValidationResponse(Request $request, array $errors)
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['validation' => $errors], 422);
|
||||
}
|
||||
|
||||
return redirect()->to($this->getRedirectUrl())
|
||||
->withInput($request->input())
|
||||
->withErrors($errors, $this->errorBag());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,41 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
|
||||
protected $activityService;
|
||||
protected $bookRepo;
|
||||
protected $entityRepo;
|
||||
|
||||
/**
|
||||
* HomeController constructor.
|
||||
* @param BookRepo $bookRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
*/
|
||||
public function __construct(BookRepo $bookRepo)
|
||||
public function __construct(EntityRepo $entityRepo)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->entityRepo = $entityRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display the homepage.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$activity = Activity::latest();
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(10, 0) : $this->bookRepo->getLatest(10);
|
||||
return view('home', ['activity' => $activity, 'recents' => $recents]);
|
||||
$activity = Activity::latest(10);
|
||||
$draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
|
||||
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 10*$recentFactor);
|
||||
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreated('page', 5);
|
||||
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 5);
|
||||
return view('home', [
|
||||
'activity' => $activity,
|
||||
'recents' => $recents,
|
||||
'recentlyCreatedPages' => $recentlyCreatedPages,
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||
'draftPages' => $draftPages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a js representation of the current translations
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function getTranslations() {
|
||||
$locale = app()->getLocale();
|
||||
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
|
||||
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
|
||||
$resp = cache($cacheKey);
|
||||
} else {
|
||||
$translations = [
|
||||
// Get only translations which might be used in JS
|
||||
'common' => trans('common'),
|
||||
'components' => trans('components'),
|
||||
'entities' => trans('entities'),
|
||||
'errors' => trans('errors')
|
||||
];
|
||||
if ($locale !== 'en') {
|
||||
$enTrans = [
|
||||
'common' => trans('common', [], 'en'),
|
||||
'components' => trans('components', [], 'en'),
|
||||
'entities' => trans('entities', [], 'en'),
|
||||
'errors' => trans('errors', [], 'en')
|
||||
];
|
||||
$translations = array_replace_recursive($enTrans, $translations);
|
||||
}
|
||||
$resp = 'window.translations = ' . json_encode($translations);
|
||||
cache()->put($cacheKey, $resp, 120);
|
||||
}
|
||||
|
||||
return response($resp, 200, [
|
||||
'Content-Type' => 'application/javascript'
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\ImageRepo;
|
||||
use Illuminate\Filesystem\Filesystem as File;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Intervention\Image\Facades\Image as ImageTool;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use BookStack\Image;
|
||||
use BookStack\Repos\PageRepo;
|
||||
|
||||
@@ -20,8 +16,8 @@ class ImageController extends Controller
|
||||
|
||||
/**
|
||||
* ImageController constructor.
|
||||
* @param Image $image
|
||||
* @param File $file
|
||||
* @param Image $image
|
||||
* @param File $file
|
||||
* @param ImageRepo $imageRepo
|
||||
*/
|
||||
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
|
||||
@@ -32,9 +28,9 @@ class ImageController extends Controller
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all images for a specific type, Paginated
|
||||
* @param string $type
|
||||
* @param int $page
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
@@ -44,6 +40,24 @@ class ImageController extends Controller
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through images within a particular type.
|
||||
* @param $type
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchByType($type, $page = 0, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'term' => 'required|string'
|
||||
]);
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm);
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all images for a user.
|
||||
* @param int $page
|
||||
@@ -55,24 +69,46 @@ class ImageController extends Controller
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gallery images with a specific filter such as book or page
|
||||
* @param $filter
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function getGalleryFiltered($filter, $page = 0, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page_id' => 'required|integer'
|
||||
]);
|
||||
|
||||
$validFilters = collect(['page', 'book']);
|
||||
if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
|
||||
|
||||
$pageId = $request->get('page_id');
|
||||
$imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
|
||||
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles image uploads for use on pages.
|
||||
* @param string $type
|
||||
* @param string $type
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function uploadByType($type, Request $request)
|
||||
{
|
||||
$this->checkPermission('image-create');
|
||||
$this->checkPermission('image-create-all');
|
||||
$this->validate($request, [
|
||||
'file' => 'image|mimes:jpeg,gif,png'
|
||||
'file' => 'is_image'
|
||||
]);
|
||||
|
||||
$imageUpload = $request->file('file');
|
||||
|
||||
try {
|
||||
$image = $this->imageRepo->saveNew($imageUpload, $type);
|
||||
$uploadedTo = $request->has('uploaded_to') ? $request->get('uploaded_to') : 0;
|
||||
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
|
||||
} catch (ImageUploadException $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
}
|
||||
@@ -90,7 +126,7 @@ class ImageController extends Controller
|
||||
*/
|
||||
public function getThumbnail($id, $width, $height, $crop)
|
||||
{
|
||||
$this->checkPermission('image-create');
|
||||
$this->checkPermission('image-create-all');
|
||||
$image = $this->imageRepo->getById($id);
|
||||
$thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
|
||||
return response()->json(['url' => $thumbnailUrl]);
|
||||
@@ -98,45 +134,44 @@ class ImageController extends Controller
|
||||
|
||||
/**
|
||||
* Update image details
|
||||
* @param $imageId
|
||||
* @param integer $imageId
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function update($imageId, Request $request)
|
||||
{
|
||||
$this->checkPermission('image-update');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|min:2|string'
|
||||
]);
|
||||
$image = $this->imageRepo->getById($imageId);
|
||||
$this->checkOwnablePermission('image-update', $image);
|
||||
$image = $this->imageRepo->updateImageDetails($image, $request->all());
|
||||
return response()->json($image);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes an image and all thumbnail/image files
|
||||
* @param PageRepo $pageRepo
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function destroy(PageRepo $pageRepo, Request $request, $id)
|
||||
public function destroy(EntityRepo $entityRepo, Request $request, $id)
|
||||
{
|
||||
$this->checkPermission('image-delete');
|
||||
$image = $this->imageRepo->getById($id);
|
||||
$this->checkOwnablePermission('image-delete', $image);
|
||||
|
||||
// Check if this image is used on any pages
|
||||
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
|
||||
if (!$isForced) {
|
||||
$pageSearch = $pageRepo->searchForImage($image->url);
|
||||
$pageSearch = $entityRepo->searchForImage($image->url);
|
||||
if ($pageSearch !== false) {
|
||||
return response()->json($pageSearch, 400);
|
||||
}
|
||||
}
|
||||
|
||||
$this->imageRepo->destroyImage($image);
|
||||
return response()->json('Image Deleted');
|
||||
return response()->json(trans('components.images_deleted'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,79 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use BookStack\Repos\PageRepo;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
use GatherContent\Htmldiff\Htmldiff;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
|
||||
protected $pageRepo;
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $entityRepo;
|
||||
protected $exportService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* PageController constructor.
|
||||
* @param PageRepo $pageRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param ExportService $exportService
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService)
|
||||
public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->exportService = $exportService;
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new page.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @param bool $chapterSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $chapterSlug
|
||||
* @return Response
|
||||
* @internal param bool $pageSlug
|
||||
*/
|
||||
public function create($bookSlug, $chapterSlug = false)
|
||||
public function create($bookSlug, $chapterSlug = null)
|
||||
{
|
||||
$this->checkPermission('page-create');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false;
|
||||
$this->setPageTitle('Create New Page');
|
||||
return view('pages/create', ['book' => $book, 'chapter' => $chapter]);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
|
||||
$parent = $chapter ? $chapter : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
// Redirect to draft edit screen if signed in
|
||||
if ($this->signedIn) {
|
||||
$draft = $this->entityRepo->getDraftPage($book, $chapter);
|
||||
return redirect($draft->getUrl());
|
||||
}
|
||||
|
||||
// Otherwise show edit view
|
||||
$this->setPageTitle(trans('entities.pages_new'));
|
||||
return view('pages/guest-create', ['parent' => $parent]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created page in storage.
|
||||
*
|
||||
* Create a new page as a guest user.
|
||||
* @param Request $request
|
||||
* @param string $bookSlug
|
||||
* @param string|null $chapterSlug
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
|
||||
$parent = $chapter ? $chapter : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
$page = $this->entityRepo->getDraftPage($book, $chapter);
|
||||
$this->entityRepo->publishPageDraft($page, [
|
||||
'name' => $request->get('name'),
|
||||
'html' => ''
|
||||
]);
|
||||
return redirect($page->getUrl('/edit'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show form to continue editing a draft page.
|
||||
* @param string $bookSlug
|
||||
* @param int $pageId
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function editDraft($bookSlug, $pageId)
|
||||
{
|
||||
$draft = $this->entityRepo->getById('page', $pageId, true);
|
||||
$this->checkOwnablePermission('page-create', $draft->book);
|
||||
$this->setPageTitle(trans('entities.pages_edit_draft'));
|
||||
|
||||
$draftsEnabled = $this->signedIn;
|
||||
return view('pages/edit', [
|
||||
'page' => $draft,
|
||||
'book' => $draft->book,
|
||||
'isDraft' => true,
|
||||
'draftsEnabled' => $draftsEnabled
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new page by changing a draft into a page.
|
||||
* @param Request $request
|
||||
* @param $bookSlug
|
||||
* @param string $bookSlug
|
||||
* @param int $pageId
|
||||
* @return Response
|
||||
*/
|
||||
public function store(Request $request, $bookSlug)
|
||||
public function store(Request $request, $bookSlug, $pageId)
|
||||
{
|
||||
$this->checkPermission('page-create');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255'
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
$input = $request->all();
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null;
|
||||
$input['priority'] = $this->bookRepo->getNewPriority($book);
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
|
||||
$page = $this->pageRepo->saveNew($input, $book, $chapterId);
|
||||
$draftPage = $this->entityRepo->getById('page', $pageId, true);
|
||||
|
||||
$chapterId = intval($draftPage->chapter_id);
|
||||
$parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
if ($parent->isA('chapter')) {
|
||||
$input['priority'] = $this->entityRepo->getNewChapterPriority($parent);
|
||||
} else {
|
||||
$input['priority'] = $this->entityRepo->getNewBookPriority($parent);
|
||||
}
|
||||
|
||||
$page = $this->entityRepo->publishPageDraft($draftPage, $input);
|
||||
|
||||
Activity::add($page, 'page_create', $book->id);
|
||||
return redirect($page->getUrl());
|
||||
@@ -81,201 +141,453 @@ class PageController extends Controller
|
||||
|
||||
/**
|
||||
* Display the specified page.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* If the page is not found via the slug the revisions are searched for a match.
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function show($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
try {
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
} catch (NotFoundException $e) {
|
||||
$page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
|
||||
if ($page === null) abort(404);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
$pageContent = $this->entityRepo->renderPage($page);
|
||||
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
|
||||
$pageNav = $this->entityRepo->getPageNav($pageContent);
|
||||
|
||||
Views::add($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]);
|
||||
return view('pages/show', [
|
||||
'page' => $page,'book' => $page->book,
|
||||
'current' => $page, 'sidebarTree' => $sidebarTree,
|
||||
'pageNav' => $pageNav, 'pageContent' => $pageContent]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page from an ajax request.
|
||||
* @param int $pageId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getPageAjax($pageId)
|
||||
{
|
||||
$page = $this->entityRepo->getById('page', $pageId);
|
||||
return response()->json($page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified page.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function edit($bookSlug, $pageSlug)
|
||||
{
|
||||
$this->checkPermission('page-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->setPageTitle('Editing Page ' . $page->getShortName());
|
||||
return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
|
||||
$page->isDraft = false;
|
||||
|
||||
// Check for active editing
|
||||
$warnings = [];
|
||||
if ($this->entityRepo->isPageEditingActive($page, 60)) {
|
||||
$warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60);
|
||||
}
|
||||
|
||||
// Check for a current draft version for this user
|
||||
if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
|
||||
$draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id);
|
||||
$page->name = $draft->name;
|
||||
$page->html = $draft->html;
|
||||
$page->markdown = $draft->markdown;
|
||||
$page->isDraft = true;
|
||||
$warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
|
||||
}
|
||||
|
||||
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
|
||||
|
||||
$draftsEnabled = $this->signedIn;
|
||||
return view('pages/edit', [
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'current' => $page,
|
||||
'draftsEnabled' => $draftsEnabled
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified page in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return Response
|
||||
*/
|
||||
public function update(Request $request, $bookSlug, $pageSlug)
|
||||
{
|
||||
$this->checkPermission('page-update');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255'
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->pageRepo->updatePage($page, $book->id, $request->all());
|
||||
Activity::add($page, 'page_update', $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->entityRepo->updatePage($page, $page->book->id, $request->all());
|
||||
Activity::add($page, 'page_update', $page->book->id);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a draft update as a revision.
|
||||
* @param Request $request
|
||||
* @param int $pageId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function saveDraft(Request $request, $pageId)
|
||||
{
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
if (!$this->signedIn) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => trans('errors.guests_cannot_save_drafts'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
|
||||
|
||||
$updateTime = $draft->updated_at->timestamp;
|
||||
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => trans('entities.pages_edit_draft_save_at'),
|
||||
'timestamp' => $utcUpdateTimestamp
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect from a special link url which
|
||||
* uses the page id rather than the name.
|
||||
* @param $pageId
|
||||
* @param int $pageId
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function redirectFromLink($pageId)
|
||||
{
|
||||
$page = $this->pageRepo->getById($pageId);
|
||||
$page = $this->entityRepo->getById('page', $pageId);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the deletion page for the specified page.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function showDelete($bookSlug, $pageSlug)
|
||||
{
|
||||
$this->checkPermission('page-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->setPageTitle('Delete Page ' . $page->getShortName());
|
||||
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
|
||||
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the deletion page for the specified page.
|
||||
* @param string $bookSlug
|
||||
* @param int $pageId
|
||||
* @return \Illuminate\View\View
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function showDeleteDraft($bookSlug, $pageId)
|
||||
{
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
|
||||
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified page from storage.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return Response
|
||||
* @internal param int $id
|
||||
*/
|
||||
public function destroy($bookSlug, $pageSlug)
|
||||
{
|
||||
$this->checkPermission('page-delete');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$book = $page->book;
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
Activity::addMessage('page_delete', $book->id, $page->name);
|
||||
$this->pageRepo->destroy($page);
|
||||
session()->flash('success', trans('entities.pages_delete_success'));
|
||||
$this->entityRepo->destroyPage($page);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified draft page from storage.
|
||||
* @param string $bookSlug
|
||||
* @param int $pageId
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function destroyDraft($bookSlug, $pageId)
|
||||
{
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$book = $page->book;
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
session()->flash('success', trans('entities.pages_delete_draft_success'));
|
||||
$this->entityRepo->destroyPage($page);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the last revisions for this page.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function showRevisions($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$this->setPageTitle('Revisions For ' . $page->getShortName());
|
||||
return view('pages/revisions', ['page' => $page, 'book' => $book, 'current' => $page]);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
|
||||
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a preview of a single revision
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param $revisionId
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param int $revisionId
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function showRevision($bookSlug, $pageSlug, $revisionId)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$revision = $this->pageRepo->getRevisionById($revisionId);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||
if ($revision === null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$page->fill($revision->toArray());
|
||||
$this->setPageTitle('Page Revision For ' . $page->getShortName());
|
||||
return view('pages/revision', ['page' => $page, 'book' => $book]);
|
||||
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
|
||||
|
||||
return view('pages/revision', [
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the changes of a single revision
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param int $revisionId
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||
if ($revision === null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$prev = $revision->getPrevious();
|
||||
$prevContent = ($prev === null) ? '' : $prev->html;
|
||||
$diff = (new Htmldiff)->diff($prevContent, $revision->html);
|
||||
|
||||
$page->fill($revision->toArray());
|
||||
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
|
||||
|
||||
return view('pages/revision', [
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'diff' => $diff,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a page using the content of the specified revision.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param $revisionId
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param int $revisionId
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
|
||||
{
|
||||
$this->checkPermission('page-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
|
||||
Activity::add($page, 'page_restore', $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId);
|
||||
Activity::add($page, 'page_restore', $page->book->id);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
|
||||
* Exports a page to a PDF.
|
||||
* https://github.com/barryvdh/laravel-dompdf
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportPdf($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$pdfContent = $this->exportService->pageToPdf($page);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.pdf'
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a page to a self-contained HTML file.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportHtml($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$containedHtml = $this->exportService->pageToContainedHtml($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html'
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a page to a simple plaintext .txt file.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportPlainText($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$containedHtml = $this->exportService->pageToPlainText($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.txt'
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a listing of recently created pages
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRecentlyCreated()
|
||||
{
|
||||
$pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => trans('entities.recently_created_pages'),
|
||||
'pages' => $pages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a listing of recently created pages
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRecentlyUpdated()
|
||||
{
|
||||
$pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => trans('entities.recently_updated_pages'),
|
||||
'pages' => $pages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRestrict($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('pages/restrictions', [
|
||||
'page' => $page,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the view to choose a new parent to move a page into.
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function showMove($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
return view('pages/move', [
|
||||
'book' => $page->book,
|
||||
'page' => $page
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the action of moving the location of a page
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function move($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$entitySelection = $request->get('entity_selection', null);
|
||||
if ($entitySelection === null || $entitySelection === '') {
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
$stringExploded = explode(':', $entitySelection);
|
||||
$entityType = $stringExploded[0];
|
||||
$entityId = intval($stringExploded[1]);
|
||||
|
||||
|
||||
try {
|
||||
$parent = $this->entityRepo->getById($entityType, $entityId);
|
||||
} catch (\Exception $e) {
|
||||
session()->flash(trans('entities.selected_book_chapter_not_found'));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
$this->entityRepo->changePageParent($page, $parent);
|
||||
Activity::add($page, 'page_move', $page->book->id);
|
||||
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
|
||||
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the permissions for this page.
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function restrict($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $page);
|
||||
session()->flash('success', trans('entities.pages_permissions_success'));
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
130
app/Http/Controllers/PermissionController.php
Normal file
130
app/Http/Controllers/PermissionController.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Repos\PermissionsRepo;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PermissionController extends Controller
|
||||
{
|
||||
|
||||
protected $permissionsRepo;
|
||||
|
||||
/**
|
||||
* PermissionController constructor.
|
||||
* @param PermissionsRepo $permissionsRepo
|
||||
*/
|
||||
public function __construct(PermissionsRepo $permissionsRepo)
|
||||
{
|
||||
$this->permissionsRepo = $permissionsRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a listing of the roles in the system.
|
||||
*/
|
||||
public function listRoles()
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$roles = $this->permissionsRepo->getAllRoles();
|
||||
return view('settings/roles/index', ['roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form to create a new role
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function createRole()
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
return view('settings/roles/create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new role in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function storeRole(Request $request)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$this->validate($request, [
|
||||
'display_name' => 'required|min:3|max:200',
|
||||
'description' => 'max:250'
|
||||
]);
|
||||
|
||||
$this->permissionsRepo->saveNewRole($request->all());
|
||||
session()->flash('success', trans('settings.role_create_success'));
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing a user role.
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function editRole($id)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
if ($role->hidden) throw new PermissionsException(trans('errors.role_cannot_be_edited'));
|
||||
return view('settings/roles/edit', ['role' => $role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a user role.
|
||||
* @param $id
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function updateRole($id, Request $request)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$this->validate($request, [
|
||||
'display_name' => 'required|min:3|max:200',
|
||||
'description' => 'max:250'
|
||||
]);
|
||||
|
||||
$this->permissionsRepo->updateRole($id, $request->all());
|
||||
session()->flash('success', trans('settings.role_update_success'));
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the view to delete a role.
|
||||
* Offers the chance to migrate users.
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showDeleteRole($id)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
$roles = $this->permissionsRepo->getAllRolesExcept($role);
|
||||
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
|
||||
$roles->prepend($blankRole);
|
||||
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a role from the system,
|
||||
* Migrate from a previous role if set.
|
||||
* @param $id
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function deleteRole($id, Request $request)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
|
||||
try {
|
||||
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
|
||||
} catch (PermissionsException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
session()->flash('success', trans('settings.role_delete_success'));
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Services\SearchService;
|
||||
use BookStack\Services\ViewService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use BookStack\Repos\PageRepo;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
protected $pageRepo;
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $entityRepo;
|
||||
protected $viewService;
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* SearchController constructor.
|
||||
* @param $pageRepo
|
||||
* @param $bookRepo
|
||||
* @param $chapterRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param ViewService $viewService
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->viewService = $viewService;
|
||||
$this->searchService = $searchService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -36,19 +31,27 @@ class SearchController extends Controller
|
||||
* @return \Illuminate\View\View
|
||||
* @internal param string $searchTerm
|
||||
*/
|
||||
public function searchAll(Request $request)
|
||||
public function search(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) {
|
||||
return redirect()->back();
|
||||
}
|
||||
$searchTerm = $request->get('term');
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm);
|
||||
$books = $this->bookRepo->getBySearch($searchTerm);
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm);
|
||||
$this->setPageTitle('Search For ' . $searchTerm);
|
||||
return view('search/all', ['pages' => $pages, 'books' => $books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
|
||||
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
|
||||
|
||||
$page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1;
|
||||
$nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
|
||||
|
||||
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
|
||||
$hasNextPage = $this->searchService->searchEntities($searchTerm, 'all', $page+1, 20)['count'] > 0;
|
||||
|
||||
return view('search/all', [
|
||||
'entities' => $results['results'],
|
||||
'totalResults' => $results['total'],
|
||||
'searchTerm' => $searchTerm,
|
||||
'hasNextPage' => $hasNextPage,
|
||||
'nextPageLink' => $nextPageLink
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Searches all entities within a book.
|
||||
* @param Request $request
|
||||
@@ -58,14 +61,50 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function searchBook(Request $request, $bookId)
|
||||
{
|
||||
if (!$request->has('term')) {
|
||||
return redirect()->back();
|
||||
$term = $request->get('term', '');
|
||||
$results = $this->searchService->searchBook($bookId, $term);
|
||||
return view('partials/entity-list', ['entities' => $results]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches all entities within a chapter.
|
||||
* @param Request $request
|
||||
* @param integer $chapterId
|
||||
* @return \Illuminate\View\View
|
||||
* @internal param string $searchTerm
|
||||
*/
|
||||
public function searchChapter(Request $request, $chapterId)
|
||||
{
|
||||
$term = $request->get('term', '');
|
||||
$results = $this->searchService->searchChapter($chapterId, $term);
|
||||
return view('partials/entity-list', ['entities' => $results]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a list of entities and return a partial HTML response of matching entities.
|
||||
* Returns the most popular entities if no search is provided.
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchEntitiesAjax(Request $request)
|
||||
{
|
||||
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
|
||||
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
|
||||
|
||||
// Search for entities otherwise show most popular
|
||||
if ($searchTerm !== false) {
|
||||
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
|
||||
$entities = $this->searchService->searchEntities($searchTerm)['results'];
|
||||
} else {
|
||||
$entityNames = $entityTypes->map(function ($type) {
|
||||
return 'BookStack\\' . ucfirst($type);
|
||||
})->toArray();
|
||||
$entities = $this->viewService->getPopular(20, 0, $entityNames);
|
||||
}
|
||||
$searchTerm = $request->get('term');
|
||||
$searchWhereTerms = [['book_id', '=', $bookId]];
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm, $searchWhereTerms);
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, $searchWhereTerms);
|
||||
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
|
||||
|
||||
return view('search/entity-ajax-list', ['entities' => $entities]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,47 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Response;
|
||||
use Setting;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the settings.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->checkPermission('settings-update');
|
||||
$this->checkPermission('settings-manage');
|
||||
$this->setPageTitle('Settings');
|
||||
return view('settings/index');
|
||||
}
|
||||
|
||||
// Get application version
|
||||
$version = trim(file_get_contents(base_path('version')));
|
||||
|
||||
return view('settings/index', ['version' => $version]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified settings in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$this->preventAccessForDemoUsers();
|
||||
$this->checkPermission('settings-update');
|
||||
$this->checkPermission('settings-manage');
|
||||
|
||||
// Cycles through posted settings and update them
|
||||
foreach($request->all() as $name => $value) {
|
||||
if(strpos($name, 'setting-') !== 0) continue;
|
||||
foreach ($request->all() as $name => $value) {
|
||||
if (strpos($name, 'setting-') !== 0) continue;
|
||||
$key = str_replace('setting-', '', trim($name));
|
||||
Setting::put($key, $value);
|
||||
}
|
||||
|
||||
session()->flash('success', 'Settings Saved');
|
||||
session()->flash('success', trans('settings.settings_save_success'));
|
||||
return redirect('/settings');
|
||||
}
|
||||
|
||||
|
||||
58
app/Http/Controllers/TagController.php
Normal file
58
app/Http/Controllers/TagController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Repos\TagRepo;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
|
||||
protected $tagRepo;
|
||||
|
||||
/**
|
||||
* TagController constructor.
|
||||
* @param $tagRepo
|
||||
*/
|
||||
public function __construct(TagRepo $tagRepo)
|
||||
{
|
||||
$this->tagRepo = $tagRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the Tags for a particular entity
|
||||
* @param $entityType
|
||||
* @param $entityId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getForEntity($entityType, $entityId)
|
||||
{
|
||||
$tags = $this->tagRepo->getForEntity($entityType, $entityId);
|
||||
return response()->json($tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag name suggestions from a given search term.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getNameSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->has('search') ? $request->get('search') : false;
|
||||
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
|
||||
return response()->json($suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag value suggestions from a given search term.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getValueSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->has('search') ? $request->get('search') : false;
|
||||
$tagName = $request->has('name') ? $request->get('name') : false;
|
||||
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
|
||||
return response()->json($suggestions);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use BookStack\User;
|
||||
@@ -30,13 +27,21 @@ class UserController extends Controller
|
||||
|
||||
/**
|
||||
* Display a listing of the users.
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
$users = $this->user->all();
|
||||
$this->setPageTitle('Users');
|
||||
return view('users/index', ['users' => $users]);
|
||||
$this->checkPermission('users-manage');
|
||||
$listDetails = [
|
||||
'order' => $request->has('order') ? $request->get('order') : 'asc',
|
||||
'search' => $request->has('search') ? $request->get('search') : '',
|
||||
'sort' => $request->has('sort') ? $request->get('sort') : 'name',
|
||||
];
|
||||
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
|
||||
$this->setPageTitle(trans('settings.users'));
|
||||
$users->appends($listDetails);
|
||||
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,9 +50,10 @@ class UserController extends Controller
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$this->checkPermission('user-create');
|
||||
$this->checkPermission('users-manage');
|
||||
$authMethod = config('auth.method');
|
||||
return view('users/create', ['authMethod' => $authMethod]);
|
||||
$roles = $this->userRepo->getAllRoles();
|
||||
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,11 +63,10 @@ class UserController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('user-create');
|
||||
$this->checkPermission('users-manage');
|
||||
$validationRules = [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'role' => 'required|exists:roles,id'
|
||||
'email' => 'required|email|unique:users,email'
|
||||
];
|
||||
|
||||
$authMethod = config('auth.method');
|
||||
@@ -73,7 +78,6 @@ class UserController extends Controller
|
||||
}
|
||||
$this->validate($request, $validationRules);
|
||||
|
||||
|
||||
$user = $this->user->fill($request->all());
|
||||
|
||||
if ($authMethod === 'standard') {
|
||||
@@ -83,19 +87,27 @@ class UserController extends Controller
|
||||
}
|
||||
|
||||
$user->save();
|
||||
$user->attachRoleId($request->get('role'));
|
||||
|
||||
if ($request->has('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
$user->roles()->sync($roles);
|
||||
}
|
||||
|
||||
// Get avatar from gravatar and save
|
||||
if (!config('services.disable_services')) {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
try {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
} catch (Exception $e) {
|
||||
\Log::error('Failed to save user gravatar image');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return redirect('/users');
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified user.
|
||||
* @param int $id
|
||||
@@ -104,16 +116,18 @@ class UserController extends Controller
|
||||
*/
|
||||
public function edit($id, SocialAuthService $socialAuthService)
|
||||
{
|
||||
$this->checkPermissionOr('user-update', function () use ($id) {
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$authMethod = config('auth.method');
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
|
||||
$authMethod = ($user->system_name) ? 'system' : config('auth.method');
|
||||
|
||||
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
||||
$this->setPageTitle('User Profile');
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod]);
|
||||
$this->setPageTitle(trans('settings.user_profile'));
|
||||
$roles = $this->userRepo->getAllRoles();
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,7 +139,7 @@ class UserController extends Controller
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$this->preventAccessForDemoUsers();
|
||||
$this->checkPermissionOr('user-update', function () use ($id) {
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
@@ -134,17 +148,16 @@ class UserController extends Controller
|
||||
'email' => 'min:2|email|unique:users,email,' . $id,
|
||||
'password' => 'min:5|required_with:password_confirm',
|
||||
'password-confirm' => 'same:password|required_with:password',
|
||||
'role' => 'exists:roles,id'
|
||||
], [
|
||||
'password-confirm.required_with' => 'Password confirmation required'
|
||||
'setting' => 'array'
|
||||
]);
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
$user->fill($request->all());
|
||||
|
||||
// Role updates
|
||||
if ($this->currentUser->can('user-update') && $request->has('role')) {
|
||||
$user->attachRoleId($request->get('role'));
|
||||
if (userCan('users-manage') && $request->has('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
$user->roles()->sync($roles);
|
||||
}
|
||||
|
||||
// Password updates
|
||||
@@ -154,27 +167,37 @@ class UserController extends Controller
|
||||
}
|
||||
|
||||
// External auth id updates
|
||||
if ($this->currentUser->can('user-update') && $request->has('external_auth_id')) {
|
||||
if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) {
|
||||
$user->external_auth_id = $request->get('external_auth_id');
|
||||
}
|
||||
|
||||
// Save an user-specific settings
|
||||
if ($request->has('setting')) {
|
||||
foreach ($request->get('setting') as $key => $value) {
|
||||
setting()->putUser($user, $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
$user->save();
|
||||
return redirect('/users');
|
||||
session()->flash('success', trans('settings.users_edit_success'));
|
||||
|
||||
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
|
||||
return redirect($redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the user delete page.
|
||||
* @param $id
|
||||
* @param int $id
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
$this->checkPermissionOr('user-delete', function () use ($id) {
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
$this->setPageTitle('Delete User ' . $user->name);
|
||||
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
|
||||
return view('users/delete', ['user' => $user]);
|
||||
}
|
||||
|
||||
@@ -186,17 +209,44 @@ class UserController extends Controller
|
||||
public function destroy($id)
|
||||
{
|
||||
$this->preventAccessForDemoUsers();
|
||||
$this->checkPermissionOr('user-delete', function () use ($id) {
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
|
||||
if ($this->userRepo->isOnlyAdmin($user)) {
|
||||
session()->flash('error', 'You cannot delete the only admin');
|
||||
session()->flash('error', trans('errors.users_cannot_delete_only_admin'));
|
||||
return redirect($user->getEditUrl());
|
||||
}
|
||||
$this->userRepo->destroy($user);
|
||||
|
||||
return redirect('/users');
|
||||
if ($user->system_name === 'public') {
|
||||
session()->flash('error', trans('errors.users_cannot_delete_guest'));
|
||||
return redirect($user->getEditUrl());
|
||||
}
|
||||
|
||||
$this->userRepo->destroy($user);
|
||||
session()->flash('success', trans('settings.users_delete_success'));
|
||||
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the user profile page
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showProfilePage($id)
|
||||
{
|
||||
$user = $this->userRepo->getById($id);
|
||||
$userActivity = $this->userRepo->getActivity($user);
|
||||
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
|
||||
$assetCounts = $this->userRepo->getAssetCounts($user);
|
||||
return view('users/profile', [
|
||||
'user' => $user,
|
||||
'activity' => $userActivity,
|
||||
'recentlyCreated' => $recentlyCreated,
|
||||
'assetCounts' => $assetCounts
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http;
|
||||
<?php namespace BookStack\Http;
|
||||
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
@@ -9,15 +7,33 @@ class Kernel extends HttpKernel
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*
|
||||
* These middleware are run during every request to your application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middleware = [
|
||||
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
||||
\BookStack\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\BookStack\Http\Middleware\VerifyCsrfToken::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\BookStack\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\BookStack\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\BookStack\Http\Middleware\Localization::class
|
||||
],
|
||||
'api' => [
|
||||
'throttle:60,1',
|
||||
'bindings',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -26,6 +42,7 @@ class Kernel extends HttpKernel
|
||||
* @var array
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'auth' => \BookStack\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
|
||||
@@ -4,21 +4,17 @@ namespace BookStack\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use Setting;
|
||||
|
||||
class Authenticate
|
||||
{
|
||||
/**
|
||||
* The Guard implementation.
|
||||
*
|
||||
* @var Guard
|
||||
*/
|
||||
protected $auth;
|
||||
|
||||
/**
|
||||
* Create a new filter instance.
|
||||
*
|
||||
* @param Guard $auth
|
||||
*/
|
||||
public function __construct(Guard $auth)
|
||||
@@ -28,22 +24,21 @@ class Authenticate
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if(auth()->check() && auth()->user()->email_confirmed == false) {
|
||||
return redirect()->guest('/register/confirm/awaiting');
|
||||
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
|
||||
return redirect(baseUrl('/register/confirm/awaiting'));
|
||||
}
|
||||
|
||||
if ($this->auth->guest() && !Setting::get('app-public')) {
|
||||
if ($this->auth->guest() && !setting('app-public')) {
|
||||
if ($request->ajax()) {
|
||||
return response('Unauthorized.', 401);
|
||||
} else {
|
||||
return redirect()->guest('/login');
|
||||
return redirect()->guest(baseUrl('/login'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
app/Http/Middleware/Localization.php
Normal file
33
app/Http/Middleware/Localization.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php namespace BookStack\Http\Middleware;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Closure;
|
||||
|
||||
class Localization
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$defaultLang = config('app.locale');
|
||||
if (user()->isDefault()) {
|
||||
$locale = $defaultLang;
|
||||
$availableLocales = config('app.locales');
|
||||
foreach ($request->getLanguages() as $lang) {
|
||||
if (!in_array($lang, $availableLocales)) continue;
|
||||
$locale = $lang;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$locale = setting()->getUser(user(), 'language', $defaultLang);
|
||||
}
|
||||
app()->setLocale($locale);
|
||||
Carbon::setLocale($locale);
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
<?php namespace BookStack\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
@@ -34,7 +32,8 @@ class RedirectIfAuthenticated
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if ($this->auth->check()) {
|
||||
$requireConfirmation = setting('registration-confirmation');
|
||||
if ($this->auth->check() && (!$requireConfirmation || ($requireConfirmation && $this->auth->user()->email_confirmed))) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
<?php
|
||||
|
||||
// Authenticated routes...
|
||||
Route::group(['middleware' => 'auth'], function () {
|
||||
|
||||
Route::group(['prefix' => 'books'], function () {
|
||||
|
||||
// Books
|
||||
Route::get('/', 'BookController@index');
|
||||
Route::get('/create', 'BookController@create');
|
||||
Route::post('/', 'BookController@store');
|
||||
Route::get('/{slug}/edit', 'BookController@edit');
|
||||
Route::put('/{slug}', 'BookController@update');
|
||||
Route::delete('/{id}', 'BookController@destroy');
|
||||
Route::get('/{slug}/sort-item', 'BookController@getSortItem');
|
||||
Route::get('/{slug}', 'BookController@show');
|
||||
Route::get('/{slug}/delete', 'BookController@showDelete');
|
||||
Route::get('/{bookSlug}/sort', 'BookController@sort');
|
||||
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
|
||||
|
||||
// Pages
|
||||
Route::get('/{bookSlug}/page/create', 'PageController@create');
|
||||
Route::post('/{bookSlug}/page', 'PageController@store');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
|
||||
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
|
||||
Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
|
||||
|
||||
// Revisions
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
|
||||
|
||||
// Chapters
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}/create-page', 'PageController@create');
|
||||
Route::get('/{bookSlug}/chapter/create', 'ChapterController@create');
|
||||
Route::post('/{bookSlug}/chapter/create', 'ChapterController@store');
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show');
|
||||
Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update');
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
|
||||
Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
|
||||
Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
|
||||
|
||||
});
|
||||
|
||||
// Users
|
||||
Route::get('/users', 'UserController@index');
|
||||
Route::get('/users/create', 'UserController@create');
|
||||
Route::get('/users/{id}/delete', 'UserController@delete');
|
||||
Route::post('/users/create', 'UserController@store');
|
||||
Route::get('/users/{id}', 'UserController@edit');
|
||||
Route::put('/users/{id}', 'UserController@update');
|
||||
Route::delete('/users/{id}', 'UserController@destroy');
|
||||
|
||||
// Image routes
|
||||
Route::group(['prefix' => 'images'], function() {
|
||||
// Get for user images
|
||||
Route::get('/user/all', 'ImageController@getAllForUserType');
|
||||
Route::get('/user/all/{page}', 'ImageController@getAllForUserType');
|
||||
// Standard get, update and deletion for all types
|
||||
Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail');
|
||||
Route::put('/update/{imageId}', 'ImageController@update');
|
||||
Route::post('/{type}/upload', 'ImageController@uploadByType');
|
||||
Route::get('/{type}/all', 'ImageController@getAllByType');
|
||||
Route::get('/{type}/all/{page}', 'ImageController@getAllByType');
|
||||
Route::delete('/{imageId}', 'ImageController@destroy');
|
||||
});
|
||||
|
||||
// Links
|
||||
Route::get('/link/{id}', 'PageController@redirectFromLink');
|
||||
|
||||
// Search
|
||||
Route::get('/search/all', 'SearchController@searchAll');
|
||||
Route::get('/search/book/{bookId}', 'SearchController@searchBook');
|
||||
|
||||
// Other Pages
|
||||
Route::get('/', 'HomeController@index');
|
||||
Route::get('/home', 'HomeController@index');
|
||||
|
||||
// Settings
|
||||
Route::get('/settings', 'SettingController@index');
|
||||
Route::post('/settings', 'SettingController@update');
|
||||
|
||||
});
|
||||
|
||||
// Login using social authentication
|
||||
Route::get('/login/service/{socialDriver}', 'Auth\AuthController@getSocialLogin');
|
||||
Route::get('/login/service/{socialDriver}/callback', 'Auth\AuthController@socialCallback');
|
||||
Route::get('/login/service/{socialDriver}/detach', 'Auth\AuthController@detachSocialAccount');
|
||||
|
||||
// Login/Logout routes
|
||||
Route::get('/login', 'Auth\AuthController@getLogin');
|
||||
Route::post('/login', 'Auth\AuthController@postLogin');
|
||||
Route::get('/logout', 'Auth\AuthController@getLogout');
|
||||
Route::get('/register', 'Auth\AuthController@getRegister');
|
||||
Route::get('/register/confirm', 'Auth\AuthController@getRegisterConfirmation');
|
||||
Route::get('/register/confirm/awaiting', 'Auth\AuthController@showAwaitingConfirmation');
|
||||
Route::post('/register/confirm/resend', 'Auth\AuthController@resendConfirmation');
|
||||
Route::get('/register/confirm/{token}', 'Auth\AuthController@confirmEmail');
|
||||
Route::get('/register/confirm/{token}/email', 'Auth\AuthController@viewConfirmEmail');
|
||||
Route::get('/register/service/{socialDriver}', 'Auth\AuthController@socialRegister');
|
||||
Route::post('/register', 'Auth\AuthController@postRegister');
|
||||
|
||||
// Password reset link request routes...
|
||||
Route::get('/password/email', 'Auth\PasswordController@getEmail');
|
||||
Route::post('/password/email', 'Auth\PasswordController@postEmail');
|
||||
// Password reset routes...
|
||||
Route::get('/password/reset/{token}', 'Auth\PasswordController@getReset');
|
||||
Route::post('/password/reset', 'Auth\PasswordController@postReset');
|
||||
@@ -1,14 +1,9 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Images;
|
||||
|
||||
class Image extends Model
|
||||
class Image extends Ownable
|
||||
{
|
||||
use Ownable;
|
||||
|
||||
protected $fillable = ['name'];
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
||||
abstract class Job
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queueable Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This job base class provides a central location to place any logic that
|
||||
| is shared across all of your jobs. The trait included with the class
|
||||
| provides access to the "queueOn" and "delay" queue helper methods.
|
||||
|
|
||||
*/
|
||||
|
||||
use Queueable;
|
||||
}
|
||||
24
app/JointPermission.php
Normal file
24
app/JointPermission.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
class JointPermission extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get the role that this points to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function role()
|
||||
{
|
||||
return $this->belongsTo(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity this points to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphOne(Entity::class, 'entity');
|
||||
}
|
||||
}
|
||||
19
app/Model.php
Normal file
19
app/Model.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
||||
|
||||
class Model extends EloquentModel
|
||||
{
|
||||
|
||||
/**
|
||||
* Provides public access to get the raw attribute value from the model.
|
||||
* Used in areas where no mutations are required but performance is critical.
|
||||
* @param $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRawAttribute($key)
|
||||
{
|
||||
return parent::getAttributeFromArray($key);
|
||||
}
|
||||
|
||||
}
|
||||
53
app/Notifications/ConfirmEmail.php
Normal file
53
app/Notifications/ConfirmEmail.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class ConfirmEmail extends Notification implements ShouldQueue
|
||||
{
|
||||
|
||||
use Queueable;
|
||||
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
* @param string $token
|
||||
*/
|
||||
public function __construct($token)
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
$appName = ['appName' => setting('app-name')];
|
||||
return (new MailMessage)
|
||||
->subject(trans('auth.email_confirm_subject', $appName))
|
||||
->greeting(trans('auth.email_confirm_greeting', $appName))
|
||||
->line(trans('auth.email_confirm_text'))
|
||||
->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
|
||||
}
|
||||
|
||||
}
|
||||
51
app/Notifications/ResetPassword.php
Normal file
51
app/Notifications/ResetPassword.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Notifications;
|
||||
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class ResetPassword extends Notification
|
||||
{
|
||||
/**
|
||||
* The password reset token.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* Create a notification instance.
|
||||
*
|
||||
* @param string $token
|
||||
*/
|
||||
public function __construct($token)
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array|string
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the mail representation of the notification.
|
||||
*
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
public function toMail()
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')]))
|
||||
->line(trans('auth.email_reset_text'))
|
||||
->action(trans('auth.reset_password'), baseUrl('password/reset/' . $this->token))
|
||||
->line(trans('auth.email_reset_not_requested'));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
trait Ownable
|
||||
abstract class Ownable extends Model
|
||||
{
|
||||
/**
|
||||
* Relation for the user that created this entity.
|
||||
@@ -9,7 +9,7 @@ trait Ownable
|
||||
*/
|
||||
public function createdBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'created_by');
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,6 +18,16 @@ trait Ownable
|
||||
*/
|
||||
public function updatedBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'updated_by');
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the class name.
|
||||
* @return string
|
||||
*/
|
||||
public static function getClassName()
|
||||
{
|
||||
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
|
||||
}
|
||||
|
||||
}
|
||||
79
app/Page.php
79
app/Page.php
@@ -1,15 +1,19 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Page extends Entity
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'priority'];
|
||||
protected $fillable = ['name', 'html', 'priority', 'markdown'];
|
||||
|
||||
protected $simpleAttributes = ['name', 'id', 'slug'];
|
||||
|
||||
protected $with = ['book'];
|
||||
public $textField = 'text';
|
||||
|
||||
/**
|
||||
* Converts this page into a simplified array.
|
||||
* @return mixed
|
||||
*/
|
||||
public function toSimpleArray()
|
||||
{
|
||||
$array = array_intersect_key($this->toArray(), array_flip($this->simpleAttributes));
|
||||
@@ -17,35 +21,88 @@ class Page extends Entity
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the book this page sits in.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function book()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Book');
|
||||
return $this->belongsTo(Book::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chapter that this page is in, If applicable.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function chapter()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Chapter');
|
||||
return $this->belongsTo(Chapter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this page has a chapter.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChapter()
|
||||
{
|
||||
return $this->chapter()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated page revisions, ordered by created date.
|
||||
* @return mixed
|
||||
*/
|
||||
public function revisions()
|
||||
{
|
||||
return $this->hasMany('BookStack\PageRevision')->orderBy('created_at', 'desc');
|
||||
return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
/**
|
||||
* Get the attachments assigned to this page.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function attachments()
|
||||
{
|
||||
return $this->hasMany(Attachment::class, 'uploaded_to')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this page.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
||||
return '/books/' . $bookSlug . '/page/' . $this->slug;
|
||||
$midText = $this->draft ? '/draft/' : '/page/';
|
||||
$idComponent = $this->draft ? $this->id : urlencode($this->slug);
|
||||
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/'));
|
||||
}
|
||||
|
||||
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this page's content to the specified length.
|
||||
* @param int $length
|
||||
* @return mixed
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
return strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
|
||||
$text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
|
||||
return mb_convert_encoding($text, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @param bool $withContent
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery($withContent = false)
|
||||
{ $htmlQuery = $withContent ? 'html' : "'' as html";
|
||||
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,26 +1,50 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PageRevision extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'text'];
|
||||
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
|
||||
|
||||
/**
|
||||
* Get the user that created the page revision
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function createdBy()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User', 'created_by');
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page this revision originates from.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function page()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Page');
|
||||
return $this->belongsTo(Page::class);
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
/**
|
||||
* Get the url for this revision.
|
||||
* @param null|string $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = null)
|
||||
{
|
||||
return $this->page->getUrl() . '/revisions/' . $this->id;
|
||||
$url = $this->page->getUrl() . '/revisions/' . $this->id;
|
||||
if ($path) return $url . '/' . trim($path, '/');
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous revision for the same page if existing
|
||||
* @return \BookStack\PageRevision|null
|
||||
*/
|
||||
public function getPrevious()
|
||||
{
|
||||
if ($id = static::where('page_id', '=', $this->page_id)->where('id', '<', $this->id)->max('id')) {
|
||||
return static::find($id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Permission extends Model
|
||||
{
|
||||
/**
|
||||
* The roles that belong to the permission.
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Permissions');
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
<?php
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Services\SettingService;
|
||||
use BookStack\Setting;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use BookStack\User;
|
||||
use Validator;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -15,7 +14,18 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
// Custom validation methods
|
||||
Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
|
||||
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
|
||||
return in_array($value->getMimeType(), $imageMimes);
|
||||
});
|
||||
|
||||
\Blade::directive('icon', function($expression) {
|
||||
return "<?php echo icon($expression); ?>";
|
||||
});
|
||||
|
||||
// Allow longer string lengths after upgrade to utf8mb4
|
||||
\Schema::defaultStringLength(191);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,6 +35,8 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
$this->app->singleton(SettingService::class, function($app) {
|
||||
return new SettingService($app->make(Setting::class), $app->make('Illuminate\Contracts\Cache\Repository'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Auth;
|
||||
use BookStack\Services\LdapService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
@@ -25,7 +26,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
public function register()
|
||||
{
|
||||
Auth::provider('ldap', function($app, array $config) {
|
||||
return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']);
|
||||
return new LdapUserProvider($config['model'], $app[LdapService::class]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
26
app/Providers/BroadcastServiceProvider.php
Normal file
26
app/Providers/BroadcastServiceProvider.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
|
||||
class BroadcastServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
// Broadcast::routes();
|
||||
//
|
||||
// /*
|
||||
// * Authenticate the user's personal channel...
|
||||
// */
|
||||
// Broadcast::channel('BookStack.User.*', function ($user, $userId) {
|
||||
// return (int) $user->id === (int) $userId;
|
||||
// });
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,18 @@
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use BookStack\Activity;
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Services\ViewService;
|
||||
use BookStack\Setting;
|
||||
use BookStack\View;
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Illuminate\Contracts\Filesystem\Factory;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use BookStack\Services\ActivityService;
|
||||
use BookStack\Services\SettingService;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class CustomFacadeProvider extends ServiceProvider
|
||||
{
|
||||
@@ -28,24 +35,31 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
public function register()
|
||||
{
|
||||
$this->app->bind('activity', function() {
|
||||
return new ActivityService($this->app->make('BookStack\Activity'));
|
||||
return new ActivityService(
|
||||
$this->app->make(Activity::class),
|
||||
$this->app->make(PermissionService::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('views', function() {
|
||||
return new ViewService($this->app->make('BookStack\View'));
|
||||
return new ViewService(
|
||||
$this->app->make(View::class),
|
||||
$this->app->make(PermissionService::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('setting', function() {
|
||||
return new SettingService(
|
||||
$this->app->make('BookStack\Setting'),
|
||||
$this->app->make('Illuminate\Contracts\Cache\Repository')
|
||||
$this->app->make(Setting::class),
|
||||
$this->app->make(Repository::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('images', function() {
|
||||
return new ImageService(
|
||||
$this->app->make('Intervention\Image\ImageManager'),
|
||||
$this->app->make('Illuminate\Contracts\Filesystem\Factory'),
|
||||
$this->app->make('Illuminate\Contracts\Cache\Repository')
|
||||
$this->app->make(ImageManager::class),
|
||||
$this->app->make(Factory::class),
|
||||
$this->app->make(Repository::class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -13,21 +14,18 @@ class EventServiceProvider extends ServiceProvider
|
||||
* @var array
|
||||
*/
|
||||
protected $listen = [
|
||||
'BookStack\Events\SomeEvent' => [
|
||||
'BookStack\Listeners\EventListener',
|
||||
SocialiteWasCalled::class => [
|
||||
'SocialiteProviders\Slack\SlackExtendSocialite@handle',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any other events for your application.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Events\Dispatcher $events
|
||||
* @return void
|
||||
*/
|
||||
public function boot(DispatcherContract $events)
|
||||
public function boot()
|
||||
{
|
||||
parent::boot($events);
|
||||
|
||||
//
|
||||
parent::boot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ class LdapUserProvider implements UserProvider
|
||||
$model->name = $userDetails['name'];
|
||||
$model->external_auth_id = $userDetails['uid'];
|
||||
$model->email = $userDetails['email'];
|
||||
$model->email_confirmed = true;
|
||||
$model->email_confirmed = false;
|
||||
return $model;
|
||||
}
|
||||
|
||||
|
||||
35
app/Providers/PaginationServiceProvider.php
Normal file
35
app/Providers/PaginationServiceProvider.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
|
||||
use Illuminate\Pagination\PaginationServiceProvider as IlluminatePaginationServiceProvider;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
|
||||
class PaginationServiceProvider extends IlluminatePaginationServiceProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
Paginator::viewFactoryResolver(function () {
|
||||
return $this->app['view'];
|
||||
});
|
||||
|
||||
Paginator::currentPathResolver(function () {
|
||||
return baseUrl($this->app['request']->path());
|
||||
});
|
||||
|
||||
Paginator::currentPageResolver(function ($pageName = 'page') {
|
||||
$page = $this->app['request']->input($pageName);
|
||||
|
||||
if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -19,26 +20,54 @@ class RouteServiceProvider extends ServiceProvider
|
||||
/**
|
||||
* Define your route model bindings, pattern filters, etc.
|
||||
*
|
||||
* @param \Illuminate\Routing\Router $router
|
||||
* @return void
|
||||
*/
|
||||
public function boot(Router $router)
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
|
||||
parent::boot($router);
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
*
|
||||
* @param \Illuminate\Routing\Router $router
|
||||
* @return void
|
||||
*/
|
||||
public function map(Router $router)
|
||||
public function map()
|
||||
{
|
||||
$router->group(['namespace' => $this->namespace], function ($router) {
|
||||
require app_path('Http/routes.php');
|
||||
$this->mapWebRoutes();
|
||||
// $this->mapApiRoutes();
|
||||
}
|
||||
/**
|
||||
* Define the "web" routes for the application.
|
||||
*
|
||||
* These routes all receive session state, CSRF protection, etc.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function mapWebRoutes()
|
||||
{
|
||||
Route::group([
|
||||
'middleware' => 'web',
|
||||
'namespace' => $this->namespace,
|
||||
], function ($router) {
|
||||
require base_path('routes/web.php');
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Define the "api" routes for the application.
|
||||
*
|
||||
* These routes are typically stateless.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function mapApiRoutes()
|
||||
{
|
||||
Route::group([
|
||||
'middleware' => 'api',
|
||||
'namespace' => $this->namespace,
|
||||
'prefix' => 'api',
|
||||
], function ($router) {
|
||||
require base_path('routes/api.php');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class SocialiteServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Indicates if loading of the provider is deferred.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $defer = true;
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->bindShared('Laravel\Socialite\Contracts\Factory', function ($app) {
|
||||
return new SocialiteManager($app);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides()
|
||||
{
|
||||
return ['Laravel\Socialite\Contracts\Factory'];
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use Activity;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Book;
|
||||
use Views;
|
||||
|
||||
class BookRepo
|
||||
{
|
||||
|
||||
protected $book;
|
||||
protected $pageRepo;
|
||||
protected $chapterRepo;
|
||||
|
||||
/**
|
||||
* BookRepo constructor.
|
||||
* @param Book $book
|
||||
* @param PageRepo $pageRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
*/
|
||||
public function __construct(Book $book, PageRepo $pageRepo, ChapterRepo $chapterRepo)
|
||||
{
|
||||
$this->book = $book;
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the book that has the given id.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->book->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all books, Limited by count.
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAll($count = 10)
|
||||
{
|
||||
return $this->book->orderBy('name', 'asc')->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all books paginated.
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAllPaginated($count = 10)
|
||||
{
|
||||
return $this->book->orderBy('name', 'asc')->paginate($count);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the latest books.
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLatest($count = 10)
|
||||
{
|
||||
return $this->book->orderBy('created_at', 'desc')->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most recently viewed for a user.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyViewed($count = 10, $page = 0)
|
||||
{
|
||||
return Views::getUserRecentlyViewed($count, $page, $this->book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most viewed books.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPopular($count = 10, $page = 0)
|
||||
{
|
||||
return Views::getPopular($count, $page, $this->book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a book by slug
|
||||
* @param $slug
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySlug($slug)
|
||||
{
|
||||
$book = $this->book->where('slug', '=', $slug)->first();
|
||||
if ($book === null) abort(404);
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a book exists.
|
||||
* @param $id
|
||||
* @return bool
|
||||
*/
|
||||
public function exists($id)
|
||||
{
|
||||
return $this->book->where('id', '=', $id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new book instance from request input.
|
||||
* @param $input
|
||||
* @return Book
|
||||
*/
|
||||
public function newFromInput($input)
|
||||
{
|
||||
return $this->book->fill($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the amount of books that have a specific slug.
|
||||
* @param $slug
|
||||
* @return mixed
|
||||
*/
|
||||
public function countBySlug($slug)
|
||||
{
|
||||
return $this->book->where('slug', '=', $slug)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a book identified by the given slug.
|
||||
* @param $bookSlug
|
||||
*/
|
||||
public function destroyBySlug($bookSlug)
|
||||
{
|
||||
$book = $this->getBySlug($bookSlug);
|
||||
foreach ($book->pages as $page) {
|
||||
$this->pageRepo->destroy($page);
|
||||
}
|
||||
foreach ($book->chapters as $chapter) {
|
||||
$this->chapterRepo->destroy($chapter);
|
||||
}
|
||||
$book->views()->delete();
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next child element priority.
|
||||
* @param Book $book
|
||||
* @return int
|
||||
*/
|
||||
public function getNewPriority($book)
|
||||
{
|
||||
$lastElem = $this->getChildren($book)->pop();
|
||||
return $lastElem ? $lastElem->priority + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slug
|
||||
* @param bool|false $currentId
|
||||
* @return bool
|
||||
*/
|
||||
public function doesSlugExist($slug, $currentId = false)
|
||||
{
|
||||
$query = $this->book->where('slug', '=', $slug);
|
||||
if ($currentId) {
|
||||
$query = $query->where('id', '!=', $currentId);
|
||||
}
|
||||
return $query->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a suitable slug for the given book name.
|
||||
* Ensures the returned slug is unique in the system.
|
||||
* @param string $name
|
||||
* @param bool|false $currentId
|
||||
* @return string
|
||||
*/
|
||||
public function findSuitableSlug($name, $currentId = false)
|
||||
{
|
||||
$originalSlug = Str::slug($name);
|
||||
$slug = $originalSlug;
|
||||
$count = 2;
|
||||
while ($this->doesSlugExist($slug, $currentId)) {
|
||||
$slug = $originalSlug . '-' . $count;
|
||||
$count++;
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all child objects of a book.
|
||||
* Returns a sorted collection of Pages and Chapters.
|
||||
* Loads the bookslug onto child elements to prevent access database access for getting the slug.
|
||||
* @param Book $book
|
||||
* @return mixed
|
||||
*/
|
||||
public function getChildren(Book $book)
|
||||
{
|
||||
$pages = $book->pages()->where('chapter_id', '=', 0)->get();
|
||||
$chapters = $book->chapters()->with('pages')->get();
|
||||
$children = $pages->merge($chapters);
|
||||
$bookSlug = $book->slug;
|
||||
$children->each(function ($child) use ($bookSlug) {
|
||||
$child->setAttribute('bookSlug', $bookSlug);
|
||||
if ($child->isA('chapter')) {
|
||||
$child->pages->each(function ($page) use ($bookSlug) {
|
||||
$page->setAttribute('bookSlug', $bookSlug);
|
||||
});
|
||||
}
|
||||
});
|
||||
return $children->sortBy('priority');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get books by search term.
|
||||
* @param $term
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term)
|
||||
{
|
||||
$terms = explode(' ', $term);
|
||||
$books = $this->book->fullTextSearch(['name', 'description'], $terms);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
foreach ($books as $book) {
|
||||
//highlight
|
||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $book->getExcerpt(100));
|
||||
$book->searchSnippet = $result;
|
||||
}
|
||||
return $books;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use Activity;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Chapter;
|
||||
|
||||
class ChapterRepo
|
||||
{
|
||||
|
||||
protected $chapter;
|
||||
|
||||
/**
|
||||
* ChapterRepo constructor.
|
||||
* @param $chapter
|
||||
*/
|
||||
public function __construct(Chapter $chapter)
|
||||
{
|
||||
$this->chapter = $chapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an id exists.
|
||||
* @param $id
|
||||
* @return bool
|
||||
*/
|
||||
public function idExists($id)
|
||||
{
|
||||
return $this->chapter->where('id', '=', $id)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chapter by a specific id.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->chapter->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all chapters.
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
return $this->chapter->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chapter that has the given slug within the given book.
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySlug($slug, $bookId)
|
||||
{
|
||||
$chapter = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
if ($chapter === null) abort(404);
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new chapter from request input.
|
||||
* @param $input
|
||||
* @return $this
|
||||
*/
|
||||
public function newFromInput($input)
|
||||
{
|
||||
return $this->chapter->fill($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a chapter and its relations by providing its slug.
|
||||
* @param Chapter $chapter
|
||||
*/
|
||||
public function destroy(Chapter $chapter)
|
||||
{
|
||||
if (count($chapter->pages) > 0) {
|
||||
foreach ($chapter->pages as $page) {
|
||||
$page->chapter_id = 0;
|
||||
$page->save();
|
||||
}
|
||||
}
|
||||
Activity::removeEntity($chapter);
|
||||
$chapter->views()->delete();
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a chapter's slug exists.
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @param bool|false $currentId
|
||||
* @return bool
|
||||
*/
|
||||
public function doesSlugExist($slug, $bookId, $currentId = false)
|
||||
{
|
||||
$query = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId);
|
||||
if ($currentId) {
|
||||
$query = $query->where('id', '!=', $currentId);
|
||||
}
|
||||
return $query->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a suitable slug for the provided name.
|
||||
* Checks database to prevent duplicate slugs.
|
||||
* @param $name
|
||||
* @param $bookId
|
||||
* @param bool|false $currentId
|
||||
* @return string
|
||||
*/
|
||||
public function findSuitableSlug($name, $bookId, $currentId = false)
|
||||
{
|
||||
$slug = Str::slug($name);
|
||||
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chapters by the given search term.
|
||||
* @param $term
|
||||
* @param array $whereTerms
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term, $whereTerms = [])
|
||||
{
|
||||
$terms = explode(' ', $term);
|
||||
$chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms, $whereTerms);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
foreach ($chapters as $chapter) {
|
||||
//highlight
|
||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $chapter->getExcerpt(100));
|
||||
$chapter->searchSnippet = $result;
|
||||
}
|
||||
return $chapters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the book relation of this chapter.
|
||||
* @param $bookId
|
||||
* @param Chapter $chapter
|
||||
* @return Chapter
|
||||
*/
|
||||
public function changeBook($bookId, Chapter $chapter)
|
||||
{
|
||||
$chapter->book_id = $bookId;
|
||||
foreach ($chapter->activity as $activity) {
|
||||
$activity->book_id = $bookId;
|
||||
$activity->save();
|
||||
}
|
||||
$chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id);
|
||||
$chapter->save();
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
}
|
||||
1091
app/Repos/EntityRepo.php
Normal file
1091
app/Repos/EntityRepo.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,10 @@
|
||||
|
||||
|
||||
use BookStack\Image;
|
||||
use BookStack\Page;
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Setting;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
@@ -11,16 +14,22 @@ class ImageRepo
|
||||
|
||||
protected $image;
|
||||
protected $imageService;
|
||||
protected $restrictionService;
|
||||
protected $page;
|
||||
|
||||
/**
|
||||
* ImageRepo constructor.
|
||||
* @param Image $image
|
||||
* @param Image $image
|
||||
* @param ImageService $imageService
|
||||
* @param PermissionService $permissionService
|
||||
* @param Page $page
|
||||
*/
|
||||
public function __construct(Image $image, ImageService $imageService)
|
||||
public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page)
|
||||
{
|
||||
$this->image = $image;
|
||||
$this->imageService = $imageService;
|
||||
$this->restrictionService = $permissionService;
|
||||
$this->page = $page;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,23 +43,17 @@ class ImageRepo
|
||||
return $this->image->findOrFail($id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets a load images paginated, filtered by image type.
|
||||
* @param string $type
|
||||
* @param int $page
|
||||
* @param int $pageSize
|
||||
* @param bool|int $userFilter
|
||||
* Execute a paginated query, returning in a standard format.
|
||||
* Also runs the query through the restriction system.
|
||||
* @param $query
|
||||
* @param int $page
|
||||
* @param int $pageSize
|
||||
* @return array
|
||||
*/
|
||||
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
|
||||
private function returnPaginated($query, $page = 0, $pageSize = 24)
|
||||
{
|
||||
$images = $this->image->where('type', '=', strtolower($type));
|
||||
|
||||
if ($userFilter !== false) {
|
||||
$images = $images->where('created_by', '=', $userFilter);
|
||||
}
|
||||
|
||||
$images = $this->restrictionService->filterRelatedPages($query, 'images', 'uploaded_to');
|
||||
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
|
||||
$hasMore = count($images) > $pageSize;
|
||||
|
||||
@@ -65,15 +68,74 @@ class ImageRepo
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a load images paginated, filtered by image type.
|
||||
* @param string $type
|
||||
* @param int $page
|
||||
* @param int $pageSize
|
||||
* @param bool|int $userFilter
|
||||
* @return array
|
||||
*/
|
||||
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
|
||||
{
|
||||
$images = $this->image->where('type', '=', strtolower($type));
|
||||
|
||||
if ($userFilter !== false) {
|
||||
$images = $images->where('created_by', '=', $userFilter);
|
||||
}
|
||||
|
||||
return $this->returnPaginated($images, $page, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for images by query, of a particular type.
|
||||
* @param string $type
|
||||
* @param int $page
|
||||
* @param int $pageSize
|
||||
* @param string $searchTerm
|
||||
* @return array
|
||||
*/
|
||||
public function searchPaginatedByType($type, $page = 0, $pageSize = 24, $searchTerm)
|
||||
{
|
||||
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
|
||||
return $this->returnPaginated($images, $page, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gallery images with a particular filter criteria such as
|
||||
* being within the current book or page.
|
||||
* @param int $pagination
|
||||
* @param int $pageSize
|
||||
* @param $filter
|
||||
* @param $pageId
|
||||
* @return array
|
||||
*/
|
||||
public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId)
|
||||
{
|
||||
$images = $this->image->where('type', '=', 'gallery');
|
||||
|
||||
$page = $this->page->findOrFail($pageId);
|
||||
|
||||
if ($filter === 'page') {
|
||||
$images = $images->where('uploaded_to', '=', $page->id);
|
||||
} elseif ($filter === 'book') {
|
||||
$validPageIds = $page->book->pages->pluck('id')->toArray();
|
||||
$images = $images->whereIn('uploaded_to', $validPageIds);
|
||||
}
|
||||
|
||||
return $this->returnPaginated($images, $pagination, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new image into storage and return the new image.
|
||||
* @param UploadedFile $uploadFile
|
||||
* @param string $type
|
||||
* @param string $type
|
||||
* @param int $uploadedTo
|
||||
* @return Image
|
||||
*/
|
||||
public function saveNew(UploadedFile $uploadFile, $type)
|
||||
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
|
||||
{
|
||||
$image = $this->imageService->saveNewFromUpload($uploadFile, $type);
|
||||
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo);
|
||||
$this->loadThumbs($image);
|
||||
return $image;
|
||||
}
|
||||
@@ -123,14 +185,19 @@ class ImageRepo
|
||||
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
|
||||
*
|
||||
* @param Image $image
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool $keepRatio
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool $keepRatio
|
||||
* @return string
|
||||
*/
|
||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||
{
|
||||
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
||||
try {
|
||||
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
||||
} catch (FileNotFoundException $exception) {
|
||||
$image->delete();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Page;
|
||||
use BookStack\PageRevision;
|
||||
|
||||
class PageRepo
|
||||
{
|
||||
protected $page;
|
||||
protected $pageRevision;
|
||||
|
||||
/**
|
||||
* PageRepo constructor.
|
||||
* @param Page $page
|
||||
* @param PageRevision $pageRevision
|
||||
*/
|
||||
public function __construct(Page $page, PageRevision $pageRevision)
|
||||
{
|
||||
$this->page = $page;
|
||||
$this->pageRevision = $pageRevision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page id exists.
|
||||
* @param $id
|
||||
* @return bool
|
||||
*/
|
||||
public function idExists($id)
|
||||
{
|
||||
return $this->page->where('page_id', '=', $id)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page via a specific ID.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->page->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages.
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
return $this->page->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page identified by the given slug.
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySlug($slug, $bookId)
|
||||
{
|
||||
$page = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
if ($page === null) abort(404);
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $input
|
||||
* @return Page
|
||||
*/
|
||||
public function newFromInput($input)
|
||||
{
|
||||
$page = $this->page->fill($input);
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the pages with a particular slug within a book.
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
*/
|
||||
public function countBySlug($slug, $bookId)
|
||||
{
|
||||
return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new page into the system.
|
||||
* Input validation must be done beforehand.
|
||||
* @param array $input
|
||||
* @param Book $book
|
||||
* @param int $chapterId
|
||||
* @return Page
|
||||
*/
|
||||
public function saveNew(array $input, Book $book, $chapterId = null)
|
||||
{
|
||||
$page = $this->newFromInput($input);
|
||||
$page->slug = $this->findSuitableSlug($page->name, $book->id);
|
||||
|
||||
if ($chapterId) $page->chapter_id = $chapterId;
|
||||
|
||||
$page->html = $this->formatHtml($input['html']);
|
||||
$page->text = strip_tags($page->html);
|
||||
$page->created_by = auth()->user()->id;
|
||||
$page->updated_by = auth()->user()->id;
|
||||
|
||||
$book->pages()->save($page);
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a page's html to be tagged correctly
|
||||
* within the system.
|
||||
* @param string $htmlText
|
||||
* @return string
|
||||
*/
|
||||
protected function formatHtml($htmlText)
|
||||
{
|
||||
if($htmlText == '') return $htmlText;
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new \DOMDocument();
|
||||
$doc->loadHTML($htmlText);
|
||||
|
||||
$container = $doc->documentElement;
|
||||
$body = $container->childNodes->item(0);
|
||||
$childNodes = $body->childNodes;
|
||||
|
||||
// Ensure no duplicate ids are used
|
||||
$lastId = false;
|
||||
$idArray = [];
|
||||
|
||||
foreach ($childNodes as $index => $childNode) {
|
||||
/** @var \DOMElement $childNode */
|
||||
if (get_class($childNode) !== 'DOMElement') continue;
|
||||
|
||||
// Overwrite id if not a bookstack custom id
|
||||
if ($childNode->hasAttribute('id')) {
|
||||
$id = $childNode->getAttribute('id');
|
||||
if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
|
||||
$idArray[] = $id;
|
||||
continue;
|
||||
};
|
||||
}
|
||||
|
||||
// Create an unique id for the element
|
||||
do {
|
||||
$id = 'bkmrk-' . substr(uniqid(), -5);
|
||||
} while ($id == $lastId);
|
||||
$lastId = $id;
|
||||
|
||||
$childNode->setAttribute('id', $id);
|
||||
$idArray[] = $id;
|
||||
}
|
||||
|
||||
// Generate inner html as a string
|
||||
$html = '';
|
||||
foreach ($childNodes as $childNode) {
|
||||
$html .= $doc->saveHTML($childNode);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets pages by a search term.
|
||||
* Highlights page content for showing in results.
|
||||
* @param string $term
|
||||
* @param array $whereTerms
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term, $whereTerms = [])
|
||||
{
|
||||
$terms = explode(' ', $term);
|
||||
$pages = $this->page->fullTextSearch(['name', 'text'], $terms, $whereTerms);
|
||||
|
||||
// Add highlights to page text.
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
//lookahead/behind assertions ensures cut between words
|
||||
$s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
|
||||
|
||||
foreach ($pages as $page) {
|
||||
preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
|
||||
//delimiter between occurrences
|
||||
$results = [];
|
||||
foreach ($matches as $line) {
|
||||
$results[] = htmlspecialchars($line[0], 0, 'UTF-8');
|
||||
}
|
||||
$matchLimit = 6;
|
||||
if (count($results) > $matchLimit) {
|
||||
$results = array_slice($results, 0, $matchLimit);
|
||||
}
|
||||
$result = join('... ', $results);
|
||||
|
||||
//highlight
|
||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
|
||||
if (strlen($result) < 5) {
|
||||
$result = $page->getExcerpt(80);
|
||||
}
|
||||
$page->searchSnippet = $result;
|
||||
}
|
||||
return $pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for image usage.
|
||||
* @param $imageString
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchForImage($imageString)
|
||||
{
|
||||
$pages = $this->page->where('html', 'like', '%' . $imageString . '%')->get();
|
||||
foreach ($pages as $page) {
|
||||
$page->url = $page->getUrl();
|
||||
$page->html = '';
|
||||
$page->text = '';
|
||||
}
|
||||
return count($pages) > 0 ? $pages : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a page with any fillable data and saves it into the database.
|
||||
* @param Page $page
|
||||
* @param int $book_id
|
||||
* @param string $input
|
||||
* @return Page
|
||||
*/
|
||||
public function updatePage(Page $page, $book_id, $input)
|
||||
{
|
||||
// Save a revision before updating
|
||||
if ($page->html !== $input['html'] || $page->name !== $input['name']) {
|
||||
$this->saveRevision($page);
|
||||
}
|
||||
|
||||
// Update with new details
|
||||
$page->fill($input);
|
||||
$page->slug = $this->findSuitableSlug($page->name, $book_id, $page->id);
|
||||
$page->html = $this->formatHtml($input['html']);
|
||||
$page->text = strip_tags($page->html);
|
||||
$page->updated_by = auth()->user()->id;
|
||||
$page->save();
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a revision's content back into a page.
|
||||
* @param Page $page
|
||||
* @param Book $book
|
||||
* @param int $revisionId
|
||||
* @return Page
|
||||
*/
|
||||
public function restoreRevision(Page $page, Book $book, $revisionId)
|
||||
{
|
||||
$this->saveRevision($page);
|
||||
$revision = $this->getRevisionById($revisionId);
|
||||
$page->fill($revision->toArray());
|
||||
$page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id);
|
||||
$page->text = strip_tags($page->html);
|
||||
$page->updated_by = auth()->user()->id;
|
||||
$page->save();
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a page revision into the system.
|
||||
* @param Page $page
|
||||
* @return $this
|
||||
*/
|
||||
public function saveRevision(Page $page)
|
||||
{
|
||||
$revision = $this->pageRevision->fill($page->toArray());
|
||||
$revision->page_id = $page->id;
|
||||
$revision->created_by = auth()->user()->id;
|
||||
$revision->created_at = $page->updated_at;
|
||||
$revision->save();
|
||||
// Clear old revisions
|
||||
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
|
||||
$this->pageRevision->where('page_id', '=', $page->id)
|
||||
->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
|
||||
}
|
||||
return $revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single revision via it's id.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRevisionById($id)
|
||||
{
|
||||
return $this->pageRevision->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a slug exists within a book already.
|
||||
* @param $slug
|
||||
* @param $bookId
|
||||
* @param bool|false $currentId
|
||||
* @return bool
|
||||
*/
|
||||
public function doesSlugExist($slug, $bookId, $currentId = false)
|
||||
{
|
||||
$query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId);
|
||||
if ($currentId) $query = $query->where('id', '!=', $currentId);
|
||||
return $query->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the related book for the specified page.
|
||||
* Changes the book id of any relations to the page that store the book id.
|
||||
* @param int $bookId
|
||||
* @param Page $page
|
||||
* @return Page
|
||||
*/
|
||||
public function changeBook($bookId, Page $page)
|
||||
{
|
||||
$page->book_id = $bookId;
|
||||
foreach ($page->activity as $activity) {
|
||||
$activity->book_id = $bookId;
|
||||
$activity->save();
|
||||
}
|
||||
$page->slug = $this->findSuitableSlug($page->name, $bookId, $page->id);
|
||||
$page->save();
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a suitable slug for the resource
|
||||
* @param $name
|
||||
* @param $bookId
|
||||
* @param bool|false $currentId
|
||||
* @return string
|
||||
*/
|
||||
public function findSuitableSlug($name, $bookId, $currentId = false)
|
||||
{
|
||||
$slug = Str::slug($name);
|
||||
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a given page along with its dependencies.
|
||||
* @param $page
|
||||
*/
|
||||
public function destroy($page)
|
||||
{
|
||||
Activity::removeEntity($page);
|
||||
$page->views()->delete();
|
||||
$page->revisions()->delete();
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
153
app/Repos/PermissionsRepo.php
Normal file
153
app/Repos/PermissionsRepo.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\RolePermission;
|
||||
use BookStack\Role;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Setting;
|
||||
|
||||
class PermissionsRepo
|
||||
{
|
||||
|
||||
protected $permission;
|
||||
protected $role;
|
||||
protected $permissionService;
|
||||
|
||||
protected $systemRoles = ['admin', 'public'];
|
||||
|
||||
/**
|
||||
* PermissionsRepo constructor.
|
||||
* @param RolePermission $permission
|
||||
* @param Role $role
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
|
||||
{
|
||||
$this->permission = $permission;
|
||||
$this->role = $role;
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the user roles from the system.
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getAllRoles()
|
||||
{
|
||||
return $this->role->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the roles except for the provided one.
|
||||
* @param Role $role
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAllRolesExcept(Role $role)
|
||||
{
|
||||
return $this->role->where('id', '!=', $role->id)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a role via its ID.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRoleById($id)
|
||||
{
|
||||
return $this->role->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new role into the system.
|
||||
* @param array $roleData
|
||||
* @return Role
|
||||
*/
|
||||
public function saveNewRole($roleData)
|
||||
{
|
||||
$role = $this->role->newInstance($roleData);
|
||||
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
|
||||
// Prevent duplicate names
|
||||
while ($this->role->where('name', '=', $role->name)->count() > 0) {
|
||||
$role->name .= strtolower(str_random(2));
|
||||
}
|
||||
$role->save();
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
return $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing role.
|
||||
* Ensure Admin role always has all permissions.
|
||||
* @param $roleId
|
||||
* @param $roleData
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function updateRole($roleId, $roleData)
|
||||
{
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
if ($role->system_name === 'admin') {
|
||||
$permissions = $this->permission->all()->pluck('id')->toArray();
|
||||
$role->permissions()->sync($permissions);
|
||||
}
|
||||
|
||||
$role->fill($roleData);
|
||||
$role->save();
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign an list of permission names to an role.
|
||||
* @param Role $role
|
||||
* @param array $permissionNameArray
|
||||
*/
|
||||
public function assignRolePermissions(Role $role, $permissionNameArray = [])
|
||||
{
|
||||
$permissions = [];
|
||||
$permissionNameArray = array_values($permissionNameArray);
|
||||
if ($permissionNameArray && count($permissionNameArray) > 0) {
|
||||
$permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray();
|
||||
}
|
||||
$role->permissions()->sync($permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a role from the system.
|
||||
* Check it's not an admin role or set as default before deleting.
|
||||
* If an migration Role ID is specified the users assign to the current role
|
||||
* will be added to the role of the specified id.
|
||||
* @param $roleId
|
||||
* @param $migrateRoleId
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function deleteRole($roleId, $migrateRoleId)
|
||||
{
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
// Prevent deleting admin role or default registration role.
|
||||
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
|
||||
throw new PermissionsException(trans('errors.role_system_cannot_be_deleted'));
|
||||
} else if ($role->id == setting('registration-role')) {
|
||||
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
|
||||
}
|
||||
|
||||
if ($migrateRoleId) {
|
||||
$newRole = $this->role->find($migrateRoleId);
|
||||
if ($newRole) {
|
||||
$users = $role->users->pluck('id')->toArray();
|
||||
$newRole->users()->sync($users);
|
||||
}
|
||||
}
|
||||
|
||||
$this->permissionService->deleteJointPermissionsForRole($role);
|
||||
$role->delete();
|
||||
}
|
||||
|
||||
}
|
||||
135
app/Repos/TagRepo.php
Normal file
135
app/Repos/TagRepo.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use BookStack\Tag;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Services\PermissionService;
|
||||
|
||||
/**
|
||||
* Class TagRepo
|
||||
* @package BookStack\Repos
|
||||
*/
|
||||
class TagRepo
|
||||
{
|
||||
|
||||
protected $tag;
|
||||
protected $entity;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* TagRepo constructor.
|
||||
* @param Tag $attr
|
||||
* @param Entity $ent
|
||||
* @param PermissionService $ps
|
||||
*/
|
||||
public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
|
||||
{
|
||||
$this->tag = $attr;
|
||||
$this->entity = $ent;
|
||||
$this->permissionService = $ps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance of its particular type.
|
||||
* @param $entityType
|
||||
* @param $entityId
|
||||
* @param string $action
|
||||
*/
|
||||
public function getEntity($entityType, $entityId, $action = 'view')
|
||||
{
|
||||
$entityInstance = $this->entity->getEntityInstance($entityType);
|
||||
$searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
|
||||
$searchQuery = $this->permissionService->enforceEntityRestrictions($entityType, $searchQuery, $action);
|
||||
return $searchQuery->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags for a particular entity.
|
||||
* @param string $entityType
|
||||
* @param int $entityId
|
||||
* @return mixed
|
||||
*/
|
||||
public function getForEntity($entityType, $entityId)
|
||||
{
|
||||
$entity = $this->getEntity($entityType, $entityId);
|
||||
if ($entity === null) return collect();
|
||||
|
||||
return $entity->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag name suggestions from scanning existing tag names.
|
||||
* If no search term is given the 50 most popular tag names are provided.
|
||||
* @param $searchTerm
|
||||
* @return array
|
||||
*/
|
||||
public function getNameSuggestions($searchTerm = false)
|
||||
{
|
||||
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
|
||||
} else {
|
||||
$query = $query->orderBy('count', 'desc')->take(50);
|
||||
}
|
||||
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
return $query->get(['name'])->pluck('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag value suggestions from scanning existing tag values.
|
||||
* If no search is given the 50 most popular values are provided.
|
||||
* Passing a tagName will only find values for a tags with a particular name.
|
||||
* @param $searchTerm
|
||||
* @param $tagName
|
||||
* @return array
|
||||
*/
|
||||
public function getValueSuggestions($searchTerm = false, $tagName = false)
|
||||
{
|
||||
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
|
||||
} else {
|
||||
$query = $query->orderBy('count', 'desc')->take(50);
|
||||
}
|
||||
|
||||
if ($tagName !== false) $query = $query->where('name', '=', $tagName);
|
||||
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
return $query->get(['value'])->pluck('value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an array of tags to an entity
|
||||
* @param Entity $entity
|
||||
* @param array $tags
|
||||
* @return array|\Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function saveTagsToEntity(Entity $entity, $tags = [])
|
||||
{
|
||||
$entity->tags()->delete();
|
||||
$newTags = [];
|
||||
foreach ($tags as $tag) {
|
||||
if (trim($tag['name']) === '') continue;
|
||||
$newTags[] = $this->newInstanceFromInput($tag);
|
||||
}
|
||||
|
||||
return $entity->tags()->saveMany($newTags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Tag instance from user input.
|
||||
* @param $input
|
||||
* @return Tag
|
||||
*/
|
||||
protected function newInstanceFromInput($input)
|
||||
{
|
||||
$name = trim($input['name']);
|
||||
$value = isset($input['value']) ? trim($input['value']) : '';
|
||||
// Any other modification or cleanup required can go here
|
||||
$values = ['name' => $name, 'value' => $value];
|
||||
return $this->tag->newInstance($values);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +1,27 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use Setting;
|
||||
use Exception;
|
||||
|
||||
class UserRepo
|
||||
{
|
||||
|
||||
protected $user;
|
||||
protected $role;
|
||||
protected $entityRepo;
|
||||
|
||||
/**
|
||||
* UserRepo constructor.
|
||||
* @param $user
|
||||
* @param User $user
|
||||
* @param Role $role
|
||||
* @param EntityRepo $entityRepo
|
||||
*/
|
||||
public function __construct(User $user, Role $role)
|
||||
public function __construct(User $user, Role $role, EntityRepo $entityRepo)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->role = $role;
|
||||
$this->entityRepo = $entityRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,6 +42,36 @@ class UserRepo
|
||||
return $this->user->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions.
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function getAllUsers()
|
||||
{
|
||||
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions in a paginated format.
|
||||
* @param int $count
|
||||
* @param $sortData
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function getAllUsersPaginatedAndSorted($count = 20, $sortData)
|
||||
{
|
||||
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
|
||||
|
||||
if ($sortData['search']) {
|
||||
$term = '%' . $sortData['search'] . '%';
|
||||
$query->where(function($query) use ($term) {
|
||||
$query->where('name', 'like', $term)
|
||||
->orWhere('email', 'like', $term);
|
||||
});
|
||||
}
|
||||
|
||||
return $query->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user and attaches a role to them.
|
||||
* @param array $data
|
||||
@@ -51,9 +84,14 @@ class UserRepo
|
||||
|
||||
// Get avatar from gravatar and save
|
||||
if (!config('services.disable_services')) {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
try {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
} catch (Exception $e) {
|
||||
$user->save();
|
||||
\Log::error('Failed to save user gravatar image');
|
||||
}
|
||||
}
|
||||
|
||||
return $user;
|
||||
@@ -65,8 +103,8 @@ class UserRepo
|
||||
*/
|
||||
public function attachDefaultRole($user)
|
||||
{
|
||||
$roleId = Setting::get('registration-role');
|
||||
if ($roleId === false) $roleId = $this->role->getDefault()->id;
|
||||
$roleId = setting('registration-role');
|
||||
if ($roleId === false) $roleId = $this->role->first()->id;
|
||||
$user->attachRoleId($roleId);
|
||||
}
|
||||
|
||||
@@ -77,15 +115,10 @@ class UserRepo
|
||||
*/
|
||||
public function isOnlyAdmin(User $user)
|
||||
{
|
||||
if ($user->role->name != 'admin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$adminRole = $this->role->where('name', '=', 'admin')->first();
|
||||
if (count($adminRole->users) > 1) {
|
||||
return false;
|
||||
}
|
||||
if (!$user->roles->pluck('name')->contains('admin')) return false;
|
||||
|
||||
$adminRole = $this->role->getRole('admin');
|
||||
if ($adminRole->users->count() > 1) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -99,7 +132,8 @@ class UserRepo
|
||||
return $this->user->forceCreate([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password'])
|
||||
'password' => bcrypt($data['password']),
|
||||
'email_confirmed' => false
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -112,4 +146,71 @@ class UserRepo
|
||||
$user->socialAccounts()->delete();
|
||||
$user->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest activity for a user.
|
||||
* @param User $user
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function getActivity(User $user, $count = 20, $page = 0)
|
||||
{
|
||||
return \Activity::userActivity($user, $count, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recently created content for this given user.
|
||||
* @param User $user
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyCreated(User $user, $count = 20)
|
||||
{
|
||||
return [
|
||||
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, function ($query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
}),
|
||||
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, function ($query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
}),
|
||||
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, function ($query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset created counts for the give user.
|
||||
* @param User $user
|
||||
* @return array
|
||||
*/
|
||||
public function getAssetCounts(User $user)
|
||||
{
|
||||
return [
|
||||
'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(),
|
||||
'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->count(),
|
||||
'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles in the system that are assignable to a user.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAllRoles()
|
||||
{
|
||||
return $this->role->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the roles which can be given restricted access to
|
||||
* other entities in the system.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRestrictableRoles()
|
||||
{
|
||||
return $this->role->where('system_name', '!=', 'admin')->get();
|
||||
}
|
||||
|
||||
}
|
||||
75
app/Role.php
75
app/Role.php
@@ -1,58 +1,95 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
/**
|
||||
* Sets the default role name for newly registered users.
|
||||
* @var string
|
||||
*/
|
||||
protected static $default = 'viewer';
|
||||
|
||||
protected $fillable = ['display_name', 'description'];
|
||||
|
||||
/**
|
||||
* The roles that belong to the role.
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\User');
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The permissions that belong to the role.
|
||||
* Get all related JointPermissions.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function jointPermissions()
|
||||
{
|
||||
return $this->hasMany(JointPermission::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The RolePermissions that belong to the role.
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Permission');
|
||||
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this role has a permission.
|
||||
* @param $permissionName
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPermission($permissionName)
|
||||
{
|
||||
$permissions = $this->getRelationValue('permissions');
|
||||
foreach ($permissions as $permission) {
|
||||
if ($permission->getRawAttribute('name') === $permissionName) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a permission to this role.
|
||||
* @param Permission $permission
|
||||
* @param RolePermission $permission
|
||||
*/
|
||||
public function attachPermission(Permission $permission)
|
||||
public function attachPermission(RolePermission $permission)
|
||||
{
|
||||
$this->permissions()->attach($permission->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the default role.
|
||||
* @return Role
|
||||
* Detach a single permission from this role.
|
||||
* @param RolePermission $permission
|
||||
*/
|
||||
public static function getDefault()
|
||||
public function detachPermission(RolePermission $permission)
|
||||
{
|
||||
return static::getRole(static::$default);
|
||||
$this->permissions()->detach($permission->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role object for the specified role.
|
||||
* @param $roleName
|
||||
* @return mixed
|
||||
* @return Role
|
||||
*/
|
||||
public static function getRole($roleName)
|
||||
{
|
||||
return static::where('name', '=', $roleName)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role object for the specified system role.
|
||||
* @param $roleName
|
||||
* @return Role
|
||||
*/
|
||||
public static function getSystemRole($roleName)
|
||||
{
|
||||
return static::where('system_name', '=', $roleName)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible roles
|
||||
* @return mixed
|
||||
*/
|
||||
public static function visible()
|
||||
{
|
||||
return static::where('hidden', '=', false)->orderBy('name')->get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
23
app/RolePermission.php
Normal file
23
app/RolePermission.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class RolePermission extends Model
|
||||
{
|
||||
/**
|
||||
* The roles that belong to the permission.
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany(Role::class, 'permission_role','permission_id', 'role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permission object by name.
|
||||
* @param $name
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getByName($name)
|
||||
{
|
||||
return static::where('name', '=', $name)->first();
|
||||
}
|
||||
}
|
||||
18
app/SearchTerm.php
Normal file
18
app/SearchTerm.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
class SearchTerm extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get the entity that this term belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use BookStack\Activity;
|
||||
use BookStack\Entity;
|
||||
use Session;
|
||||
@@ -9,40 +8,44 @@ class ActivityService
|
||||
{
|
||||
protected $activity;
|
||||
protected $user;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* ActivityService constructor.
|
||||
* @param $activity
|
||||
* @param Activity $activity
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(Activity $activity)
|
||||
public function __construct(Activity $activity, PermissionService $permissionService)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
$this->user = auth()->user();
|
||||
$this->permissionService = $permissionService;
|
||||
$this->user = user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add activity data to database.
|
||||
* @param Entity $entity
|
||||
* @param $activityKey
|
||||
* @param int $bookId
|
||||
* @param bool $extra
|
||||
* @param int $bookId
|
||||
* @param bool $extra
|
||||
*/
|
||||
public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false)
|
||||
{
|
||||
$this->activity->user_id = $this->user->id;
|
||||
$this->activity->book_id = $bookId;
|
||||
$this->activity->key = strtolower($activityKey);
|
||||
$activity = $this->activity->newInstance();
|
||||
$activity->user_id = $this->user->id;
|
||||
$activity->book_id = $bookId;
|
||||
$activity->key = strtolower($activityKey);
|
||||
if ($extra !== false) {
|
||||
$this->activity->extra = $extra;
|
||||
$activity->extra = $extra;
|
||||
}
|
||||
$entity->activity()->save($this->activity);
|
||||
$entity->activity()->save($activity);
|
||||
$this->setNotification($activityKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a activity history with a message & without binding to a entitiy.
|
||||
* Adds a activity history with a message & without binding to a entity.
|
||||
* @param $activityKey
|
||||
* @param int $bookId
|
||||
* @param int $bookId
|
||||
* @param bool|false $extra
|
||||
*/
|
||||
public function addMessage($activityKey, $bookId = 0, $extra = false)
|
||||
@@ -85,37 +88,63 @@ class ActivityService
|
||||
*/
|
||||
public function latest($count = 20, $page = 0)
|
||||
{
|
||||
$activityList = $this->activity->orderBy('created_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get();
|
||||
|
||||
return $this->filterSimilar($activityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest activity for an entitiy, Filtering out similar
|
||||
* Gets the latest activity for an entity, Filtering out similar
|
||||
* items to prevent a message activity list.
|
||||
* @param Entity $entity
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
function entityActivity($entity, $count = 20, $page = 0)
|
||||
public function entityActivity($entity, $count = 20, $page = 0)
|
||||
{
|
||||
$activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
if ($entity->isA('book')) {
|
||||
$query = $this->activity->where('book_id', '=', $entity->id);
|
||||
} else {
|
||||
$query = $this->activity->where('entity_type', '=', get_class($entity))
|
||||
->where('entity_id', '=', $entity->id);
|
||||
}
|
||||
|
||||
$activity = $this->permissionService
|
||||
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->with(['entity', 'user.avatar'])->skip($count * $page)->take($count)->get();
|
||||
|
||||
return $this->filterSimilar($activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out similar activity.
|
||||
* @param Activity[] $activity
|
||||
* Get latest activity for a user, Filtering out similar
|
||||
* items.
|
||||
* @param $user
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
protected function filterSimilar($activity)
|
||||
public function userActivity($user, $count = 20, $page = 0)
|
||||
{
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get();
|
||||
return $this->filterSimilar($activityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out similar activity.
|
||||
* @param Activity[] $activities
|
||||
* @return array
|
||||
*/
|
||||
protected function filterSimilar($activities)
|
||||
{
|
||||
$newActivity = [];
|
||||
$previousItem = false;
|
||||
foreach ($activity as $activityItem) {
|
||||
foreach ($activities as $activityItem) {
|
||||
if ($previousItem === false) {
|
||||
$previousItem = $activityItem;
|
||||
$newActivity[] = $activityItem;
|
||||
|
||||
201
app/Services/AttachmentService.php
Normal file
201
app/Services/AttachmentService.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
use BookStack\Attachment;
|
||||
use Exception;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class AttachmentService extends UploadService
|
||||
{
|
||||
|
||||
/**
|
||||
* Get an attachment from storage.
|
||||
* @param Attachment $attachment
|
||||
* @return string
|
||||
*/
|
||||
public function getAttachmentFromStorage(Attachment $attachment)
|
||||
{
|
||||
$attachmentPath = $this->getStorageBasePath() . $attachment->path;
|
||||
return $this->getStorage()->get($attachmentPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new attachment upon user upload.
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @param int $page_id
|
||||
* @return Attachment
|
||||
* @throws FileUploadException
|
||||
*/
|
||||
public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
|
||||
{
|
||||
$attachmentName = $uploadedFile->getClientOriginalName();
|
||||
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
|
||||
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
|
||||
|
||||
$attachment = Attachment::forceCreate([
|
||||
'name' => $attachmentName,
|
||||
'path' => $attachmentPath,
|
||||
'extension' => $uploadedFile->getClientOriginalExtension(),
|
||||
'uploaded_to' => $page_id,
|
||||
'created_by' => user()->id,
|
||||
'updated_by' => user()->id,
|
||||
'order' => $largestExistingOrder + 1
|
||||
]);
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a upload, saving to a file and deleting any existing uploads
|
||||
* attached to that file.
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @param Attachment $attachment
|
||||
* @return Attachment
|
||||
* @throws FileUploadException
|
||||
*/
|
||||
public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment)
|
||||
{
|
||||
if (!$attachment->external) {
|
||||
$this->deleteFileInStorage($attachment);
|
||||
}
|
||||
|
||||
$attachmentName = $uploadedFile->getClientOriginalName();
|
||||
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
|
||||
|
||||
$attachment->name = $attachmentName;
|
||||
$attachment->path = $attachmentPath;
|
||||
$attachment->external = false;
|
||||
$attachment->extension = $uploadedFile->getClientOriginalExtension();
|
||||
$attachment->save();
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new File attachment from a given link and name.
|
||||
* @param string $name
|
||||
* @param string $link
|
||||
* @param int $page_id
|
||||
* @return Attachment
|
||||
*/
|
||||
public function saveNewFromLink($name, $link, $page_id)
|
||||
{
|
||||
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
|
||||
return Attachment::forceCreate([
|
||||
'name' => $name,
|
||||
'path' => $link,
|
||||
'external' => true,
|
||||
'extension' => '',
|
||||
'uploaded_to' => $page_id,
|
||||
'created_by' => user()->id,
|
||||
'updated_by' => user()->id,
|
||||
'order' => $largestExistingOrder + 1
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file storage base path, amended for storage type.
|
||||
* This allows us to keep a generic path in the database.
|
||||
* @return string
|
||||
*/
|
||||
private function getStorageBasePath()
|
||||
{
|
||||
return $this->isLocal() ? 'storage/' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the file ordering for a listing of attached files.
|
||||
* @param array $attachmentList
|
||||
* @param $pageId
|
||||
*/
|
||||
public function updateFileOrderWithinPage($attachmentList, $pageId)
|
||||
{
|
||||
foreach ($attachmentList as $index => $attachment) {
|
||||
Attachment::where('uploaded_to', '=', $pageId)->where('id', '=', $attachment['id'])->update(['order' => $index]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the details of a file.
|
||||
* @param Attachment $attachment
|
||||
* @param $requestData
|
||||
* @return Attachment
|
||||
*/
|
||||
public function updateFile(Attachment $attachment, $requestData)
|
||||
{
|
||||
$attachment->name = $requestData['name'];
|
||||
if (isset($requestData['link']) && trim($requestData['link']) !== '') {
|
||||
$attachment->path = $requestData['link'];
|
||||
if (!$attachment->external) {
|
||||
$this->deleteFileInStorage($attachment);
|
||||
$attachment->external = true;
|
||||
}
|
||||
}
|
||||
$attachment->save();
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a File from the database and storage.
|
||||
* @param Attachment $attachment
|
||||
*/
|
||||
public function deleteFile(Attachment $attachment)
|
||||
{
|
||||
if ($attachment->external) {
|
||||
$attachment->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->deleteFileInStorage($attachment);
|
||||
$attachment->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file from the filesystem it sits on.
|
||||
* Cleans any empty leftover folders.
|
||||
* @param Attachment $attachment
|
||||
*/
|
||||
protected function deleteFileInStorage(Attachment $attachment)
|
||||
{
|
||||
$storedFilePath = $this->getStorageBasePath() . $attachment->path;
|
||||
$storage = $this->getStorage();
|
||||
$dirPath = dirname($storedFilePath);
|
||||
|
||||
$storage->delete($storedFilePath);
|
||||
if (count($storage->allFiles($dirPath)) === 0) {
|
||||
$storage->deleteDirectory($dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a file in storage with the given filename
|
||||
* @param $attachmentName
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @return string
|
||||
* @throws FileUploadException
|
||||
*/
|
||||
protected function putFileInStorage($attachmentName, UploadedFile $uploadedFile)
|
||||
{
|
||||
$attachmentData = file_get_contents($uploadedFile->getRealPath());
|
||||
|
||||
$storage = $this->getStorage();
|
||||
$attachmentBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
|
||||
$storageBasePath = $this->getStorageBasePath() . $attachmentBasePath;
|
||||
|
||||
$uploadFileName = $attachmentName;
|
||||
while ($storage->exists($storageBasePath . $uploadFileName)) {
|
||||
$uploadFileName = str_random(3) . $uploadFileName;
|
||||
}
|
||||
|
||||
$attachmentPath = $attachmentBasePath . $uploadFileName;
|
||||
$attachmentStoragePath = $this->getStorageBasePath() . $attachmentPath;
|
||||
|
||||
try {
|
||||
$storage->put($attachmentStoragePath, $attachmentData);
|
||||
} catch (Exception $e) {
|
||||
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentStoragePath]));
|
||||
}
|
||||
return $attachmentPath;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,30 +1,27 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Notifications\ConfirmEmail;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Mail\Mailer;
|
||||
use Illuminate\Mail\Message;
|
||||
use BookStack\EmailConfirmation;
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Setting;
|
||||
use BookStack\User;
|
||||
use Illuminate\Database\Connection as Database;
|
||||
|
||||
class EmailConfirmationService
|
||||
{
|
||||
protected $mailer;
|
||||
protected $emailConfirmation;
|
||||
protected $db;
|
||||
protected $users;
|
||||
|
||||
/**
|
||||
* EmailConfirmationService constructor.
|
||||
* @param Mailer $mailer
|
||||
* @param EmailConfirmation $emailConfirmation
|
||||
* @param Database $db
|
||||
* @param UserRepo $users
|
||||
*/
|
||||
public function __construct(Mailer $mailer, EmailConfirmation $emailConfirmation)
|
||||
public function __construct(Database $db, UserRepo $users)
|
||||
{
|
||||
$this->mailer = $mailer;
|
||||
$this->emailConfirmation = $emailConfirmation;
|
||||
$this->db = $db;
|
||||
$this->users = $users;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,45 +33,59 @@ class EmailConfirmationService
|
||||
public function sendConfirmation(User $user)
|
||||
{
|
||||
if ($user->email_confirmed) {
|
||||
throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login');
|
||||
throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
|
||||
}
|
||||
|
||||
$this->deleteConfirmationsByUser($user);
|
||||
$token = $this->createEmailConfirmation($user);
|
||||
|
||||
$user->notify(new ConfirmEmail($token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new email confirmation in the database and returns the token.
|
||||
* @param User $user
|
||||
* @return string
|
||||
*/
|
||||
public function createEmailConfirmation(User $user)
|
||||
{
|
||||
$token = $this->getToken();
|
||||
$this->emailConfirmation->create([
|
||||
$this->db->table('email_confirmations')->insert([
|
||||
'user_id' => $user->id,
|
||||
'token' => $token,
|
||||
'token' => $token,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now()
|
||||
]);
|
||||
$this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) {
|
||||
$appName = \Setting::get('app-name', 'BookStack');
|
||||
$message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.');
|
||||
});
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an email confirmation by looking up the token,
|
||||
* Ensures the token has not expired.
|
||||
* @param string $token
|
||||
* @return EmailConfirmation
|
||||
* @return array|null|\stdClass
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function getEmailConfirmationFromToken($token)
|
||||
{
|
||||
$emailConfirmation = $this->emailConfirmation->where('token', '=', $token)->first();
|
||||
// If not found
|
||||
$emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first();
|
||||
|
||||
// If not found show error
|
||||
if ($emailConfirmation === null) {
|
||||
throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register');
|
||||
throw new UserRegistrationException(trans('errors.email_confirmation_invalid'), '/register');
|
||||
}
|
||||
|
||||
// If more than a day old
|
||||
if (Carbon::now()->subDay()->gt($emailConfirmation->created_at)) {
|
||||
$this->sendConfirmation($emailConfirmation->user);
|
||||
throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm');
|
||||
if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
|
||||
$user = $this->users->getById($emailConfirmation->user_id);
|
||||
$this->sendConfirmation($user);
|
||||
throw new UserRegistrationException(trans('errors.email_confirmation_expired'), '/register/confirm');
|
||||
}
|
||||
|
||||
$emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
|
||||
return $emailConfirmation;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete all email confirmations that belong to a user.
|
||||
* @param User $user
|
||||
@@ -82,7 +93,7 @@ class EmailConfirmationService
|
||||
*/
|
||||
public function deleteConfirmationsByUser(User $user)
|
||||
{
|
||||
return $this->emailConfirmation->where('user_id', '=', $user->id)->delete();
|
||||
return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +103,7 @@ class EmailConfirmationService
|
||||
protected function getToken()
|
||||
{
|
||||
$token = str_random(24);
|
||||
while ($this->emailConfirmation->where('token', '=', $token)->exists()) {
|
||||
while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) {
|
||||
$token = str_random(25);
|
||||
}
|
||||
return $token;
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Page;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
|
||||
class ExportService
|
||||
{
|
||||
|
||||
protected $entityRepo;
|
||||
|
||||
/**
|
||||
* ExportService constructor.
|
||||
* @param $entityRepo
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page to a self-contained HTML file.
|
||||
* Includes required CSS & image content. Images are base64 encoded into the HTML.
|
||||
@@ -14,22 +27,108 @@ class ExportService
|
||||
*/
|
||||
public function pageToContainedHtml(Page $page)
|
||||
{
|
||||
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
|
||||
$pageHtml = view('pages/export', ['page' => $page, 'css' => $cssContent])->render();
|
||||
$pageHtml = view('pages/export', [
|
||||
'page' => $page,
|
||||
'pageContent' => $this->entityRepo->renderPage($page)
|
||||
])->render();
|
||||
return $this->containHtml($pageHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page to a pdf file.
|
||||
* Convert a chapter to a self-contained HTML file.
|
||||
* @param Chapter $chapter
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function chapterToContainedHtml(Chapter $chapter)
|
||||
{
|
||||
$pages = $this->entityRepo->getChapterChildren($chapter);
|
||||
$pages->each(function($page) {
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
});
|
||||
$html = view('chapters/export', [
|
||||
'chapter' => $chapter,
|
||||
'pages' => $pages
|
||||
])->render();
|
||||
return $this->containHtml($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a book to a self-contained HTML file.
|
||||
* @param Book $book
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function bookToContainedHtml(Book $book)
|
||||
{
|
||||
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
|
||||
$html = view('books/export', [
|
||||
'book' => $book,
|
||||
'bookChildren' => $bookTree
|
||||
])->render();
|
||||
return $this->containHtml($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page to a PDF file.
|
||||
* @param Page $page
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function pageToPdf(Page $page)
|
||||
{
|
||||
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
|
||||
$pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render();
|
||||
$containedHtml = $this->containHtml($pageHtml);
|
||||
$pdf = \PDF::loadHTML($containedHtml);
|
||||
$html = view('pages/pdf', [
|
||||
'page' => $page,
|
||||
'pageContent' => $this->entityRepo->renderPage($page)
|
||||
])->render();
|
||||
return $this->htmlToPdf($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a chapter to a PDF file.
|
||||
* @param Chapter $chapter
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function chapterToPdf(Chapter $chapter)
|
||||
{
|
||||
$pages = $this->entityRepo->getChapterChildren($chapter);
|
||||
$pages->each(function($page) {
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
});
|
||||
$html = view('chapters/export', [
|
||||
'chapter' => $chapter,
|
||||
'pages' => $pages
|
||||
])->render();
|
||||
return $this->htmlToPdf($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a book to a PDF file
|
||||
* @param Book $book
|
||||
* @return string
|
||||
*/
|
||||
public function bookToPdf(Book $book)
|
||||
{
|
||||
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
|
||||
$html = view('books/export', [
|
||||
'book' => $book,
|
||||
'bookChildren' => $bookTree
|
||||
])->render();
|
||||
return $this->htmlToPdf($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert normal webpage HTML to a PDF.
|
||||
* @param $html
|
||||
* @return string
|
||||
*/
|
||||
protected function htmlToPdf($html)
|
||||
{
|
||||
$containedHtml = $this->containHtml($html);
|
||||
$useWKHTML = config('snappy.pdf.binary') !== false;
|
||||
if ($useWKHTML) {
|
||||
$pdf = \SnappyPDF::loadHTML($containedHtml);
|
||||
$pdf->setOption('print-media-type', true);
|
||||
} else {
|
||||
$pdf = \PDF::loadHTML($containedHtml);
|
||||
}
|
||||
return $pdf->output();
|
||||
}
|
||||
|
||||
@@ -48,14 +147,20 @@ class ExportService
|
||||
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
||||
$oldImgString = $imgMatch;
|
||||
$srcString = $imageTagsOutput[2][$index];
|
||||
if (strpos(trim($srcString), 'http') !== 0) {
|
||||
$pathString = public_path($srcString);
|
||||
$isLocal = strpos(trim($srcString), 'http') !== 0;
|
||||
if ($isLocal) {
|
||||
$pathString = public_path(trim($srcString, '/'));
|
||||
} else {
|
||||
$pathString = $srcString;
|
||||
}
|
||||
$imageContent = file_get_contents($pathString);
|
||||
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
||||
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
||||
if ($isLocal && !file_exists($pathString)) continue;
|
||||
try {
|
||||
$imageContent = file_get_contents($pathString);
|
||||
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
||||
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
||||
} catch (\ErrorException $e) {
|
||||
$newImageString = '';
|
||||
}
|
||||
$htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
|
||||
}
|
||||
}
|
||||
@@ -82,14 +187,14 @@ class ExportService
|
||||
|
||||
/**
|
||||
* Converts the page contents into simple plain text.
|
||||
* This method filters any bad looking content to
|
||||
* provide a nice final output.
|
||||
* This method filters any bad looking content to provide a nice final output.
|
||||
* @param Page $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function pageToPlainText(Page $page)
|
||||
{
|
||||
$text = $page->text;
|
||||
$html = $this->entityRepo->renderPage($page);
|
||||
$text = strip_tags($html);
|
||||
// Replace multiple spaces with single spaces
|
||||
$text = preg_replace('/\ {2,}/', ' ', $text);
|
||||
// Reduce multiple horrid whitespace characters.
|
||||
@@ -100,6 +205,40 @@ class ExportService
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a chapter into a plain text string.
|
||||
* @param Chapter $chapter
|
||||
* @return string
|
||||
*/
|
||||
public function chapterToPlainText(Chapter $chapter)
|
||||
{
|
||||
$text = $chapter->name . "\n\n";
|
||||
$text .= $chapter->description . "\n\n";
|
||||
foreach ($chapter->pages as $page) {
|
||||
$text .= $this->pageToPlainText($page);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a book into a plain text string.
|
||||
* @param Book $book
|
||||
* @return string
|
||||
*/
|
||||
public function bookToPlainText(Book $book)
|
||||
{
|
||||
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
|
||||
$text = $book->name . "\n\n";
|
||||
foreach ($bookTree as $bookChild) {
|
||||
if ($bookChild->isA('chapter')) {
|
||||
$text .= $this->chapterToPlainText($bookChild);
|
||||
} else {
|
||||
$text .= $this->pageToPlainText($bookChild);
|
||||
}
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,20 +9,13 @@ use Intervention\Image\ImageManager;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Setting;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImageService
|
||||
class ImageService extends UploadService
|
||||
{
|
||||
|
||||
protected $imageTool;
|
||||
protected $fileSystem;
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* @var FileSystemInstance
|
||||
*/
|
||||
protected $storageInstance;
|
||||
protected $storageUrl;
|
||||
|
||||
/**
|
||||
@@ -34,21 +27,23 @@ class ImageService
|
||||
public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
|
||||
{
|
||||
$this->imageTool = $imageTool;
|
||||
$this->fileSystem = $fileSystem;
|
||||
$this->cache = $cache;
|
||||
parent::__construct($fileSystem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new image from an upload.
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @param string $type
|
||||
* @param string $type
|
||||
* @param int $uploadedTo
|
||||
* @return mixed
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
public function saveNewFromUpload(UploadedFile $uploadedFile, $type)
|
||||
public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0)
|
||||
{
|
||||
$imageName = $uploadedFile->getClientOriginalName();
|
||||
$imageData = file_get_contents($uploadedFile->getRealPath());
|
||||
return $this->saveNew($imageName, $imageData, $type);
|
||||
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +59,7 @@ class ImageService
|
||||
{
|
||||
$imageName = $imageName ? $imageName : basename($url);
|
||||
$imageData = file_get_contents($url);
|
||||
if($imageData === false) throw new \Exception('Cannot get image from ' . $url);
|
||||
if($imageData === false) throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
|
||||
return $this->saveNew($imageName, $imageData, $type);
|
||||
}
|
||||
|
||||
@@ -73,18 +68,22 @@ class ImageService
|
||||
* @param string $imageName
|
||||
* @param string $imageData
|
||||
* @param string $type
|
||||
* @param int $uploadedTo
|
||||
* @return Image
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
private function saveNew($imageName, $imageData, $type)
|
||||
private function saveNew($imageName, $imageData, $type, $uploadedTo = 0)
|
||||
{
|
||||
$storage = $this->getStorage();
|
||||
$secureUploads = Setting::get('app-secure-images');
|
||||
$secureUploads = setting('app-secure-images');
|
||||
$imageName = str_replace(' ', '-', $imageName);
|
||||
|
||||
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
|
||||
|
||||
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
|
||||
|
||||
if ($this->isLocal()) $imagePath = '/public' . $imagePath;
|
||||
|
||||
while ($storage->exists($imagePath . $imageName)) {
|
||||
$imageName = str_random(3) . $imageName;
|
||||
}
|
||||
@@ -92,19 +91,23 @@ class ImageService
|
||||
|
||||
try {
|
||||
$storage->put($fullPath, $imageData);
|
||||
$storage->setVisibility($fullPath, 'public');
|
||||
} catch (Exception $e) {
|
||||
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
|
||||
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
|
||||
}
|
||||
|
||||
if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
|
||||
|
||||
$imageDetails = [
|
||||
'name' => $imageName,
|
||||
'path' => $fullPath,
|
||||
'url' => $this->getPublicUrl($fullPath),
|
||||
'type' => $type
|
||||
'type' => $type,
|
||||
'uploaded_to' => $uploadedTo
|
||||
];
|
||||
|
||||
if (auth()->user() && auth()->user()->id !== 0) {
|
||||
$userId = auth()->user()->id;
|
||||
if (user()->id !== 0) {
|
||||
$userId = user()->id;
|
||||
$imageDetails['created_by'] = $userId;
|
||||
$imageDetails['updated_by'] = $userId;
|
||||
}
|
||||
@@ -114,6 +117,16 @@ class ImageService
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage path, Dependant of storage type.
|
||||
* @param Image $image
|
||||
* @return mixed|string
|
||||
*/
|
||||
protected function getPath(Image $image)
|
||||
{
|
||||
return ($this->isLocal()) ? ('public/' . $image->path) : $image->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the thumbnail for an image.
|
||||
* If $keepRatio is true only the width will be used.
|
||||
@@ -130,7 +143,8 @@ class ImageService
|
||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||
{
|
||||
$thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
|
||||
$thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path);
|
||||
$imagePath = $this->getPath($image);
|
||||
$thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
|
||||
|
||||
if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
@@ -143,10 +157,10 @@ class ImageService
|
||||
}
|
||||
|
||||
try {
|
||||
$thumb = $this->imageTool->make($storage->get($image->path));
|
||||
$thumb = $this->imageTool->make($storage->get($imagePath));
|
||||
} catch (Exception $e) {
|
||||
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
||||
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
|
||||
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
@@ -163,6 +177,7 @@ class ImageService
|
||||
|
||||
$thumbData = (string)$thumb->encode();
|
||||
$storage->put($thumbFilePath, $thumbData);
|
||||
$storage->setVisibility($thumbFilePath, 'public');
|
||||
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
|
||||
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
@@ -177,8 +192,8 @@ class ImageService
|
||||
{
|
||||
$storage = $this->getStorage();
|
||||
|
||||
$imageFolder = dirname($image->path);
|
||||
$imageFileName = basename($image->path);
|
||||
$imageFolder = dirname($this->getPath($image));
|
||||
$imageFileName = basename($this->getPath($image));
|
||||
$allImages = collect($storage->allFiles($imageFolder));
|
||||
|
||||
$imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
|
||||
@@ -207,7 +222,7 @@ class ImageService
|
||||
public function saveUserGravatar(User $user, $size = 500)
|
||||
{
|
||||
$emailHash = md5(strtolower(trim($user->email)));
|
||||
$url = 'http://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
|
||||
$url = 'https://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
|
||||
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
|
||||
$image = $this->saveNewFromUrl($url, 'user', $imageName);
|
||||
$image->created_by = $user->id;
|
||||
@@ -216,35 +231,9 @@ class ImageService
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage that will be used for storing images.
|
||||
* @return FileSystemInstance
|
||||
*/
|
||||
private function getStorage()
|
||||
{
|
||||
if ($this->storageInstance !== null) return $this->storageInstance;
|
||||
|
||||
$storageType = config('filesystems.default');
|
||||
$this->storageInstance = $this->fileSystem->disk($storageType);
|
||||
|
||||
return $this->storageInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not a folder is empty.
|
||||
* @param $path
|
||||
* @return int
|
||||
*/
|
||||
private function isFolderEmpty($path)
|
||||
{
|
||||
$files = $this->getStorage()->files($path);
|
||||
$folders = $this->getStorage()->directories($path);
|
||||
return count($files) === 0 && count($folders) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a public facing url for an image by checking relevant environment variables.
|
||||
* @param $filePath
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
*/
|
||||
private function getPublicUrl($filePath)
|
||||
@@ -253,16 +242,24 @@ class ImageService
|
||||
$storageUrl = config('filesystems.url');
|
||||
|
||||
// Get the standard public s3 url if s3 is set as storage type
|
||||
// Uses the nice, short URL if bucket name has no periods in otherwise the longer
|
||||
// region-based url will be used to prevent http issues.
|
||||
if ($storageUrl == false && config('filesystems.default') === 's3') {
|
||||
$storageDetails = config('filesystems.disks.s3');
|
||||
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
||||
if (strpos($storageDetails['bucket'], '.') === false) {
|
||||
$storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
|
||||
} else {
|
||||
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->storageUrl = $storageUrl;
|
||||
}
|
||||
|
||||
return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath;
|
||||
if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath);
|
||||
|
||||
return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,17 @@ class Ldap
|
||||
return ldap_set_option($ldapConnection, $option, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the version number for the given ldap connection.
|
||||
* @param $ldapConnection
|
||||
* @param $version
|
||||
* @return bool
|
||||
*/
|
||||
public function setVersion($ldapConnection, $version)
|
||||
{
|
||||
return $this->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search LDAP tree using the provided filter.
|
||||
* @param resource $ldapConnection
|
||||
@@ -83,4 +94,4 @@ class Ldap
|
||||
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ class LdapService
|
||||
// Find user
|
||||
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
|
||||
$baseDn = $this->config['base_dn'];
|
||||
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', 'mail']);
|
||||
$emailAttr = $this->config['email_attribute'];
|
||||
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
|
||||
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
|
||||
if ($users['count'] === 0) return null;
|
||||
|
||||
$user = $users[0];
|
||||
@@ -49,7 +52,7 @@ class LdapService
|
||||
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
|
||||
'name' => $user['cn'][0],
|
||||
'dn' => $user['dn'],
|
||||
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
|
||||
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
|
||||
];
|
||||
}
|
||||
|
||||
@@ -94,7 +97,7 @@ class LdapService
|
||||
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
|
||||
}
|
||||
|
||||
if (!$ldapBind) throw new LdapException('LDAP access failed using ' . ($isAnonymous ? ' anonymous bind.' : ' given dn & pass details'));
|
||||
if (!$ldapBind) throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,20 +112,24 @@ class LdapService
|
||||
|
||||
// Check LDAP extension in installed
|
||||
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
|
||||
throw new LdapException('LDAP PHP extension not installed');
|
||||
throw new LdapException(trans('errors.ldap_extension_not_installed'));
|
||||
}
|
||||
|
||||
// Get port from server string if specified.
|
||||
// Get port from server string and protocol if specified.
|
||||
$ldapServer = explode(':', $this->config['server']);
|
||||
$ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
|
||||
$hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
|
||||
if (!$hasProtocol) array_unshift($ldapServer, '');
|
||||
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
|
||||
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
|
||||
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
|
||||
|
||||
if ($ldapConnection === false) {
|
||||
throw new LdapException('Cannot connect to ldap server, Initial connection failed');
|
||||
throw new LdapException(trans('errors.ldap_cannot_connect'));
|
||||
}
|
||||
|
||||
// Set any required options
|
||||
if ($this->config['version']) {
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']);
|
||||
$this->ldap->setVersion($ldapConnection, $this->config['version']);
|
||||
}
|
||||
|
||||
$this->ldapConnection = $ldapConnection;
|
||||
|
||||
714
app/Services/PermissionService.php
Normal file
714
app/Services/PermissionService.php
Normal file
@@ -0,0 +1,714 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\EntityPermission;
|
||||
use BookStack\JointPermission;
|
||||
use BookStack\Ownable;
|
||||
use BookStack\Page;
|
||||
use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PermissionService
|
||||
{
|
||||
|
||||
protected $currentAction;
|
||||
protected $isAdminUser;
|
||||
protected $userRoles = false;
|
||||
protected $currentUserModel = false;
|
||||
|
||||
public $book;
|
||||
public $chapter;
|
||||
public $page;
|
||||
|
||||
protected $db;
|
||||
|
||||
protected $jointPermission;
|
||||
protected $role;
|
||||
protected $entityPermission;
|
||||
|
||||
protected $entityCache;
|
||||
|
||||
/**
|
||||
* PermissionService constructor.
|
||||
* @param JointPermission $jointPermission
|
||||
* @param EntityPermission $entityPermission
|
||||
* @param Connection $db
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param Role $role
|
||||
*/
|
||||
public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->jointPermission = $jointPermission;
|
||||
$this->entityPermission = $entityPermission;
|
||||
$this->role = $role;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
// TODO - Update so admin still goes through filters
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the database connection
|
||||
* @param Connection $connection
|
||||
*/
|
||||
public function setConnection(Connection $connection)
|
||||
{
|
||||
$this->db = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the local entity cache and ensure it's empty
|
||||
*/
|
||||
protected function readyEntityCache()
|
||||
{
|
||||
$this->entityCache = [
|
||||
'books' => collect(),
|
||||
'chapters' => collect()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a book via ID, Checks local cache
|
||||
* @param $bookId
|
||||
* @return Book
|
||||
*/
|
||||
protected function getBook($bookId)
|
||||
{
|
||||
if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
|
||||
return $this->entityCache['books']->get($bookId);
|
||||
}
|
||||
|
||||
$book = $this->book->find($bookId);
|
||||
if ($book === null) $book = false;
|
||||
if (isset($this->entityCache['books'])) {
|
||||
$this->entityCache['books']->put($bookId, $book);
|
||||
}
|
||||
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chapter via ID, Checks local cache
|
||||
* @param $chapterId
|
||||
* @return Book
|
||||
*/
|
||||
protected function getChapter($chapterId)
|
||||
{
|
||||
if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
|
||||
return $this->entityCache['chapters']->get($chapterId);
|
||||
}
|
||||
|
||||
$chapter = $this->chapter->find($chapterId);
|
||||
if ($chapter === null) $chapter = false;
|
||||
if (isset($this->entityCache['chapters'])) {
|
||||
$this->entityCache['chapters']->put($chapterId, $chapter);
|
||||
}
|
||||
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles for the current user;
|
||||
* @return array|bool
|
||||
*/
|
||||
protected function getRoles()
|
||||
{
|
||||
if ($this->userRoles !== false) return $this->userRoles;
|
||||
|
||||
$roles = [];
|
||||
|
||||
if (auth()->guest()) {
|
||||
$roles[] = $this->role->getSystemRole('public')->id;
|
||||
return $roles;
|
||||
}
|
||||
|
||||
|
||||
foreach ($this->currentUser()->roles as $role) {
|
||||
$roles[] = $role->id;
|
||||
}
|
||||
return $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-generate all entity permission from scratch.
|
||||
*/
|
||||
public function buildJointPermissions()
|
||||
{
|
||||
$this->jointPermission->truncate();
|
||||
$this->readyEntityCache();
|
||||
|
||||
// Get all roles (Should be the most limited dimension)
|
||||
$roles = $this->role->with('permissions')->get()->all();
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a query for fetching a book with it's children.
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
protected function bookFetchQuery()
|
||||
{
|
||||
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id']);
|
||||
}, 'pages' => function($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
|
||||
}]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build joint permissions for an array of books
|
||||
* @param Collection $books
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
*/
|
||||
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false) {
|
||||
$entities = clone $books;
|
||||
|
||||
/** @var Book $book */
|
||||
foreach ($books->all() as $book) {
|
||||
foreach ($book->getRelation('chapters') as $chapter) {
|
||||
$entities->push($chapter);
|
||||
}
|
||||
foreach ($book->getRelation('pages') as $page) {
|
||||
$entities->push($page);
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteOld) $this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function buildJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
$entities = [$entity];
|
||||
if ($entity->isA('book')) {
|
||||
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
|
||||
$this->buildJointPermissionsForBooks($books, $this->role->newQuery()->get(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
$entities[] = $entity->book;
|
||||
if ($entity->isA('page') && $entity->chapter_id) $entities[] = $entity->chapter;
|
||||
if ($entity->isA('chapter')) {
|
||||
foreach ($entity->pages as $page) {
|
||||
$entities[] = $page;
|
||||
}
|
||||
}
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->buildJointPermissionsForEntities(collect($entities));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a collection of entities.
|
||||
* @param Collection $entities
|
||||
*/
|
||||
public function buildJointPermissionsForEntities(Collection $entities)
|
||||
{
|
||||
$roles = $this->role->newQuery()->get();
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the entity jointPermissions for a particular role.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function buildJointPermissionForRole(Role $role)
|
||||
{
|
||||
$roles = [$role];
|
||||
$this->deleteManyJointPermissionsForRoles($roles);
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions attached to a particular role.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function deleteJointPermissionsForRole(Role $role)
|
||||
{
|
||||
$this->deleteManyJointPermissionsForRoles([$role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
* @param Role[] $roles
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForRoles($roles)
|
||||
{
|
||||
$roleIds = array_map(function($role) {
|
||||
return $role->id;
|
||||
}, $roles);
|
||||
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function deleteJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
$this->deleteManyJointPermissionsForEntities([$entity]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForEntities($entities)
|
||||
{
|
||||
if (count($entities) === 0) return;
|
||||
|
||||
$this->db->transaction(function() use ($entities) {
|
||||
|
||||
foreach (array_chunk($entities, 1000) as $entityChunk) {
|
||||
$query = $this->db->table('joint_permissions');
|
||||
foreach ($entityChunk as $entity) {
|
||||
$query->orWhere(function(QueryBuilder $query) use ($entity) {
|
||||
$query->where('entity_id', '=', $entity->id)
|
||||
->where('entity_type', '=', $entity->getMorphClass());
|
||||
});
|
||||
}
|
||||
$query->delete();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & Save entity jointPermissions for many entities and jointPermissions.
|
||||
* @param Collection $entities
|
||||
* @param array $roles
|
||||
*/
|
||||
protected function createManyJointPermissions($entities, $roles)
|
||||
{
|
||||
$this->readyEntityCache();
|
||||
$jointPermissions = [];
|
||||
|
||||
// Fetch Entity Permissions and create a mapping of entity restricted statuses
|
||||
$entityRestrictedMap = [];
|
||||
$permissionFetch = $this->entityPermission->newQuery();
|
||||
foreach ($entities as $entity) {
|
||||
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
|
||||
$permissionFetch->orWhere(function($query) use ($entity) {
|
||||
$query->where('restrictable_id', '=', $entity->id)->where('restrictable_type', '=', $entity->getMorphClass());
|
||||
});
|
||||
}
|
||||
$permissions = $permissionFetch->get();
|
||||
|
||||
// Create a mapping of explicit entity permissions
|
||||
$permissionMap = [];
|
||||
foreach ($permissions as $permission) {
|
||||
$key = $permission->restrictable_type . ':' . $permission->restrictable_id . ':' . $permission->role_id . ':' . $permission->action;
|
||||
$isRestricted = $entityRestrictedMap[$permission->restrictable_type . ':' . $permission->restrictable_id];
|
||||
$permissionMap[$key] = $isRestricted;
|
||||
}
|
||||
|
||||
// Create a mapping of role permissions
|
||||
$rolePermissionMap = [];
|
||||
foreach ($roles as $role) {
|
||||
foreach ($role->getRelationValue('permissions') as $permission) {
|
||||
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Joint Permission Data
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($roles as $role) {
|
||||
foreach ($this->getActions($entity) as $action) {
|
||||
$jointPermissions[] = $this->createJointPermissionData($entity, $role, $action, $permissionMap, $rolePermissionMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->transaction(function() use ($jointPermissions) {
|
||||
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
|
||||
$this->db->table('joint_permissions')->insert($jointPermissionChunk);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the actions related to an entity.
|
||||
* @param Entity $entity
|
||||
* @return array
|
||||
*/
|
||||
protected function getActions(Entity $entity)
|
||||
{
|
||||
$baseActions = ['view', 'update', 'delete'];
|
||||
if ($entity->isA('chapter') || $entity->isA('book')) $baseActions[] = 'page-create';
|
||||
if ($entity->isA('book')) $baseActions[] = 'chapter-create';
|
||||
return $baseActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create entity permission data for an entity and role
|
||||
* for a particular action.
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param string $action
|
||||
* @param array $permissionMap
|
||||
* @param array $rolePermissionMap
|
||||
* @return array
|
||||
*/
|
||||
protected function createJointPermissionData(Entity $entity, Role $role, $action, $permissionMap, $rolePermissionMap)
|
||||
{
|
||||
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
|
||||
$roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
|
||||
$roleHasPermissionOwn = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-own']);
|
||||
$explodedAction = explode('-', $action);
|
||||
$restrictionAction = end($explodedAction);
|
||||
|
||||
if ($role->system_name === 'admin') {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
|
||||
}
|
||||
|
||||
if ($entity->restricted) {
|
||||
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
|
||||
}
|
||||
|
||||
// For chapters and pages, Check if explicit permissions are set on the Book.
|
||||
$book = $this->getBook($entity->book_id);
|
||||
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $role, $restrictionAction);
|
||||
$hasPermissiveAccessToParents = !$book->restricted;
|
||||
|
||||
// For pages with a chapter, Check if explicit permissions are set on the Chapter
|
||||
if ($entity->isA('page') && $entity->chapter_id !== 0 && $entity->chapter_id !== '0') {
|
||||
$chapter = $this->getChapter($entity->chapter_id);
|
||||
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
|
||||
if ($chapter->restricted) {
|
||||
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $role, $restrictionAction);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action,
|
||||
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
|
||||
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an active restriction in an entity map.
|
||||
* @param $entityMap
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action) {
|
||||
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
|
||||
return isset($entityMap[$key]) ? $entityMap[$key] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of data with the information of an entity jointPermissions.
|
||||
* Used to build data for bulk insertion.
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @param $permissionAll
|
||||
* @param $permissionOwn
|
||||
* @return array
|
||||
*/
|
||||
protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
|
||||
{
|
||||
return [
|
||||
'role_id' => $role->getRawAttribute('id'),
|
||||
'entity_id' => $entity->getRawAttribute('id'),
|
||||
'entity_type' => $entity->getMorphClass(),
|
||||
'action' => $action,
|
||||
'has_permission' => $permissionAll,
|
||||
'has_permission_own' => $permissionOwn,
|
||||
'created_by' => $entity->getRawAttribute('created_by')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entity has a restriction set upon it.
|
||||
* @param Ownable $ownable
|
||||
* @param $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function checkOwnableUserAccess(Ownable $ownable, $permission)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return true;
|
||||
}
|
||||
|
||||
$explodedPermission = explode('-', $permission);
|
||||
|
||||
$baseQuery = $ownable->where('id', '=', $ownable->id);
|
||||
$action = end($explodedPermission);
|
||||
$this->currentAction = $action;
|
||||
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment'];
|
||||
|
||||
// Handle non entity specific jointPermissions
|
||||
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
||||
$allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
|
||||
$ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
|
||||
$this->currentAction = 'view';
|
||||
$isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
|
||||
return ($allPermission || ($isOwner && $ownPermission));
|
||||
}
|
||||
|
||||
// Handle abnormal create jointPermissions
|
||||
if ($action === 'create') {
|
||||
$this->currentAction = $permission;
|
||||
}
|
||||
|
||||
$q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
|
||||
$this->clean();
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has restrictions set on itself or its
|
||||
* parent tree.
|
||||
* @param Entity $entity
|
||||
* @param $action
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function checkIfRestrictionsSet(Entity $entity, $action)
|
||||
{
|
||||
$this->currentAction = $action;
|
||||
if ($entity->isA('page')) {
|
||||
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
return $entity->restricted || $entity->book->restricted;
|
||||
} elseif ($entity->isA('book')) {
|
||||
return $entity->restricted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The general query filter to remove all entities
|
||||
* that the current user does not have access to.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
protected function entityRestrictionQuery($query)
|
||||
{
|
||||
$q = $query->where(function ($parentQuery) {
|
||||
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
|
||||
$permissionQuery->whereIn('role_id', $this->getRoles())
|
||||
->where('action', '=', $this->currentAction)
|
||||
->where(function ($query) {
|
||||
$query->where('has_permission', '=', true)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
$this->clean();
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the children of a book in an efficient single query, Filtered by the permission system.
|
||||
* @param integer $book_id
|
||||
* @param bool $filterDrafts
|
||||
* @param bool $fetchPageContent
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
|
||||
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
|
||||
$query->where('draft', '=', 0);
|
||||
if (!$filterDrafts) {
|
||||
$query->orWhere(function($query) {
|
||||
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
$chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id);
|
||||
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
|
||||
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
|
||||
|
||||
if (!$this->isAdmin()) {
|
||||
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
|
||||
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
|
||||
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
|
||||
->where(function($query) {
|
||||
$query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
|
||||
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
|
||||
}
|
||||
|
||||
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions for a generic entity
|
||||
* @param string $entityType
|
||||
* @param Builder|Entity $query
|
||||
* @param string $action
|
||||
* @return Builder
|
||||
*/
|
||||
public function enforceEntityRestrictions($entityType, $query, $action = 'view')
|
||||
{
|
||||
if (strtolower($entityType) === 'page') {
|
||||
// Prevent drafts being visible to others.
|
||||
$query = $query->where(function ($query) {
|
||||
$query->where('draft', '=', false);
|
||||
if ($this->currentUser()) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = $action;
|
||||
return $this->entityRestrictionQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set as a polymorphic relation.
|
||||
* @param $query
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
* @param string $entityTypeColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
|
||||
->where('action', '=', $this->currentAction)
|
||||
->whereIn('role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('has_permission', '=', true)->orWhere(function ($query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
$this->clean();
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters pages that are a direct relation to another item.
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $entityIdColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails) {
|
||||
$query->where(function ($query) use (&$tableDetails) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where('entity_type', '=', 'Bookstack\\Page')
|
||||
->where('action', '=', $this->currentAction)
|
||||
->whereIn('role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('has_permission', '=', true)->orWhere(function ($query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||
});
|
||||
$this->clean();
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user is an admin.
|
||||
* @return bool
|
||||
*/
|
||||
private function isAdmin()
|
||||
{
|
||||
if ($this->isAdminUser === null) {
|
||||
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
|
||||
}
|
||||
|
||||
return $this->isAdminUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user
|
||||
* @return User
|
||||
*/
|
||||
private function currentUser()
|
||||
{
|
||||
if ($this->currentUserModel === false) {
|
||||
$this->currentUserModel = user();
|
||||
}
|
||||
|
||||
return $this->currentUserModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the cached user elements.
|
||||
*/
|
||||
private function clean()
|
||||
{
|
||||
$this->currentUserModel = false;
|
||||
$this->userRoles = false;
|
||||
$this->isAdminUser = null;
|
||||
}
|
||||
|
||||
}
|
||||
482
app/Services/SearchService.php
Normal file
482
app/Services/SearchService.php
Normal file
@@ -0,0 +1,482 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Page;
|
||||
use BookStack\SearchTerm;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SearchService
|
||||
{
|
||||
protected $searchTerm;
|
||||
protected $book;
|
||||
protected $chapter;
|
||||
protected $page;
|
||||
protected $db;
|
||||
protected $permissionService;
|
||||
protected $entities;
|
||||
|
||||
/**
|
||||
* Acceptable operators to be used in a query
|
||||
* @var array
|
||||
*/
|
||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||
|
||||
/**
|
||||
* SearchService constructor.
|
||||
* @param SearchTerm $searchTerm
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param Connection $db
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService)
|
||||
{
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
$this->db = $db;
|
||||
$this->entities = [
|
||||
'page' => $this->page,
|
||||
'chapter' => $this->chapter,
|
||||
'book' => $this->book
|
||||
];
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the database connection
|
||||
* @param Connection $connection
|
||||
*/
|
||||
public function setConnection(Connection $connection)
|
||||
{
|
||||
$this->db = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search all entities in the system.
|
||||
* @param string $searchString
|
||||
* @param string $entityType
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @return array[int, Collection];
|
||||
*/
|
||||
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$entityTypes = array_keys($this->entities);
|
||||
$entityTypesToSearch = $entityTypes;
|
||||
$results = collect();
|
||||
|
||||
if ($entityType !== 'all') {
|
||||
$entityTypesToSearch = $entityType;
|
||||
} else if (isset($terms['filters']['type'])) {
|
||||
$entityTypesToSearch = explode('|', $terms['filters']['type']);
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
|
||||
foreach ($entityTypesToSearch as $entityType) {
|
||||
if (!in_array($entityType, $entityTypes)) continue;
|
||||
$search = $this->searchEntityTable($terms, $entityType, $page, $count);
|
||||
$total += $this->searchEntityTable($terms, $entityType, $page, $count, true);
|
||||
$results = $results->merge($search);
|
||||
}
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'count' => count($results),
|
||||
'results' => $results->sortByDesc('score')
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search a book for entities
|
||||
* @param integer $bookId
|
||||
* @param string $searchString
|
||||
* @return Collection
|
||||
*/
|
||||
public function searchBook($bookId, $searchString)
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$entityTypes = ['page', 'chapter'];
|
||||
$entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes;
|
||||
|
||||
$results = collect();
|
||||
foreach ($entityTypesToSearch as $entityType) {
|
||||
if (!in_array($entityType, $entityTypes)) continue;
|
||||
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
|
||||
$results = $results->merge($search);
|
||||
}
|
||||
return $results->sortByDesc('score')->take(20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a book for entities
|
||||
* @param integer $chapterId
|
||||
* @param string $searchString
|
||||
* @return Collection
|
||||
*/
|
||||
public function searchChapter($chapterId, $searchString)
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
|
||||
return $pages->sortByDesc('score');
|
||||
}
|
||||
|
||||
/**
|
||||
* Search across a particular entity type.
|
||||
* @param array $terms
|
||||
* @param string $entityType
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param bool $getCount Return the total count of the search
|
||||
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
|
||||
*/
|
||||
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
|
||||
{
|
||||
$query = $this->buildEntitySearchQuery($terms, $entityType);
|
||||
if ($getCount) return $query->count();
|
||||
|
||||
$query = $query->skip(($page-1) * $count)->take($count);
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a search query for an entity
|
||||
* @param array $terms
|
||||
* @param string $entityType
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function buildEntitySearchQuery($terms, $entityType = 'page')
|
||||
{
|
||||
$entity = $this->getEntity($entityType);
|
||||
$entitySelect = $entity->newQuery();
|
||||
|
||||
// Handle normal search terms
|
||||
if (count($terms['search']) > 0) {
|
||||
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
|
||||
$subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType));
|
||||
$subQuery->where(function(Builder $query) use ($terms) {
|
||||
foreach ($terms['search'] as $inputTerm) {
|
||||
$query->orWhere('term', 'like', $inputTerm .'%');
|
||||
}
|
||||
})->groupBy('entity_type', 'entity_id');
|
||||
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) {
|
||||
$join->on('id', '=', 'entity_id');
|
||||
})->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
|
||||
$entitySelect->mergeBindings($subQuery);
|
||||
}
|
||||
|
||||
// Handle exact term matching
|
||||
if (count($terms['exact']) > 0) {
|
||||
$entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
|
||||
foreach ($terms['exact'] as $inputTerm) {
|
||||
$query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) {
|
||||
$query->where('name', 'like', '%'.$inputTerm .'%')
|
||||
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle tag searches
|
||||
foreach ($terms['tags'] as $inputTerm) {
|
||||
$this->applyTagSearch($entitySelect, $inputTerm);
|
||||
}
|
||||
|
||||
// Handle filters
|
||||
foreach ($terms['filters'] as $filterTerm => $filterValue) {
|
||||
$functionName = camel_case('filter_' . $filterTerm);
|
||||
if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue);
|
||||
}
|
||||
|
||||
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse a search string into components.
|
||||
* @param $searchString
|
||||
* @return array
|
||||
*/
|
||||
protected function parseSearchString($searchString)
|
||||
{
|
||||
$terms = [
|
||||
'search' => [],
|
||||
'exact' => [],
|
||||
'tags' => [],
|
||||
'filters' => []
|
||||
];
|
||||
|
||||
$patterns = [
|
||||
'exact' => '/"(.*?)"/',
|
||||
'tags' => '/\[(.*?)\]/',
|
||||
'filters' => '/\{(.*?)\}/'
|
||||
];
|
||||
|
||||
// Parse special terms
|
||||
foreach ($patterns as $termType => $pattern) {
|
||||
$matches = [];
|
||||
preg_match_all($pattern, $searchString, $matches);
|
||||
if (count($matches) > 0) {
|
||||
$terms[$termType] = $matches[1];
|
||||
$searchString = preg_replace($pattern, '', $searchString);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse standard terms
|
||||
foreach (explode(' ', trim($searchString)) as $searchTerm) {
|
||||
if ($searchTerm !== '') $terms['search'][] = $searchTerm;
|
||||
}
|
||||
|
||||
// Split filter values out
|
||||
$splitFilters = [];
|
||||
foreach ($terms['filters'] as $filter) {
|
||||
$explodedFilter = explode(':', $filter, 2);
|
||||
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
|
||||
}
|
||||
$terms['filters'] = $splitFilters;
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available query operators as a regex escaped list.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getRegexEscapedOperators()
|
||||
{
|
||||
$escapedOperators = [];
|
||||
foreach ($this->queryOperators as $operator) {
|
||||
$escapedOperators[] = preg_quote($operator);
|
||||
}
|
||||
return join('|', $escapedOperators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a tag search term onto a entity query.
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $tagTerm
|
||||
* @return mixed
|
||||
*/
|
||||
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) {
|
||||
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
|
||||
$query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
|
||||
$tagName = $tagSplit[1];
|
||||
$tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
|
||||
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
|
||||
$validOperator = in_array($tagOperator, $this->queryOperators);
|
||||
if (!empty($tagOperator) && !empty($tagValue) && $validOperator) {
|
||||
if (!empty($tagName)) $query->where('name', '=', $tagName);
|
||||
if (is_numeric($tagValue) && $tagOperator !== 'like') {
|
||||
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
|
||||
// search the value as a string which prevents being able to do number-based operations
|
||||
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
|
||||
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
|
||||
$query->whereRaw("value ${tagOperator} ${tagValue}");
|
||||
} else {
|
||||
$query->where('value', $tagOperator, $tagValue);
|
||||
}
|
||||
} else {
|
||||
$query->where('name', '=', $tagName);
|
||||
}
|
||||
});
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance via type.
|
||||
* @param $type
|
||||
* @return Entity
|
||||
*/
|
||||
protected function getEntity($type)
|
||||
{
|
||||
return $this->entities[strtolower($type)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Index the given entity.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function indexEntity(Entity $entity)
|
||||
{
|
||||
$this->deleteEntityTerms($entity);
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
|
||||
$terms = array_merge($nameTerms, $bodyTerms);
|
||||
foreach ($terms as $index => $term) {
|
||||
$terms[$index]['entity_type'] = $entity->getMorphClass();
|
||||
$terms[$index]['entity_id'] = $entity->id;
|
||||
}
|
||||
$this->searchTerm->newQuery()->insert($terms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Index multiple Entities at once
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function indexEntities($entities) {
|
||||
$terms = [];
|
||||
foreach ($entities as $entity) {
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
|
||||
foreach (array_merge($nameTerms, $bodyTerms) as $term) {
|
||||
$term['entity_id'] = $entity->id;
|
||||
$term['entity_type'] = $entity->getMorphClass();
|
||||
$terms[] = $term;
|
||||
}
|
||||
}
|
||||
|
||||
$chunkedTerms = array_chunk($terms, 500);
|
||||
foreach ($chunkedTerms as $termChunk) {
|
||||
$this->searchTerm->newQuery()->insert($termChunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete and re-index the terms for all entities in the system.
|
||||
*/
|
||||
public function indexAllEntities()
|
||||
{
|
||||
$this->searchTerm->truncate();
|
||||
|
||||
// Chunk through all books
|
||||
$this->book->chunk(1000, function ($books) {
|
||||
$this->indexEntities($books);
|
||||
});
|
||||
|
||||
// Chunk through all chapters
|
||||
$this->chapter->chunk(1000, function ($chapters) {
|
||||
$this->indexEntities($chapters);
|
||||
});
|
||||
|
||||
// Chunk through all pages
|
||||
$this->page->chunk(1000, function ($pages) {
|
||||
$this->indexEntities($pages);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete related Entity search terms.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function deleteEntityTerms(Entity $entity)
|
||||
{
|
||||
$entity->searchTerms()->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a scored term array from the given text.
|
||||
* @param $text
|
||||
* @param float|int $scoreAdjustment
|
||||
* @return array
|
||||
*/
|
||||
protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
|
||||
{
|
||||
$tokenMap = []; // {TextToken => OccurrenceCount}
|
||||
$splitText = explode(' ', $text);
|
||||
foreach ($splitText as $token) {
|
||||
if ($token === '') continue;
|
||||
if (!isset($tokenMap[$token])) $tokenMap[$token] = 0;
|
||||
$tokenMap[$token]++;
|
||||
}
|
||||
|
||||
$terms = [];
|
||||
foreach ($tokenMap as $token => $count) {
|
||||
$terms[] = [
|
||||
'term' => $token,
|
||||
'score' => $count * $scoreAdjustment
|
||||
];
|
||||
}
|
||||
return $terms;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Custom entity search filters
|
||||
*/
|
||||
|
||||
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
$query->where('updated_at', '>=', $date);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
$query->where('updated_at', '<', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
$query->where('created_at', '>=', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
$query->where('created_at', '<', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') return;
|
||||
if ($input === 'me') $input = user()->id;
|
||||
$query->where('created_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') return;
|
||||
if ($input === 'me') $input = user()->id;
|
||||
$query->where('updated_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where('name', 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);}
|
||||
|
||||
protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where($model->textField, 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where('restricted', '=', true);
|
||||
}
|
||||
|
||||
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereHas('views', function($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereDoesntHave('views', function($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Setting;
|
||||
use BookStack\User;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
|
||||
/**
|
||||
@@ -15,6 +16,7 @@ class SettingService
|
||||
|
||||
protected $setting;
|
||||
protected $cache;
|
||||
protected $localCache = [];
|
||||
|
||||
protected $cachePrefix = 'setting-';
|
||||
|
||||
@@ -38,28 +40,47 @@ class SettingService
|
||||
*/
|
||||
public function get($key, $default = false)
|
||||
{
|
||||
if ($default === false) $default = config('setting-defaults.' . $key, false);
|
||||
if (isset($this->localCache[$key])) return $this->localCache[$key];
|
||||
|
||||
$value = $this->getValueFromStore($key, $default);
|
||||
return $this->formatValue($value, $default);
|
||||
$formatted = $this->formatValue($value, $default);
|
||||
$this->localCache[$key] = $formatted;
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user-specific setting from the database or cache.
|
||||
* @param User $user
|
||||
* @param $key
|
||||
* @param bool $default
|
||||
* @return bool|string
|
||||
*/
|
||||
public function getUser($user, $key, $default = false)
|
||||
{
|
||||
return $this->get($this->userKey($user->id, $key), $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a setting value from the cache or database.
|
||||
* Looks at the system defaults if not cached or in database.
|
||||
* @param $key
|
||||
* @param $default
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getValueFromStore($key, $default)
|
||||
{
|
||||
// Check for an overriding value
|
||||
$overrideValue = $this->getOverrideValue($key);
|
||||
if ($overrideValue !== null) return $overrideValue;
|
||||
|
||||
// Check the cache
|
||||
$cacheKey = $this->cachePrefix . $key;
|
||||
if ($this->cache->has($cacheKey)) {
|
||||
return $this->cache->get($cacheKey);
|
||||
}
|
||||
$cacheVal = $this->cache->get($cacheKey, null);
|
||||
if ($cacheVal !== null) return $cacheVal;
|
||||
|
||||
// Check the database
|
||||
$settingObject = $this->getSettingObjectByKey($key);
|
||||
|
||||
if ($settingObject !== null) {
|
||||
$value = $settingObject->value;
|
||||
$this->cache->forever($cacheKey, $value);
|
||||
@@ -107,6 +128,16 @@ class SettingService
|
||||
return $setting !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user setting is in the database.
|
||||
* @param $key
|
||||
* @return bool
|
||||
*/
|
||||
public function hasUser($key)
|
||||
{
|
||||
return $this->has($this->userKey($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a setting to the database.
|
||||
* @param $key
|
||||
@@ -124,6 +155,28 @@ class SettingService
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a user-specific setting into the database.
|
||||
* @param User $user
|
||||
* @param $key
|
||||
* @param $value
|
||||
* @return bool
|
||||
*/
|
||||
public function putUser($user, $key, $value)
|
||||
{
|
||||
return $this->put($this->userKey($user->id, $key), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a setting key into a user-specific key.
|
||||
* @param $key
|
||||
* @return string
|
||||
*/
|
||||
protected function userKey($userId, $key = '')
|
||||
{
|
||||
return 'user:' . $userId . ':' . $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a setting from the database.
|
||||
* @param $key
|
||||
@@ -139,6 +192,16 @@ class SettingService
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete settings for a given user id.
|
||||
* @param $userId
|
||||
* @return mixed
|
||||
*/
|
||||
public function deleteUserSettings($userId)
|
||||
{
|
||||
return $this->setting->where('setting_key', 'like', $this->userKey($userId) . '%')->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a setting model from the database for the given key.
|
||||
* @param $key
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Http\Controllers\Auth\AuthController;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\SocialAccount;
|
||||
use BookStack\User;
|
||||
|
||||
class SocialAuthService
|
||||
{
|
||||
@@ -17,7 +14,7 @@ class SocialAuthService
|
||||
protected $socialite;
|
||||
protected $socialAccount;
|
||||
|
||||
protected $validSocialDrivers = ['google', 'github'];
|
||||
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter'];
|
||||
|
||||
/**
|
||||
* SocialAuthService constructor.
|
||||
@@ -73,12 +70,12 @@ class SocialAuthService
|
||||
|
||||
// Check social account has not already been used
|
||||
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
|
||||
throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login');
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
|
||||
}
|
||||
|
||||
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
|
||||
$email = $socialUser->getEmail();
|
||||
throw new UserRegistrationException('The email ' . $email . ' is already in use. If you already have an account you can connect your ' . $socialDriver . ' account from your profile settings.', '/login');
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver, 'email' => $email]), '/login');
|
||||
}
|
||||
|
||||
return $socialUser;
|
||||
@@ -101,9 +98,8 @@ class SocialAuthService
|
||||
|
||||
// Get any attached social accounts or users
|
||||
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
|
||||
$user = $this->userRepo->getByEmail($socialUser->getEmail());
|
||||
$isLoggedIn = auth()->check();
|
||||
$currentUser = auth()->user();
|
||||
$currentUser = user();
|
||||
|
||||
// When a user is not logged in and a matching SocialAccount exists,
|
||||
// Simply log the user into the application.
|
||||
@@ -116,28 +112,28 @@ class SocialAuthService
|
||||
if ($isLoggedIn && $socialAccount === null) {
|
||||
$this->fillSocialAccount($socialDriver, $socialUser);
|
||||
$currentUser->socialAccounts()->save($this->socialAccount);
|
||||
\Session::flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.');
|
||||
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => title_case($socialDriver)]));
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
// When a user is logged in and the social account exists and is already linked to the current user.
|
||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
|
||||
\Session::flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
|
||||
session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => title_case($socialDriver)]));
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
// When a user is logged in, A social account exists but the users do not match.
|
||||
// Change the user that the social account is assigned to.
|
||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
|
||||
\Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
|
||||
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => title_case($socialDriver)]));
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
// Otherwise let the user know this social account is not used by anyone.
|
||||
$message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings';
|
||||
if (\Setting::get('registration-enabled')) {
|
||||
$message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option';
|
||||
$message = trans('errors.social_account_not_used', ['socialAccount' => title_case($socialDriver)]);
|
||||
if (setting('registration-enabled')) {
|
||||
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
|
||||
}
|
||||
|
||||
throw new SocialSignInException($message . '.', '/login');
|
||||
}
|
||||
|
||||
@@ -159,8 +155,8 @@ class SocialAuthService
|
||||
{
|
||||
$driver = trim(strtolower($socialDriver));
|
||||
|
||||
if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
|
||||
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured;
|
||||
if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found'));
|
||||
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
|
||||
|
||||
return $driver;
|
||||
}
|
||||
@@ -185,14 +181,24 @@ class SocialAuthService
|
||||
public function getActiveDrivers()
|
||||
{
|
||||
$activeDrivers = [];
|
||||
foreach ($this->validSocialDrivers as $driverName) {
|
||||
if ($this->checkDriverConfigured($driverName)) {
|
||||
$activeDrivers[$driverName] = true;
|
||||
foreach ($this->validSocialDrivers as $driverKey) {
|
||||
if ($this->checkDriverConfigured($driverKey)) {
|
||||
$activeDrivers[$driverKey] = $this->getDriverName($driverKey);
|
||||
}
|
||||
}
|
||||
return $activeDrivers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the presentational name for a driver.
|
||||
* @param $driver
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDriverName($driver)
|
||||
{
|
||||
return config('services.' . strtolower($driver) . '.name');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $socialDriver
|
||||
* @param \Laravel\Socialite\Contracts\User $socialUser
|
||||
@@ -215,10 +221,9 @@ class SocialAuthService
|
||||
*/
|
||||
public function detachSocialAccount($socialDriver)
|
||||
{
|
||||
session();
|
||||
auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
||||
\Session::flash('success', $socialDriver . ' account successfully detached');
|
||||
return redirect(auth()->user()->getEditUrl());
|
||||
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
||||
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
|
||||
return redirect(user()->getEditUrl());
|
||||
}
|
||||
|
||||
}
|
||||
64
app/Services/UploadService.php
Normal file
64
app/Services/UploadService.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
|
||||
class UploadService
|
||||
{
|
||||
|
||||
/**
|
||||
* @var FileSystem
|
||||
*/
|
||||
protected $fileSystem;
|
||||
|
||||
/**
|
||||
* @var FileSystemInstance
|
||||
*/
|
||||
protected $storageInstance;
|
||||
|
||||
|
||||
/**
|
||||
* FileService constructor.
|
||||
* @param $fileSystem
|
||||
*/
|
||||
public function __construct(FileSystem $fileSystem)
|
||||
{
|
||||
$this->fileSystem = $fileSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage that will be used for storing images.
|
||||
* @return FileSystemInstance
|
||||
*/
|
||||
protected function getStorage()
|
||||
{
|
||||
if ($this->storageInstance !== null) return $this->storageInstance;
|
||||
|
||||
$storageType = config('filesystems.default');
|
||||
$this->storageInstance = $this->fileSystem->disk($storageType);
|
||||
|
||||
return $this->storageInstance;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether or not a folder is empty.
|
||||
* @param $path
|
||||
* @return bool
|
||||
*/
|
||||
protected function isFolderEmpty($path)
|
||||
{
|
||||
$files = $this->getStorage()->files($path);
|
||||
$folders = $this->getStorage()->directories($path);
|
||||
return (count($files) === 0 && count($folders) === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if using a local filesystem.
|
||||
* @return bool
|
||||
*/
|
||||
protected function isLocal()
|
||||
{
|
||||
return strtolower(config('filesystems.default')) === 'local';
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,22 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Entity;
|
||||
use BookStack\View;
|
||||
|
||||
class ViewService
|
||||
{
|
||||
|
||||
protected $view;
|
||||
protected $user;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* ViewService constructor.
|
||||
* @param $view
|
||||
* @param View $view
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(View $view)
|
||||
public function __construct(View $view, PermissionService $permissionService)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->user = auth()->user();
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,8 +26,9 @@ class ViewService
|
||||
*/
|
||||
public function add(Entity $entity)
|
||||
{
|
||||
if($this->user === null) return 0;
|
||||
$view = $entity->views()->where('user_id', '=', $this->user->id)->first();
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) return 0;
|
||||
$view = $entity->views()->where('user_id', '=', $user->id)->first();
|
||||
// Add view if model exists
|
||||
if ($view) {
|
||||
$view->increment('views');
|
||||
@@ -37,59 +37,59 @@ class ViewService
|
||||
|
||||
// Otherwise create new view count
|
||||
$entity->views()->save($this->view->create([
|
||||
'user_id' => $this->user->id,
|
||||
'user_id' => $user->id,
|
||||
'views' => 1
|
||||
]));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the entities with the most views.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|false $filterModel
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|false|array $filterModel
|
||||
*/
|
||||
public function getPopular($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->view->select('id', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
|
||||
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
|
||||
if($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
|
||||
if ($filterModel && is_array($filterModel)) {
|
||||
$query->whereIn('viewable_type', $filterModel);
|
||||
} else if ($filterModel) {
|
||||
$query->where('viewable_type', '=', get_class($filterModel));
|
||||
};
|
||||
|
||||
$views = $query->with('viewable')->skip($skipCount)->take($count)->get();
|
||||
$viewedEntities = $views->map(function ($item) {
|
||||
return $item->viewable()->getResults();
|
||||
});
|
||||
return $viewedEntities;
|
||||
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all recently viewed entities for the current user.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param Entity|bool $filterModel
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
if($this->user === null) return collect();
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->view->where('user_id', '=', auth()->user()->id);
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) return collect();
|
||||
|
||||
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
|
||||
$query = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
||||
|
||||
$views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get();
|
||||
$viewedEntities = $views->map(function ($item) {
|
||||
return $item->viewable()->getResults();
|
||||
});
|
||||
return $viewedEntities;
|
||||
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||
$query = $query->where('user_id', '=', $user->id);
|
||||
|
||||
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get()->pluck('viewable');
|
||||
return $viewables;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset all view counts by deleting all views.
|
||||
*/
|
||||
@@ -98,5 +98,4 @@ class ViewService
|
||||
$this->view->truncate();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<?php
|
||||
<?php namespace BookStack;
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SocialAccount extends Model
|
||||
{
|
||||
@@ -11,6 +8,6 @@ class SocialAccount extends Model
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('BookStack\User');
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
|
||||
19
app/Tag.php
Normal file
19
app/Tag.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
/**
|
||||
* Class Attribute
|
||||
* @package BookStack
|
||||
*/
|
||||
class Tag extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'value', 'order'];
|
||||
|
||||
/**
|
||||
* Get the entity that this tag belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
}
|
||||
137
app/User.php
137
app/User.php
@@ -1,34 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
<?php namespace BookStack;
|
||||
|
||||
use BookStack\Notifications\ResetPassword;
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
|
||||
{
|
||||
use Authenticatable, CanResetPassword;
|
||||
use Authenticatable, CanResetPassword, Notifiable;
|
||||
|
||||
/**
|
||||
* The database table used by the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'users';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['name', 'email', 'image_id'];
|
||||
|
||||
/**
|
||||
* The attributes excluded from the model's JSON form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = ['password', 'remember_token'];
|
||||
@@ -40,43 +37,67 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
protected $permissions;
|
||||
|
||||
/**
|
||||
* Returns a default guest user.
|
||||
* Returns the default public user.
|
||||
* @return User
|
||||
*/
|
||||
public static function getDefault()
|
||||
{
|
||||
return new static([
|
||||
'email' => 'guest',
|
||||
'name' => 'Guest'
|
||||
]);
|
||||
return static::where('system_name', '=', 'public')->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions and roles
|
||||
* Check if the user is the default public user.
|
||||
* @return bool
|
||||
*/
|
||||
public function isDefault()
|
||||
{
|
||||
return $this->system_name === 'public';
|
||||
}
|
||||
|
||||
/**
|
||||
* The roles that belong to the user.
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany('BookStack\Role');
|
||||
}
|
||||
|
||||
public function getRoleAttribute()
|
||||
{
|
||||
return $this->roles()->with('permissions')->first();
|
||||
if ($this->id === 0) return ;
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the user's permissions from their role.
|
||||
* Check if the user has a role.
|
||||
* @param $role
|
||||
* @return mixed
|
||||
*/
|
||||
private function loadPermissions()
|
||||
public function hasRole($role)
|
||||
{
|
||||
if (isset($this->permissions)) return;
|
||||
return $this->roles->pluck('name')->contains($role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has a role.
|
||||
* @param $role
|
||||
* @return mixed
|
||||
*/
|
||||
public function hasSystemRole($role)
|
||||
{
|
||||
return $this->roles->pluck('system_name')->contains('admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions belonging to a the current user.
|
||||
* @param bool $cache
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
|
||||
*/
|
||||
public function permissions($cache = true)
|
||||
{
|
||||
if(isset($this->permissions) && $cache) return $this->permissions;
|
||||
$this->load('roles.permissions');
|
||||
$permissions = $this->roles[0]->permissions;
|
||||
$permissionsArray = $permissions->pluck('name')->all();
|
||||
$this->permissions = $permissionsArray;
|
||||
$permissions = $this->roles->map(function($role) {
|
||||
return $role->permissions;
|
||||
})->flatten()->unique();
|
||||
$this->permissions = $permissions;
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,11 +107,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function can($permissionName)
|
||||
{
|
||||
if ($this->email == 'guest') {
|
||||
return false;
|
||||
}
|
||||
$this->loadPermissions();
|
||||
return array_search($permissionName, $this->permissions) !== false;
|
||||
if ($this->email === 'guest') return false;
|
||||
return $this->permissions()->pluck('name')->contains($permissionName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,17 +126,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function attachRoleId($id)
|
||||
{
|
||||
$this->roles()->sync([$id]);
|
||||
$this->roles()->attach($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the social account associated with this user.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function socialAccounts()
|
||||
{
|
||||
return $this->hasMany('BookStack\SocialAccount');
|
||||
return $this->hasMany(SocialAccount::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,15 +155,21 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Returns the user's avatar,
|
||||
* Uses Gravatar as the avatar service.
|
||||
*
|
||||
* @param int $size
|
||||
* @return string
|
||||
*/
|
||||
public function getAvatar($size = 50)
|
||||
{
|
||||
if ($this->image_id === 0 || $this->image_id === '0' || $this->image_id === null) return '/user_avatar.png';
|
||||
return $this->avatar->getThumb($size, $size, false);
|
||||
$default = baseUrl('/user_avatar.png');
|
||||
$imageId = $this->image_id;
|
||||
if ($imageId === 0 || $imageId === '0' || $imageId === null) return $default;
|
||||
|
||||
try {
|
||||
$avatar = $this->avatar ? baseUrl($this->avatar->getThumb($size, $size, false)) : $default;
|
||||
} catch (\Exception $err) {
|
||||
$avatar = $default;
|
||||
}
|
||||
return $avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,7 +178,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function avatar()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Image', 'image_id');
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,6 +187,40 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return '/users/' . $this->id;
|
||||
return baseUrl('/settings/users/' . $this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url that links to this user's profile.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getProfileUrl()
|
||||
{
|
||||
return baseUrl('/user/' . $this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a shortened version of the user's name.
|
||||
* @param int $chars
|
||||
* @return string
|
||||
*/
|
||||
public function getShortName($chars = 8)
|
||||
{
|
||||
if (strlen($this->name) <= $chars) return $this->name;
|
||||
|
||||
$splitName = explode(' ', $this->name);
|
||||
if (strlen($splitName[0]) <= $chars) return $splitName[0];
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
$this->notify(new ResetPassword($token));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
<?php namespace BookStack;
|
||||
|
||||
class View extends Model
|
||||
{
|
||||
|
||||
187
app/helpers.php
187
app/helpers.php
@@ -1,30 +1,169 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('versioned_asset')) {
|
||||
/**
|
||||
* Get the path to a versioned file.
|
||||
*
|
||||
* @param string $file
|
||||
* @return string
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
function versioned_asset($file)
|
||||
{
|
||||
static $manifest = null;
|
||||
use BookStack\Ownable;
|
||||
|
||||
if (is_null($manifest)) {
|
||||
$manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true);
|
||||
}
|
||||
/**
|
||||
* Get the path to a versioned file.
|
||||
*
|
||||
* @param string $file
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
function versioned_asset($file = '')
|
||||
{
|
||||
static $version = null;
|
||||
|
||||
if (isset($manifest[$file])) {
|
||||
return '/' . $manifest[$file];
|
||||
}
|
||||
|
||||
if (file_exists(public_path($file))) {
|
||||
return '/' . $file;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
|
||||
if (is_null($version)) {
|
||||
$versionFile = base_path('version');
|
||||
$version = trim(file_get_contents($versionFile));
|
||||
}
|
||||
|
||||
$additional = '';
|
||||
if (config('app.env') === 'development') {
|
||||
$additional = sha1_file(public_path($file));
|
||||
}
|
||||
|
||||
$path = $file . '?version=' . urlencode($version) . $additional;
|
||||
return baseUrl($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the current User.
|
||||
* Defaults to public 'Guest' user if not logged in.
|
||||
* @return \BookStack\User
|
||||
*/
|
||||
function user()
|
||||
{
|
||||
return auth()->user() ?: \BookStack\User::getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user is a signed in user.
|
||||
* @return bool
|
||||
*/
|
||||
function signedInUser()
|
||||
{
|
||||
return auth()->user() && !auth()->user()->isDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has a permission.
|
||||
* If an ownable element is passed in the jointPermissions are checked against
|
||||
* that particular item.
|
||||
* @param $permission
|
||||
* @param Ownable $ownable
|
||||
* @return mixed
|
||||
*/
|
||||
function userCan($permission, Ownable $ownable = null)
|
||||
{
|
||||
if ($ownable === null) {
|
||||
return user() && user()->can($permission);
|
||||
}
|
||||
|
||||
// Check permission on ownable item
|
||||
$permissionService = app(\BookStack\Services\PermissionService::class);
|
||||
return $permissionService->checkOwnableUserAccess($ownable, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to access system settings.
|
||||
* @param $key
|
||||
* @param bool $default
|
||||
* @return bool|string|\BookStack\Services\SettingService
|
||||
*/
|
||||
function setting($key = null, $default = false)
|
||||
{
|
||||
$settingService = resolve(\BookStack\Services\SettingService::class);
|
||||
if (is_null($key)) return $settingService;
|
||||
return $settingService->get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create url's relative to the applications root path.
|
||||
* @param string $path
|
||||
* @param bool $forceAppDomain
|
||||
* @return string
|
||||
*/
|
||||
function baseUrl($path, $forceAppDomain = false)
|
||||
{
|
||||
$isFullUrl = strpos($path, 'http') === 0;
|
||||
if ($isFullUrl && !$forceAppDomain) return $path;
|
||||
$path = trim($path, '/');
|
||||
|
||||
// Remove non-specified domain if forced and we have a domain
|
||||
if ($isFullUrl && $forceAppDomain) {
|
||||
$explodedPath = explode('/', $path);
|
||||
$path = implode('/', array_splice($explodedPath, 3));
|
||||
}
|
||||
|
||||
// Return normal url path if not specified in config
|
||||
if (config('app.url') === '') {
|
||||
return url($path);
|
||||
}
|
||||
|
||||
return rtrim(config('app.url'), '/') . '/' . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the redirector.
|
||||
* Overrides the default laravel redirect helper.
|
||||
* Ensures it redirects even when the app is in a subdirectory.
|
||||
*
|
||||
* @param string|null $to
|
||||
* @param int $status
|
||||
* @param array $headers
|
||||
* @param bool $secure
|
||||
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
function redirect($to = null, $status = 302, $headers = [], $secure = null)
|
||||
{
|
||||
if (is_null($to)) {
|
||||
return app('redirect');
|
||||
}
|
||||
|
||||
$to = baseUrl($to);
|
||||
|
||||
return app('redirect')->to($to, $status, $headers, $secure);
|
||||
}
|
||||
|
||||
function icon($name, $attrs = []) {
|
||||
$iconPath = resource_path('assets/icons/' . $name . '.svg');
|
||||
$attrString = ' ';
|
||||
foreach ($attrs as $attrName => $attr) {
|
||||
$attrString .= $attrName . '="' . $attr . '" ';
|
||||
}
|
||||
$fileContents = file_get_contents($iconPath);
|
||||
return str_replace('<svg', '<svg' . $attrString, $fileContents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a url with multiple parameters for sorting purposes.
|
||||
* Works out the logic to set the correct sorting direction
|
||||
* Discards empty parameters and allows overriding.
|
||||
* @param $path
|
||||
* @param array $data
|
||||
* @param array $overrideData
|
||||
* @return string
|
||||
*/
|
||||
function sortUrl($path, $data, $overrideData = [])
|
||||
{
|
||||
$queryStringSections = [];
|
||||
$queryData = array_merge($data, $overrideData);
|
||||
|
||||
// Change sorting direction is already sorted on current attribute
|
||||
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
|
||||
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
|
||||
} else {
|
||||
$queryData['order'] = 'asc';
|
||||
}
|
||||
|
||||
foreach ($queryData as $name => $value) {
|
||||
$trimmedVal = trim($value);
|
||||
if ($trimmedVal === '') continue;
|
||||
$queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
|
||||
}
|
||||
|
||||
if (count($queryStringSections) === 0) return $path;
|
||||
|
||||
return baseUrl($path . '?' . implode('&', $queryStringSections));
|
||||
}
|
||||
@@ -14,6 +14,7 @@ define('LARAVEL_START', microtime(true));
|
||||
|
|
||||
*/
|
||||
|
||||
require __DIR__.'/../app/helpers.php';
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
/*
|
||||
|
||||
@@ -5,22 +5,27 @@
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=5.5.9",
|
||||
"laravel/framework": "5.2.*",
|
||||
"php": ">=5.6.4",
|
||||
"laravel/framework": "5.4.*",
|
||||
"ext-tidy": "*",
|
||||
"intervention/image": "^2.3",
|
||||
"laravel/socialite": "^2.0",
|
||||
"barryvdh/laravel-ide-helper": "^2.1",
|
||||
"barryvdh/laravel-debugbar": "^2.0",
|
||||
"laravel/socialite": "^3.0",
|
||||
"barryvdh/laravel-ide-helper": "^2.2.3",
|
||||
"barryvdh/laravel-debugbar": "^2.3.2",
|
||||
"league/flysystem-aws-s3-v3": "^1.0",
|
||||
"barryvdh/laravel-dompdf": "0.6.*"
|
||||
"barryvdh/laravel-dompdf": "^0.8",
|
||||
"predis/predis": "^1.1",
|
||||
"gathercontent/htmldiff": "^0.2.1",
|
||||
"barryvdh/laravel-snappy": "^0.3.1",
|
||||
"laravel/browser-kit-testing": "^1.0",
|
||||
"socialiteproviders/slack": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fzaninotto/faker": "~1.4",
|
||||
"mockery/mockery": "0.9.*",
|
||||
"phpunit/phpunit": "~4.0",
|
||||
"phpspec/phpspec": "~2.1",
|
||||
"symfony/dom-crawler": "~3.0",
|
||||
"symfony/css-selector": "~3.0"
|
||||
"phpunit/phpunit": "~5.0",
|
||||
"symfony/css-selector": "3.1.*",
|
||||
"symfony/dom-crawler": "3.1.*"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
@@ -28,32 +33,41 @@
|
||||
],
|
||||
"psr-4": {
|
||||
"BookStack\\": "app/"
|
||||
},
|
||||
"files": [
|
||||
"app/helpers.php"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"classmap": [
|
||||
"tests/TestCase.php"
|
||||
]
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-install-cmd": [
|
||||
"php artisan clear-compiled",
|
||||
"php artisan optimize"
|
||||
],
|
||||
"pre-update-cmd": [
|
||||
"php artisan clear-compiled"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"php artisan optimize"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"php -r \"copy('.env.example', '.env');\""
|
||||
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"php artisan key:generate"
|
||||
],
|
||||
"pre-update-cmd": [
|
||||
"php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
|
||||
"php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
|
||||
],
|
||||
"pre-install-cmd": [
|
||||
"php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
|
||||
"php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
|
||||
],
|
||||
"post-install-cmd": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postInstall",
|
||||
"php artisan optimize",
|
||||
"php artisan cache:clear",
|
||||
"php artisan view:clear"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
|
||||
"php artisan optimize"
|
||||
],
|
||||
"refresh-test-database": [
|
||||
"php artisan migrate:refresh --database=mysql_testing",
|
||||
"php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
|
||||
2540
composer.lock
generated
2540
composer.lock
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user