mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-05 08:39:55 +03:00
Compare commits
278 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7792cb3915 | ||
|
|
be26253a18 | ||
|
|
3d5899d28c | ||
|
|
917d7428d6 | ||
|
|
bcc01bd8ff | ||
|
|
2c34a99248 | ||
|
|
789d17ab3f | ||
|
|
58117bcf2d | ||
|
|
b5caaa73b7 | ||
|
|
7997300f96 | ||
|
|
888f435651 | ||
|
|
1bdd1f8189 | ||
|
|
fa62c79b17 | ||
|
|
a8471b2c66 | ||
|
|
0627efe5e9 | ||
|
|
af7d62799c | ||
|
|
bb00c331e4 | ||
|
|
807f92b693 | ||
|
|
c5d31ea7b2 | ||
|
|
ef1bde8bb1 | ||
|
|
8897945609 | ||
|
|
9382d647d7 | ||
|
|
0d17d18d07 | ||
|
|
24eef03fb9 | ||
|
|
e51352e1a4 | ||
|
|
2dfb1ae3ee | ||
|
|
39928e1c63 | ||
|
|
40ca50e44f | ||
|
|
fc7b8c49fb | ||
|
|
d7d8fa1e5b | ||
|
|
18562f1e10 | ||
|
|
fdabafffda | ||
|
|
e2df15fe20 | ||
|
|
54bac17ef0 | ||
|
|
7634ac4e12 | ||
|
|
c4f5ab12cf | ||
|
|
57a063cdfb | ||
|
|
1fa90e4f12 | ||
|
|
d62cdd58d3 | ||
|
|
ed6ec341df | ||
|
|
0cfff6ab6f | ||
|
|
7ca66c5d5e | ||
|
|
9cbea1eb08 | ||
|
|
1a2d374f24 | ||
|
|
e32929029b | ||
|
|
eb76e882c5 | ||
|
|
d326417edc | ||
|
|
a3a8fef6b2 | ||
|
|
0c16334426 | ||
|
|
600f8cd142 | ||
|
|
5c8c85a0ff | ||
|
|
7a6f21648a | ||
|
|
df0e03cd07 | ||
|
|
85db812fea | ||
|
|
fb5b5e138d | ||
|
|
3eaf03a7ac | ||
|
|
5420f3451c | ||
|
|
7d94da10fb | ||
|
|
86090a694f | ||
|
|
1ee8287c73 | ||
|
|
c7322a71f7 | ||
|
|
2c3523f6a1 | ||
|
|
dd6076049c | ||
|
|
ba8ba5c634 | ||
|
|
c2069f37cc | ||
|
|
1e0aa7ee2c | ||
|
|
27942f5ce8 | ||
|
|
d0ff79ea60 | ||
|
|
3de02566bf | ||
|
|
93fd869ba3 | ||
|
|
3ca149137e | ||
|
|
db9aa41096 | ||
|
|
bf8e7f3393 | ||
|
|
8eb98cd591 | ||
|
|
0f9ba21b05 | ||
|
|
7a059a5e90 | ||
|
|
e5fc104aff | ||
|
|
d0ed165630 | ||
|
|
68ef6a842f | ||
|
|
c1f070a136 | ||
|
|
c2cc1ec5e5 | ||
|
|
386925ad8e | ||
|
|
243c1db408 | ||
|
|
834f8e7046 | ||
|
|
32e3399334 | ||
|
|
9e7bcacf8c | ||
|
|
7be7d7d1e7 | ||
|
|
04c1d0e071 | ||
|
|
ab62e0f75b | ||
|
|
d85f99c87c | ||
|
|
c42b6aece9 | ||
|
|
7f8f3080c5 | ||
|
|
9cf4191079 | ||
|
|
b8e2d75014 | ||
|
|
f522f16526 | ||
|
|
b010d2663d | ||
|
|
a083ceaf44 | ||
|
|
95798a2eba | ||
|
|
43b6633183 | ||
|
|
c50ac022a8 | ||
|
|
a3d36237e2 | ||
|
|
a2be61f26d | ||
|
|
79f5b579d7 | ||
|
|
66ecee1e26 | ||
|
|
02e86ea18f | ||
|
|
723dbe1da7 | ||
|
|
65fe89441f | ||
|
|
2093122ac5 | ||
|
|
ab584c93bc | ||
|
|
2d8698a218 | ||
|
|
454fb883a2 | ||
|
|
fc504a3d2c | ||
|
|
dd805503fb | ||
|
|
f24336f77a | ||
|
|
aa6a752e38 | ||
|
|
83b576eb19 | ||
|
|
c4e31a0d5e | ||
|
|
f8cdd6e80d | ||
|
|
f8b5a0fd50 | ||
|
|
6f4a6ab8ea | ||
|
|
9c4b6f36f1 | ||
|
|
140aed3586 | ||
|
|
cf87b78636 | ||
|
|
ec827da5a5 | ||
|
|
20528a2442 | ||
|
|
78886b1e67 | ||
|
|
d9debaf032 | ||
|
|
b3c47649b4 | ||
|
|
70be28d22c | ||
|
|
9df4dee1b2 | ||
|
|
60ffe6a993 | ||
|
|
0c880def5e | ||
|
|
d4360d6347 | ||
|
|
175b1785c0 | ||
|
|
e4660a5ba2 | ||
|
|
e2fa6d83c6 | ||
|
|
a0d32e7b88 | ||
|
|
ced15b64a8 | ||
|
|
f0723b6ee7 | ||
|
|
2162da3a14 | ||
|
|
f02cfd8271 | ||
|
|
b6a0f9f069 | ||
|
|
5c9c1d1a4b | ||
|
|
ab4c5a55b8 | ||
|
|
43c2fc3c37 | ||
|
|
371033a0f2 | ||
|
|
06706a2d9c | ||
|
|
c548c06086 | ||
|
|
8e5067ee91 | ||
|
|
0310b8614e | ||
|
|
829fecd338 | ||
|
|
44a293f051 | ||
|
|
a92c35a7ac | ||
|
|
691db40a33 | ||
|
|
2ae89f2c32 | ||
|
|
9d37af9453 | ||
|
|
a5d2a26fcc | ||
|
|
c61c3bc608 | ||
|
|
1420f239fc | ||
|
|
3d0e1bc9db | ||
|
|
71ccb90ef4 | ||
|
|
c8564b7792 | ||
|
|
215c69acb2 | ||
|
|
c1f67372a7 | ||
|
|
b929c0adbb | ||
|
|
1e5951a75f | ||
|
|
a644f64c6b | ||
|
|
c8740c0171 | ||
|
|
91ee895a74 | ||
|
|
339d4ec355 | ||
|
|
615038ac6d | ||
|
|
3c57cbc567 | ||
|
|
da929d5edc | ||
|
|
124c4d0778 | ||
|
|
19d79b6a0f | ||
|
|
3a9caea846 | ||
|
|
34e6098687 | ||
|
|
98a1e57ba9 | ||
|
|
f31cdf7bff | ||
|
|
9a3e1490ff | ||
|
|
1f2fd58e28 | ||
|
|
bcf3a0f677 | ||
|
|
f40dedb451 | ||
|
|
266f6846b5 | ||
|
|
49ca9a9db8 | ||
|
|
e7a7d8cc1d | ||
|
|
34c2da4ab1 | ||
|
|
7497203014 | ||
|
|
d731a4f695 | ||
|
|
03f6be6d0e | ||
|
|
938b5b4d1d | ||
|
|
792b51b5dc | ||
|
|
a57eae20b0 | ||
|
|
1ffe212d83 | ||
|
|
e29c07b28d | ||
|
|
745d15d200 | ||
|
|
05cfe74904 | ||
|
|
4d4a57d1bf | ||
|
|
382f155f76 | ||
|
|
60030a774d | ||
|
|
a045e46571 | ||
|
|
44eaa65c3b | ||
|
|
26730e56ea | ||
|
|
7b6f8cb902 | ||
|
|
111835f402 | ||
|
|
3fc935d4bb | ||
|
|
b939785ece | ||
|
|
723628cfcf | ||
|
|
cf489453c9 | ||
|
|
6616065d82 | ||
|
|
2df82dd870 | ||
|
|
0ca8d7fc03 | ||
|
|
f36e6d9917 | ||
|
|
6a4b020dd8 | ||
|
|
b51ede2372 | ||
|
|
1a4797abc4 | ||
|
|
c09300c06f | ||
|
|
ae353bb3f4 | ||
|
|
54f5bf9437 | ||
|
|
b0f4500c34 | ||
|
|
af032f8993 | ||
|
|
f177b02cae | ||
|
|
5323cb5224 | ||
|
|
0a22af7b14 | ||
|
|
b54702ab08 | ||
|
|
a98fc71720 | ||
|
|
9a05223e7d | ||
|
|
a7e3c26fe3 | ||
|
|
37de4e2e0a | ||
|
|
61a911dd39 | ||
|
|
cc5d0ef4cf | ||
|
|
7843d8f054 | ||
|
|
d759f9c121 | ||
|
|
f25e585008 | ||
|
|
4f96cd9164 | ||
|
|
7893e8229f | ||
|
|
795ef67712 | ||
|
|
88f6d3f241 | ||
|
|
8e87f01aa0 | ||
|
|
c4fdcfc5d1 | ||
|
|
cb8117e8df | ||
|
|
d547ed4a6b | ||
|
|
f40c389a15 | ||
|
|
bc1e84325c | ||
|
|
a0c605faae | ||
|
|
ba2033a8fb | ||
|
|
a7848b916b | ||
|
|
44c41e9e4d | ||
|
|
a663364223 | ||
|
|
72fda8f592 | ||
|
|
1aa9465611 | ||
|
|
4d3194d784 | ||
|
|
5404f22bf9 | ||
|
|
ccb2cb5b7c | ||
|
|
26ba056302 | ||
|
|
0dac9c68f0 | ||
|
|
3df6c9ac05 | ||
|
|
2db081938f | ||
|
|
d979570227 | ||
|
|
99c42033b1 | ||
|
|
7ba6962707 | ||
|
|
6eda1c1fb2 | ||
|
|
5a218d5056 | ||
|
|
8dbc5cf9c6 | ||
|
|
d33f136660 | ||
|
|
173dad345e | ||
|
|
47b0eb6324 | ||
|
|
c35c37008d | ||
|
|
aad2ee675c | ||
|
|
b8aabfffe8 | ||
|
|
ac8e124d01 | ||
|
|
857f8c2a95 | ||
|
|
20f9a50cee | ||
|
|
a192b600fc | ||
|
|
b714652e10 | ||
|
|
034478409e | ||
|
|
fe438bdb45 | ||
|
|
6acd958927 |
@@ -51,7 +51,7 @@ DB_USERNAME=database_username
|
||||
DB_PASSWORD=database_user_password
|
||||
|
||||
# Mail system to use
|
||||
# Can be 'smtp', 'mail' or 'sendmail'
|
||||
# Can be 'smtp' or 'sendmail'
|
||||
MAIL_DRIVER=smtp
|
||||
|
||||
# Mail sending options
|
||||
@@ -195,10 +195,12 @@ LDAP_DN=false
|
||||
LDAP_PASS=false
|
||||
LDAP_USER_FILTER=false
|
||||
LDAP_VERSION=false
|
||||
LDAP_START_TLS=false
|
||||
LDAP_TLS_INSECURE=false
|
||||
LDAP_ID_ATTRIBUTE=uid
|
||||
LDAP_EMAIL_ATTRIBUTE=mail
|
||||
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
|
||||
LDAP_THUMBNAIL_ATTRIBUTE=null
|
||||
LDAP_FOLLOW_REFERRALS=true
|
||||
LDAP_DUMP_USER_DETAILS=false
|
||||
|
||||
@@ -221,6 +223,7 @@ SAML2_IDP_x509=null
|
||||
SAML2_ONELOGIN_OVERRIDES=null
|
||||
SAML2_DUMP_USER_DETAILS=false
|
||||
SAML2_AUTOLOAD_METADATA=false
|
||||
SAML2_IDP_AUTHNCONTEXT=true
|
||||
|
||||
# SAML group sync configuration
|
||||
# Refer to https://www.bookstackapp.com/docs/admin/saml2-auth/
|
||||
@@ -245,10 +248,15 @@ AVATAR_URL=
|
||||
DRAWIO=true
|
||||
|
||||
# Default item listing view
|
||||
# Used for public visitors and user's without a preference
|
||||
# Can be 'list' or 'grid'
|
||||
# Used for public visitors and user's without a preference.
|
||||
# Can be 'list' or 'grid'.
|
||||
APP_VIEWS_BOOKS=list
|
||||
APP_VIEWS_BOOKSHELVES=grid
|
||||
APP_VIEWS_BOOKSHELF=grid
|
||||
|
||||
# Use dark mode by default
|
||||
# Will be overriden by any user/session preference.
|
||||
APP_DEFAULT_DARK_MODE=false
|
||||
|
||||
# Page revision limit
|
||||
# Number of page revisions to keep in the system before deleting old revisions.
|
||||
|
||||
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discord chat support
|
||||
url: https://discord.gg/ztkBqR2
|
||||
about: Realtime support / chat with the community and the team.
|
||||
|
||||
- name: Debugging & Common Issues
|
||||
url: https://www.bookstackapp.com/docs/admin/debugging/
|
||||
about: Find details on how to debug issues and view common issues with thier resolutions.
|
||||
34
.github/translators.txt
vendored
34
.github/translators.txt
vendored
@@ -49,6 +49,12 @@ Name :: Languages
|
||||
@jzoy :: Simplified Chinese
|
||||
@ististudio :: Korean
|
||||
@leomartinez :: Spanish Argentina
|
||||
@geins :: German
|
||||
@Ereza :: Catalan
|
||||
@benediktvolke :: German
|
||||
@Baptistou :: French
|
||||
@arcoai :: Spanish
|
||||
@Jokuna :: Korean
|
||||
cipi1965 :: Italian
|
||||
Mykola Ronik (Mantikor) :: Ukrainian
|
||||
furkanoyk :: Turkish
|
||||
@@ -133,3 +139,31 @@ MatthieuParis :: French
|
||||
Douradinho :: Portuguese, Brazilian
|
||||
Gaku Yaguchi (tama11) :: Japanese
|
||||
johnroyer :: Chinese Traditional
|
||||
jackaaa :: Chinese Traditional
|
||||
Irfan Hukama Arsyad (IrfanArsyad) :: Indonesian
|
||||
Jeff Huang (s8321414) :: Chinese Traditional
|
||||
Luís Tiago Favas (starkyller) :: Portuguese
|
||||
semirte :: Bosnian
|
||||
aarchijs :: Latvian
|
||||
Martins Pilsetnieks (pilsetnieks) :: Latvian
|
||||
Yonatan Magier (yonatanmgr) :: Hebrew
|
||||
FastHogi :: German Informal; German
|
||||
Ole Anders (Swoy) :: Norwegian Bokmal
|
||||
Atlochowski (atlochowski) :: Polish
|
||||
Simon (DefaultSimon) :: Slovenian
|
||||
Reinis Mednis (Mednis) :: Latvian
|
||||
toisho (toishoki) :: Turkish
|
||||
nikservik :: Ukrainian; Russian; Polish
|
||||
HenrijsS :: Latvian
|
||||
Pascal R-B (pborgner) :: German
|
||||
Boris (Ginfred) :: Russian
|
||||
Jonas Anker Rasmussen (jonasanker) :: Danish
|
||||
Gerwin de Keijzer (gdekeijzer) :: Dutch; German; German Informal
|
||||
kometchtech :: Japanese
|
||||
Auri (Atalonica) :: Catalan
|
||||
Francesco Franchina (ffranchina) :: Italian
|
||||
Aimrane Kds (aimrane.kds) :: Arabic
|
||||
whenwesober :: Indonesian
|
||||
Rem (remkovdhoef) :: Dutch
|
||||
syn7ax69 :: Bulgarian; Turkish
|
||||
Blaade :: French
|
||||
|
||||
13
.github/workflows/phpunit.yml
vendored
13
.github/workflows/phpunit.yml
vendored
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- release
|
||||
- gh_actions_update
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -13,13 +14,19 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php: [7.2, 7.4]
|
||||
php: ['7.3', '7.4', '8.0']
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@b7d1d9c9a92d8d8463ce36d7f60da34d461724f8
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
@@ -38,7 +45,7 @@ jobs:
|
||||
- name: Setup Database
|
||||
run: |
|
||||
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
|
||||
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
|
||||
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';"
|
||||
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
||||
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
|
||||
|
||||
|
||||
13
.github/workflows/test-migrations.yml
vendored
13
.github/workflows/test-migrations.yml
vendored
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- release
|
||||
- gh_actions_update
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -13,13 +14,19 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php: [7.2, 7.4]
|
||||
php: ['7.3', '7.4', '8.0']
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@b7d1d9c9a92d8d8463ce36d7f60da34d461724f8
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
@@ -38,7 +45,7 @@ jobs:
|
||||
- name: Create database & user
|
||||
run: |
|
||||
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
|
||||
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
|
||||
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';"
|
||||
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
||||
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
@@ -23,7 +24,7 @@ class Activity extends Model
|
||||
/**
|
||||
* Get the entity for this activity.
|
||||
*/
|
||||
public function entity()
|
||||
public function entity(): MorphTo
|
||||
{
|
||||
if ($this->entity_type === '') {
|
||||
$this->entity_type = null;
|
||||
|
||||
@@ -78,7 +78,7 @@ class ActivityService
|
||||
public function latest(int $count = 20, int $page = 0): array
|
||||
{
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')
|
||||
->with(['user', 'entity'])
|
||||
->skip($count * $page)
|
||||
@@ -131,7 +131,7 @@ class ActivityService
|
||||
public function userActivity(User $user, int $count = 20, int $page = 0): array
|
||||
{
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')
|
||||
->where('user_id', '=', $user->id)
|
||||
->skip($count * $page)
|
||||
|
||||
@@ -48,4 +48,4 @@ class ActivityType
|
||||
const AUTH_PASSWORD_RESET_UPDATE = 'auth_password_reset_update';
|
||||
const AUTH_LOGIN = 'auth_login';
|
||||
const AUTH_REGISTER = 'auth_register';
|
||||
}
|
||||
}
|
||||
|
||||
17
app/Actions/Favourite.php
Normal file
17
app/Actions/Favourite.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class Favourite extends Model
|
||||
{
|
||||
protected $fillable = ['user_id'];
|
||||
|
||||
/**
|
||||
* Get the related model that can be favourited.
|
||||
*/
|
||||
public function favouritable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class Tag extends Model
|
||||
{
|
||||
@@ -9,10 +10,25 @@ class Tag extends Model
|
||||
|
||||
/**
|
||||
* Get the entity that this tag belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
public function entity(): MorphTo
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a full URL to start a tag name search for this tag name.
|
||||
*/
|
||||
public function nameUrl(): string
|
||||
{
|
||||
return url('/search?term=%5B' . urlencode($this->name) .'%5D');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a full URL to start a tag name and value search for this tag's values.
|
||||
*/
|
||||
public function valueUrl(): string
|
||||
{
|
||||
return url('/search?term=%5B' . urlencode($this->name) .'%3D' . urlencode($this->value) . '%5D');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ class TagRepo
|
||||
*/
|
||||
public function getNameSuggestions(?string $searchTerm): Collection
|
||||
{
|
||||
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('name');
|
||||
$query = $this->tag->newQuery()
|
||||
->select('*', DB::raw('count(*) as count'))
|
||||
->groupBy('name');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
|
||||
@@ -45,7 +47,9 @@ class TagRepo
|
||||
*/
|
||||
public function getValueSuggestions(?string $searchTerm, ?string $tagName): Collection
|
||||
{
|
||||
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('value');
|
||||
$query = $this->tag->newQuery()
|
||||
->select('*', DB::raw('count(*) as count'))
|
||||
->groupBy('value');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Interfaces\Viewable;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
/**
|
||||
* Class View
|
||||
* Views are stored per-item per-person within the database.
|
||||
* They can be used to find popular items or recently viewed items
|
||||
* at a per-person level. They do not record every view instance as an
|
||||
* activity. Only the latest and original view times could be recognised.
|
||||
*
|
||||
* @property int $views
|
||||
* @property int $user_id
|
||||
*/
|
||||
class View extends Model
|
||||
{
|
||||
|
||||
@@ -9,10 +21,37 @@ class View extends Model
|
||||
|
||||
/**
|
||||
* Get all owning viewable models.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function viewable()
|
||||
public function viewable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the current user's view count for the given viewable model.
|
||||
*/
|
||||
public static function incrementFor(Viewable $viewable): int
|
||||
{
|
||||
$user = user();
|
||||
if (is_null($user) || $user->isDefault()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @var View $view */
|
||||
$view = $viewable->views()->firstOrNew([
|
||||
'user_id' => $user->id,
|
||||
], ['views' => 0]);
|
||||
|
||||
$view->forceFill(['views' => $view->views + 1])->save();
|
||||
|
||||
return $view->views;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all views from the system.
|
||||
*/
|
||||
public static function clearAll()
|
||||
{
|
||||
static::query()->truncate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use DB;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ViewService
|
||||
{
|
||||
protected $view;
|
||||
protected $permissionService;
|
||||
protected $entityProvider;
|
||||
|
||||
/**
|
||||
* ViewService constructor.
|
||||
* @param View $view
|
||||
* @param PermissionService $permissionService
|
||||
* @param EntityProvider $entityProvider
|
||||
*/
|
||||
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->entityProvider = $entityProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a view to the given entity.
|
||||
* @param \BookStack\Entities\Models\Entity $entity
|
||||
* @return int
|
||||
*/
|
||||
public function add(Entity $entity)
|
||||
{
|
||||
$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');
|
||||
return $view->views;
|
||||
}
|
||||
|
||||
// Otherwise create new view count
|
||||
$entity->views()->save($this->view->newInstance([
|
||||
'user_id' => $user->id,
|
||||
'views' => 1
|
||||
]));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entities with the most views.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param string|array $filterModels
|
||||
* @param string $action - used for permission checking
|
||||
* @return Collection
|
||||
*/
|
||||
public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
|
||||
{
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
|
||||
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
|
||||
if ($filterModels) {
|
||||
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
|
||||
}
|
||||
|
||||
return $query->with('viewable')
|
||||
->skip($skipCount)
|
||||
->take($count)
|
||||
->get()
|
||||
->pluck('viewable')
|
||||
->filter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all recently viewed entities for the current user.
|
||||
*/
|
||||
public function getUserRecentlyViewed(int $count = 10, int $page = 1)
|
||||
{
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$all = collect();
|
||||
/** @var Entity $instance */
|
||||
foreach ($this->entityProvider->all() as $name => $instance) {
|
||||
$items = $instance::visible()->withLastView()
|
||||
->orderBy('last_viewed_at', 'desc')
|
||||
->skip($count * ($page - 1))
|
||||
->take($count)
|
||||
->get();
|
||||
$all = $all->concat($items);
|
||||
}
|
||||
|
||||
return $all->sortByDesc('last_viewed_at')->slice(0, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all view counts by deleting all views.
|
||||
*/
|
||||
public function resetAll()
|
||||
{
|
||||
$this->view->truncate();
|
||||
}
|
||||
}
|
||||
@@ -142,5 +142,4 @@ class ApiDocsGenerator
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,4 +163,4 @@ class ApiTokenGuard implements Guard
|
||||
{
|
||||
$this->user = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,5 +299,4 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,14 +5,12 @@ namespace BookStack\Auth\Access\Guards;
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use BookStack\Auth\Access\RegistrationService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use BookStack\Exceptions\LoginAttemptException;
|
||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
@@ -23,13 +21,13 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
/**
|
||||
* LdapSessionGuard constructor.
|
||||
*/
|
||||
public function __construct($name,
|
||||
public function __construct(
|
||||
$name,
|
||||
UserProvider $provider,
|
||||
Session $session,
|
||||
LdapService $ldapService,
|
||||
RegistrationService $registrationService
|
||||
)
|
||||
{
|
||||
) {
|
||||
$this->ldapService = $ldapService;
|
||||
parent::__construct($name, $provider, $session, $registrationService);
|
||||
}
|
||||
@@ -92,6 +90,11 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
$this->ldapService->syncGroups($user, $username);
|
||||
}
|
||||
|
||||
// Attach avatar if non-existent
|
||||
if (is_null($user->avatar)) {
|
||||
$this->ldapService->saveAndAttachAvatar($user, $userDetails);
|
||||
}
|
||||
|
||||
$this->login($user, $remember);
|
||||
return true;
|
||||
}
|
||||
@@ -117,7 +120,8 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
'password' => Str::random(32),
|
||||
];
|
||||
|
||||
return $this->registrationService->registerUser($details, null, false);
|
||||
$user = $this->registrationService->registerUser($details, null, false);
|
||||
$this->ldapService->saveAndAttachAvatar($user, $ldapUserDetails);
|
||||
return $user;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,5 +34,4 @@ class Saml2SessionGuard extends ExternalBaseSessionGuard
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@ class Ldap
|
||||
return ldap_set_option($ldapConnection, $option, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start TLS on the given LDAP connection.
|
||||
*/
|
||||
public function startTls($ldapConnection): bool
|
||||
{
|
||||
return ldap_start_tls($ldapConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the version number for the given ldap connection.
|
||||
* @param $ldapConnection
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\JsonDebugException;
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use BookStack\Uploads\UserAvatars;
|
||||
use ErrorException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class LdapService
|
||||
@@ -14,15 +16,17 @@ class LdapService extends ExternalAuthService
|
||||
|
||||
protected $ldap;
|
||||
protected $ldapConnection;
|
||||
protected $userAvatars;
|
||||
protected $config;
|
||||
protected $enabled;
|
||||
|
||||
/**
|
||||
* LdapService constructor.
|
||||
*/
|
||||
public function __construct(Ldap $ldap)
|
||||
public function __construct(Ldap $ldap, UserAvatars $userAvatars)
|
||||
{
|
||||
$this->ldap = $ldap;
|
||||
$this->userAvatars = $userAvatars;
|
||||
$this->config = config('services.ldap');
|
||||
$this->enabled = config('auth.method') === 'ldap';
|
||||
}
|
||||
@@ -76,19 +80,23 @@ class LdapService extends ExternalAuthService
|
||||
$idAttr = $this->config['id_attribute'];
|
||||
$emailAttr = $this->config['email_attribute'];
|
||||
$displayNameAttr = $this->config['display_name_attribute'];
|
||||
$thumbnailAttr = $this->config['thumbnail_attribute'];
|
||||
|
||||
$user = $this->getUserWithAttributes($userName, ['cn', 'dn', $idAttr, $emailAttr, $displayNameAttr]);
|
||||
$user = $this->getUserWithAttributes($userName, array_filter([
|
||||
'cn', 'dn', $idAttr, $emailAttr, $displayNameAttr, $thumbnailAttr,
|
||||
]));
|
||||
|
||||
if ($user === null) {
|
||||
if (is_null($user)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userCn = $this->getUserResponseProperty($user, 'cn', null);
|
||||
$formatted = [
|
||||
'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']),
|
||||
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
|
||||
'dn' => $user['dn'],
|
||||
'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']),
|
||||
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
|
||||
'dn' => $user['dn'],
|
||||
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
|
||||
'avatar'=> $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,
|
||||
];
|
||||
|
||||
if ($this->config['dump_user_details']) {
|
||||
@@ -187,8 +195,8 @@ class LdapService extends ExternalAuthService
|
||||
throw new LdapException(trans('errors.ldap_extension_not_installed'));
|
||||
}
|
||||
|
||||
// Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
|
||||
// the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not per handle.
|
||||
// Disable certificate verification.
|
||||
// This option works globally and must be set before a connection is created.
|
||||
if ($this->config['tls_insecure']) {
|
||||
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
|
||||
}
|
||||
@@ -205,6 +213,14 @@ class LdapService extends ExternalAuthService
|
||||
$this->ldap->setVersion($ldapConnection, $this->config['version']);
|
||||
}
|
||||
|
||||
// Start and verify TLS if it's enabled
|
||||
if ($this->config['start_tls']) {
|
||||
$started = $this->ldap->startTls($ldapConnection);
|
||||
if (!$started) {
|
||||
throw new LdapException('Could not start TLS connection');
|
||||
}
|
||||
}
|
||||
|
||||
$this->ldapConnection = $ldapConnection;
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
@@ -342,4 +358,22 @@ class LdapService extends ExternalAuthService
|
||||
$userLdapGroups = $this->getUserGroups($username);
|
||||
$this->syncWithGroups($user, $userLdapGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save and attach an avatar image, if found in the ldap details, and attach
|
||||
* to the given user model.
|
||||
*/
|
||||
public function saveAndAttachAvatar(User $user, array $ldapUserDetails): void
|
||||
{
|
||||
if (is_null(config('services.ldap.thumbnail_attribute')) || is_null($ldapUserDetails['avatar'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$imageData = $ldapUserDetails['avatar'];
|
||||
$this->userAvatars->assignToUserFromExistingData($user, $imageData, 'jpg');
|
||||
} catch (\Exception $exception) {
|
||||
Log::info("Failed to use avatar image from LDAP data for user id {$user->id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Exception;
|
||||
|
||||
class RegistrationService
|
||||
@@ -71,6 +73,7 @@ class RegistrationService
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::AUTH_REGISTER, $socialAccount ?? $newUser);
|
||||
Theme::dispatch(ThemeEvents::AUTH_REGISTER, $socialAccount ? $socialAccount->driver : auth()->getDefaultDriver(), $newUser);
|
||||
|
||||
// Start email confirmation flow if required
|
||||
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
|
||||
@@ -83,7 +86,6 @@ class RegistrationService
|
||||
$message = trans('auth.email_confirm_send_error');
|
||||
throw new UserRegistrationException($message, '/register/confirm');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $newUser;
|
||||
@@ -109,5 +111,4 @@ class RegistrationService
|
||||
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ use BookStack\Exceptions\JsonDebugException;
|
||||
use BookStack\Exceptions\SamlException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Exception;
|
||||
use Illuminate\Support\Str;
|
||||
use OneLogin\Saml2\Auth;
|
||||
@@ -375,6 +377,7 @@ class Saml2Service extends ExternalAuthService
|
||||
|
||||
auth()->login($user);
|
||||
Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}");
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, 'saml2', $user);
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,37 +2,63 @@
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\SocialAccount;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||
use Laravel\Socialite\Contracts\Provider;
|
||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
||||
class SocialAuthService
|
||||
{
|
||||
|
||||
protected $userRepo;
|
||||
/**
|
||||
* The core socialite library used.
|
||||
* @var Socialite
|
||||
*/
|
||||
protected $socialite;
|
||||
protected $socialAccount;
|
||||
|
||||
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch', 'discord'];
|
||||
/**
|
||||
* The default built-in social drivers we support.
|
||||
* @var string[]
|
||||
*/
|
||||
protected $validSocialDrivers = [
|
||||
'google',
|
||||
'github',
|
||||
'facebook',
|
||||
'slack',
|
||||
'twitter',
|
||||
'azure',
|
||||
'okta',
|
||||
'gitlab',
|
||||
'twitch',
|
||||
'discord'
|
||||
];
|
||||
|
||||
/**
|
||||
* Callbacks to run when configuring a social driver
|
||||
* for an initial redirect action.
|
||||
* Array is keyed by social driver name.
|
||||
* Callbacks are passed an instance of the driver.
|
||||
* @var array<string, callable>
|
||||
*/
|
||||
protected $configureForRedirectCallbacks = [];
|
||||
|
||||
/**
|
||||
* SocialAuthService constructor.
|
||||
*/
|
||||
public function __construct(UserRepo $userRepo, Socialite $socialite, SocialAccount $socialAccount)
|
||||
public function __construct(Socialite $socialite)
|
||||
{
|
||||
$this->userRepo = $userRepo;
|
||||
$this->socialite = $socialite;
|
||||
$this->socialAccount = $socialAccount;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start the social login path.
|
||||
* @throws SocialDriverNotConfigured
|
||||
@@ -40,7 +66,7 @@ class SocialAuthService
|
||||
public function startLogIn(string $socialDriver): RedirectResponse
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
return $this->getSocialDriver($driver)->redirect();
|
||||
return $this->getDriverForRedirect($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +76,7 @@ class SocialAuthService
|
||||
public function startRegister(string $socialDriver): RedirectResponse
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
return $this->getSocialDriver($driver)->redirect();
|
||||
return $this->getDriverForRedirect($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,11 +86,11 @@ class SocialAuthService
|
||||
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser
|
||||
{
|
||||
// Check social account has not already been used
|
||||
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
|
||||
if (SocialAccount::query()->where('driver_id', '=', $socialUser->getId())->exists()) {
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount' => $socialDriver]), '/login');
|
||||
}
|
||||
|
||||
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
|
||||
if (User::query()->where('email', '=', $socialUser->getEmail())->exists()) {
|
||||
$email = $socialUser->getEmail();
|
||||
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
|
||||
}
|
||||
@@ -91,7 +117,7 @@ class SocialAuthService
|
||||
$socialId = $socialUser->getId();
|
||||
|
||||
// Get any attached social accounts or users
|
||||
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
|
||||
$socialAccount = SocialAccount::query()->where('driver_id', '=', $socialId)->first();
|
||||
$isLoggedIn = auth()->check();
|
||||
$currentUser = user();
|
||||
$titleCaseDriver = Str::title($socialDriver);
|
||||
@@ -101,14 +127,15 @@ class SocialAuthService
|
||||
if (!$isLoggedIn && $socialAccount !== null) {
|
||||
auth()->login($socialAccount->user);
|
||||
Activity::add(ActivityType::AUTH_LOGIN, $socialAccount);
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $socialAccount->user);
|
||||
return redirect()->intended('/');
|
||||
}
|
||||
|
||||
// When a user is logged in but the social account does not exist,
|
||||
// Create the social account and attach it to the user & redirect to the profile page.
|
||||
if ($isLoggedIn && $socialAccount === null) {
|
||||
$this->fillSocialAccount($socialDriver, $socialUser);
|
||||
$currentUser->socialAccounts()->save($this->socialAccount);
|
||||
$account = $this->newSocialAccount($socialDriver, $socialUser);
|
||||
$currentUser->socialAccounts()->save($account);
|
||||
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
@@ -130,7 +157,7 @@ class SocialAuthService
|
||||
if (setting('registration-enabled') && config('auth.method') !== 'ldap' && config('auth.method') !== 'saml2') {
|
||||
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => $titleCaseDriver]);
|
||||
}
|
||||
|
||||
|
||||
throw new SocialSignInAccountNotUsed($message, '/login');
|
||||
}
|
||||
|
||||
@@ -207,21 +234,19 @@ class SocialAuthService
|
||||
/**
|
||||
* Fill and return a SocialAccount from the given driver name and SocialUser.
|
||||
*/
|
||||
public function fillSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
|
||||
public function newSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
|
||||
{
|
||||
$this->socialAccount->fill([
|
||||
'driver' => $socialDriver,
|
||||
return new SocialAccount([
|
||||
'driver' => $socialDriver,
|
||||
'driver_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar()
|
||||
'avatar' => $socialUser->getAvatar()
|
||||
]);
|
||||
return $this->socialAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach a social account from a user.
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function detachSocialAccount(string $socialDriver)
|
||||
public function detachSocialAccount(string $socialDriver): void
|
||||
{
|
||||
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
||||
}
|
||||
@@ -229,7 +254,7 @@ class SocialAuthService
|
||||
/**
|
||||
* Provide redirect options per service for the Laravel Socialite driver
|
||||
*/
|
||||
public function getSocialDriver(string $driverName): Provider
|
||||
protected function getDriverForRedirect(string $driverName): Provider
|
||||
{
|
||||
$driver = $this->socialite->driver($driverName);
|
||||
|
||||
@@ -240,6 +265,33 @@ class SocialAuthService
|
||||
$driver->with(['resource' => 'https://graph.windows.net']);
|
||||
}
|
||||
|
||||
if (isset($this->configureForRedirectCallbacks[$driverName])) {
|
||||
$this->configureForRedirectCallbacks[$driverName]($driver);
|
||||
}
|
||||
|
||||
return $driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom socialite driver to be used.
|
||||
* Driver name should be lower_snake_case.
|
||||
* Config array should mirror the structure of a service
|
||||
* within the `Config/services.php` file.
|
||||
* Handler should be a Class@method handler to the SocialiteWasCalled event.
|
||||
*/
|
||||
public function addSocialDriver(
|
||||
string $driverName,
|
||||
array $config,
|
||||
string $socialiteHandler,
|
||||
callable $configureForRedirect = null
|
||||
) {
|
||||
$this->validSocialDrivers[] = $driverName;
|
||||
config()->set('services.' . $driverName, $config);
|
||||
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
|
||||
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
|
||||
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
|
||||
if (!is_null($configureForRedirect)) {
|
||||
$this->configureForRedirectCallbacks[$driverName] = $configureForRedirect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Permissions;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use BookStack\Traits\HasOwner;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Throwable;
|
||||
|
||||
class PermissionService
|
||||
{
|
||||
/**
|
||||
* @var ?array
|
||||
*/
|
||||
protected $userRoles = null;
|
||||
|
||||
protected $currentAction;
|
||||
protected $isAdminUser;
|
||||
protected $userRoles = false;
|
||||
protected $currentUserModel = false;
|
||||
/**
|
||||
* @var ?User
|
||||
*/
|
||||
protected $currentUserModel = null;
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
@@ -27,47 +35,20 @@ class PermissionService
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* @var JointPermission
|
||||
* @var array
|
||||
*/
|
||||
protected $jointPermission;
|
||||
|
||||
/**
|
||||
* @var Role
|
||||
*/
|
||||
protected $role;
|
||||
|
||||
/**
|
||||
* @var EntityPermission
|
||||
*/
|
||||
protected $entityPermission;
|
||||
|
||||
/**
|
||||
* @var EntityProvider
|
||||
*/
|
||||
protected $entityProvider;
|
||||
|
||||
protected $entityCache;
|
||||
|
||||
/**
|
||||
* PermissionService constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
JointPermission $jointPermission,
|
||||
Permissions\EntityPermission $entityPermission,
|
||||
Role $role,
|
||||
Connection $db,
|
||||
EntityProvider $entityProvider
|
||||
) {
|
||||
public function __construct(Connection $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->jointPermission = $jointPermission;
|
||||
$this->entityPermission = $entityPermission;
|
||||
$this->role = $role;
|
||||
$this->entityProvider = $entityProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the database connection
|
||||
* @param Connection $connection
|
||||
*/
|
||||
public function setConnection(Connection $connection)
|
||||
{
|
||||
@@ -76,81 +57,63 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Prepare the local entity cache and ensure it's empty
|
||||
* @param \BookStack\Entities\Models\Entity[] $entities
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function readyEntityCache($entities = [])
|
||||
protected function readyEntityCache(array $entities = [])
|
||||
{
|
||||
$this->entityCache = [];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$type = $entity->getType();
|
||||
if (!isset($this->entityCache[$type])) {
|
||||
$this->entityCache[$type] = collect();
|
||||
$class = get_class($entity);
|
||||
if (!isset($this->entityCache[$class])) {
|
||||
$this->entityCache[$class] = collect();
|
||||
}
|
||||
$this->entityCache[$type]->put($entity->id, $entity);
|
||||
$this->entityCache[$class]->put($entity->id, $entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a book via ID, Checks local cache
|
||||
* @param $bookId
|
||||
* @return Book
|
||||
*/
|
||||
protected function getBook($bookId)
|
||||
protected function getBook(int $bookId): ?Book
|
||||
{
|
||||
if (isset($this->entityCache['book']) && $this->entityCache['book']->has($bookId)) {
|
||||
return $this->entityCache['book']->get($bookId);
|
||||
if (isset($this->entityCache[Book::class]) && $this->entityCache[Book::class]->has($bookId)) {
|
||||
return $this->entityCache[Book::class]->get($bookId);
|
||||
}
|
||||
|
||||
$book = $this->entityProvider->book->find($bookId);
|
||||
if ($book === null) {
|
||||
$book = false;
|
||||
}
|
||||
|
||||
return $book;
|
||||
return Book::query()->withTrashed()->find($bookId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chapter via ID, Checks local cache
|
||||
* @param $chapterId
|
||||
* @return \BookStack\Entities\Models\Book
|
||||
*/
|
||||
protected function getChapter($chapterId)
|
||||
protected function getChapter(int $chapterId): ?Chapter
|
||||
{
|
||||
if (isset($this->entityCache['chapter']) && $this->entityCache['chapter']->has($chapterId)) {
|
||||
return $this->entityCache['chapter']->get($chapterId);
|
||||
if (isset($this->entityCache[Chapter::class]) && $this->entityCache[Chapter::class]->has($chapterId)) {
|
||||
return $this->entityCache[Chapter::class]->get($chapterId);
|
||||
}
|
||||
|
||||
$chapter = $this->entityProvider->chapter->find($chapterId);
|
||||
if ($chapter === null) {
|
||||
$chapter = false;
|
||||
}
|
||||
|
||||
return $chapter;
|
||||
return Chapter::query()
|
||||
->withTrashed()
|
||||
->find($chapterId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles for the current user;
|
||||
* @return array|bool
|
||||
* Get the roles for the current logged in user.
|
||||
*/
|
||||
protected function getRoles()
|
||||
protected function getCurrentUserRoles(): array
|
||||
{
|
||||
if ($this->userRoles !== false) {
|
||||
if (!is_null($this->userRoles)) {
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
$roles = [];
|
||||
|
||||
if (auth()->guest()) {
|
||||
$roles[] = $this->role->getSystemRole('public')->id;
|
||||
return $roles;
|
||||
$this->userRoles = [Role::getSystemRole('public')->id];
|
||||
} else {
|
||||
$this->userRoles = $this->currentUser()->roles->pluck('id')->values()->all();
|
||||
}
|
||||
|
||||
|
||||
foreach ($this->currentUser()->roles as $role) {
|
||||
$roles[] = $role->id;
|
||||
}
|
||||
return $roles;
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,59 +121,57 @@ class PermissionService
|
||||
*/
|
||||
public function buildJointPermissions()
|
||||
{
|
||||
$this->jointPermission->truncate();
|
||||
JointPermission::query()->truncate();
|
||||
$this->readyEntityCache();
|
||||
|
||||
// Get all roles (Should be the most limited dimension)
|
||||
$roles = $this->role->with('permissions')->get()->all();
|
||||
$roles = Role::query()->with('permissions')->get()->all();
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
$this->entityProvider->bookshelf->newQuery()->withTrashed()->select(['id', 'restricted', 'owned_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
Bookshelf::query()->withTrashed()->select(['id', 'restricted', 'owned_by'])
|
||||
->chunk(50, function (EloquentCollection $shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a query for fetching a book with it's children.
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
protected function bookFetchQuery()
|
||||
protected function bookFetchQuery(): Builder
|
||||
{
|
||||
return $this->entityProvider->book->withTrashed()->newQuery()
|
||||
->select(['id', 'restricted', 'owned_by'])->with(['chapters' => function ($query) {
|
||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
|
||||
}, 'pages' => function ($query) {
|
||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
|
||||
}]);
|
||||
return Book::query()->withTrashed()
|
||||
->select(['id', 'restricted', 'owned_by'])->with([
|
||||
'chapters' => function ($query) {
|
||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
|
||||
},
|
||||
'pages' => function ($query) {
|
||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $shelves
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
* @throws \Throwable
|
||||
* Build joint permissions for the given shelf and role combinations.
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false)
|
||||
protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false)
|
||||
{
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($shelves->all());
|
||||
}
|
||||
$this->createManyJointPermissions($shelves, $roles);
|
||||
$this->createManyJointPermissions($shelves->all(), $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build joint permissions for an array of books
|
||||
* @param Collection $books
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
* Build joint permissions for the given book and role combinations.
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
|
||||
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
|
||||
{
|
||||
$entities = clone $books;
|
||||
|
||||
@@ -227,55 +188,53 @@ class PermissionService
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
}
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
$this->createManyJointPermissions($entities->all(), $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a particular entity.
|
||||
* @param \BookStack\Entities\Models\Entity $entity
|
||||
* @throws \Throwable
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
$entities = [$entity];
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity instanceof Book) {
|
||||
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
|
||||
$this->buildJointPermissionsForBooks($books, $this->role->newQuery()->get(), true);
|
||||
$this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var BookChild $entity */
|
||||
if ($entity->book) {
|
||||
$entities[] = $entity->book;
|
||||
}
|
||||
|
||||
if ($entity->isA('page') && $entity->chapter_id) {
|
||||
if ($entity instanceof Page && $entity->chapter_id) {
|
||||
$entities[] = $entity->chapter;
|
||||
}
|
||||
|
||||
if ($entity->isA('chapter')) {
|
||||
if ($entity instanceof Chapter) {
|
||||
foreach ($entity->pages as $page) {
|
||||
$entities[] = $page;
|
||||
}
|
||||
}
|
||||
|
||||
$this->buildJointPermissionsForEntities(collect($entities));
|
||||
$this->buildJointPermissionsForEntities($entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a collection of entities.
|
||||
* @param Collection $entities
|
||||
* @throws \Throwable
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntities(Collection $entities)
|
||||
public function buildJointPermissionsForEntities(array $entities)
|
||||
{
|
||||
$roles = $this->role->newQuery()->get();
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
$roles = Role::query()->get()->values()->all();
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the entity jointPermissions for a particular role.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function buildJointPermissionForRole(Role $role)
|
||||
{
|
||||
@@ -288,7 +247,7 @@ class PermissionService
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'owned_by'])
|
||||
Bookshelf::query()->select(['id', 'restricted', 'owned_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
@@ -296,7 +255,6 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions attached to a particular role.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function deleteJointPermissionsForRole(Role $role)
|
||||
{
|
||||
@@ -312,13 +270,13 @@ class PermissionService
|
||||
$roleIds = array_map(function ($role) {
|
||||
return $role->id;
|
||||
}, $roles);
|
||||
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
|
||||
JointPermission::query()->whereIn('role_id', $roleIds)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
* @throws \Throwable
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function deleteJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
@@ -327,10 +285,10 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
* @param \BookStack\Entities\Models\Entity[] $entities
|
||||
* @throws \Throwable
|
||||
* @param Entity[] $entities
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForEntities($entities)
|
||||
protected function deleteManyJointPermissionsForEntities(array $entities)
|
||||
{
|
||||
if (count($entities) === 0) {
|
||||
return;
|
||||
@@ -352,19 +310,19 @@ class PermissionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & Save entity jointPermissions for many entities and jointPermissions.
|
||||
* @param Collection $entities
|
||||
* @param array $roles
|
||||
* @throws \Throwable
|
||||
* Create & Save entity jointPermissions for many entities and roles.
|
||||
* @param Entity[] $entities
|
||||
* @param Role[] $roles
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function createManyJointPermissions($entities, $roles)
|
||||
protected function createManyJointPermissions(array $entities, array $roles)
|
||||
{
|
||||
$this->readyEntityCache($entities);
|
||||
$jointPermissions = [];
|
||||
|
||||
// Fetch Entity Permissions and create a mapping of entity restricted statuses
|
||||
$entityRestrictedMap = [];
|
||||
$permissionFetch = $this->entityPermission->newQuery();
|
||||
$permissionFetch = EntityPermission::query();
|
||||
foreach ($entities as $entity) {
|
||||
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
|
||||
$permissionFetch->orWhere(function ($query) use ($entity) {
|
||||
@@ -408,16 +366,14 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Get the actions related to an entity.
|
||||
* @param \BookStack\Entities\Models\Entity $entity
|
||||
* @return array
|
||||
*/
|
||||
protected function getActions(Entity $entity)
|
||||
protected function getActions(Entity $entity): array
|
||||
{
|
||||
$baseActions = ['view', 'update', 'delete'];
|
||||
if ($entity->isA('chapter') || $entity->isA('book')) {
|
||||
if ($entity instanceof Chapter || $entity instanceof Book) {
|
||||
$baseActions[] = 'page-create';
|
||||
}
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity instanceof Book) {
|
||||
$baseActions[] = 'chapter-create';
|
||||
}
|
||||
return $baseActions;
|
||||
@@ -426,14 +382,8 @@ class PermissionService
|
||||
/**
|
||||
* 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)
|
||||
protected function createJointPermissionData(Entity $entity, Role $role, string $action, array $permissionMap, array $rolePermissionMap): array
|
||||
{
|
||||
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
|
||||
$roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
|
||||
@@ -450,7 +400,7 @@ class PermissionService
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
if ($entity->isA('book') || $entity->isA('bookshelf')) {
|
||||
if ($entity instanceof Book || $entity instanceof Bookshelf) {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
|
||||
}
|
||||
|
||||
@@ -460,7 +410,7 @@ class PermissionService
|
||||
$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') {
|
||||
if ($entity instanceof Page && intval($entity->chapter_id) !== 0) {
|
||||
$chapter = $this->getChapter($entity->chapter_id);
|
||||
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
|
||||
if ($chapter->restricted) {
|
||||
@@ -479,38 +429,27 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* 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)
|
||||
protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool
|
||||
{
|
||||
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
|
||||
return isset($entityMap[$key]) ? $entityMap[$key] : false;
|
||||
return $entityMap[$key] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of data with the information of an entity jointPermissions.
|
||||
* Used to build data for bulk insertion.
|
||||
* @param \BookStack\Entities\Models\Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @param $permissionAll
|
||||
* @param $permissionOwn
|
||||
* @return array
|
||||
*/
|
||||
protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
|
||||
protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array
|
||||
{
|
||||
return [
|
||||
'role_id' => $role->getRawAttribute('id'),
|
||||
'entity_id' => $entity->getRawAttribute('id'),
|
||||
'entity_type' => $entity->getMorphClass(),
|
||||
'action' => $action,
|
||||
'has_permission' => $permissionAll,
|
||||
'role_id' => $role->getRawAttribute('id'),
|
||||
'entity_id' => $entity->getRawAttribute('id'),
|
||||
'entity_type' => $entity->getMorphClass(),
|
||||
'action' => $action,
|
||||
'has_permission' => $permissionAll,
|
||||
'has_permission_own' => $permissionOwn,
|
||||
'owned_by' => $entity->getRawAttribute('owned_by')
|
||||
'owned_by' => $entity->getRawAttribute('owned_by'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -524,55 +463,47 @@ class PermissionService
|
||||
|
||||
$baseQuery = $ownable->newQuery()->where('id', '=', $ownable->id);
|
||||
$action = end($explodedPermission);
|
||||
$this->currentAction = $action;
|
||||
$user = $this->currentUser();
|
||||
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
|
||||
|
||||
// Handle non entity specific jointPermissions
|
||||
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
||||
$allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
|
||||
$ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
|
||||
$this->currentAction = 'view';
|
||||
$allPermission = $user && $user->can($permission . '-all');
|
||||
$ownPermission = $user && $user->can($permission . '-own');
|
||||
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
|
||||
$isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->$ownerField;
|
||||
$isOwner = $user && $user->id === $ownable->$ownerField;
|
||||
return ($allPermission || ($isOwner && $ownPermission));
|
||||
}
|
||||
|
||||
// Handle abnormal create jointPermissions
|
||||
if ($action === 'create') {
|
||||
$this->currentAction = $permission;
|
||||
$action = $permission;
|
||||
}
|
||||
|
||||
$q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
|
||||
$hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0;
|
||||
$this->clean();
|
||||
return $q;
|
||||
return $hasAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user has the given permission for any items in the system.
|
||||
* Can be passed an entity instance to filter on a specific type.
|
||||
* @param string $permission
|
||||
* @param string $entityClass
|
||||
* @return bool
|
||||
*/
|
||||
public function checkUserHasPermissionOnAnything(string $permission, string $entityClass = null)
|
||||
public function checkUserHasPermissionOnAnything(string $permission, ?string $entityClass = null): bool
|
||||
{
|
||||
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
|
||||
$userId = $this->currentUser()->id;
|
||||
|
||||
$permissionQuery = $this->db->table('joint_permissions')
|
||||
$permissionQuery = JointPermission::query()
|
||||
->where('action', '=', $permission)
|
||||
->whereIn('role_id', $userRoleIds)
|
||||
->where(function ($query) use ($userId) {
|
||||
$query->where('has_permission', '=', 1)
|
||||
->orWhere(function ($query2) use ($userId) {
|
||||
$query2->where('has_permission_own', '=', 1)
|
||||
->where('owned_by', '=', $userId);
|
||||
});
|
||||
->where(function (Builder $query) use ($userId) {
|
||||
$this->addJointHasPermissionCheck($query, $userId);
|
||||
});
|
||||
|
||||
if (!is_null($entityClass)) {
|
||||
$entityInstance = app()->make($entityClass);
|
||||
$entityInstance = app($entityClass);
|
||||
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
|
||||
}
|
||||
|
||||
@@ -581,46 +512,22 @@ class PermissionService
|
||||
return $hasPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has restrictions set on itself or its
|
||||
* parent tree.
|
||||
* @param \BookStack\Entities\Models\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)
|
||||
protected function entityRestrictionQuery(Builder $query, string $action): Builder
|
||||
{
|
||||
$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('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
$q = $query->where(function ($parentQuery) use ($action) {
|
||||
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) use ($action) {
|
||||
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where('action', '=', $action)
|
||||
->where(function (Builder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$this->clean();
|
||||
return $q;
|
||||
}
|
||||
@@ -634,14 +541,10 @@ class PermissionService
|
||||
$this->clean();
|
||||
return $query->where(function (Builder $parentQuery) use ($ability) {
|
||||
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
|
||||
$permissionQuery->whereIn('role_id', $this->getRoles())
|
||||
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where('action', '=', $ability)
|
||||
->where(function (Builder $query) {
|
||||
$query->where('has_permission', '=', true)
|
||||
->orWhere(function (Builder $query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -651,7 +554,7 @@ class PermissionService
|
||||
* Extend the given page query to ensure draft items are not visible
|
||||
* unless created by the given user.
|
||||
*/
|
||||
public function enforceDraftVisiblityOnQuery(Builder $query): Builder
|
||||
public function enforceDraftVisibilityOnQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function (Builder $query) {
|
||||
$query->where('draft', '=', false)
|
||||
@@ -663,109 +566,90 @@ class PermissionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions for a generic entity
|
||||
* @param string $entityType
|
||||
* @param Builder|\BookStack\Entities\Models\Entity $query
|
||||
* @param string $action
|
||||
* @return Builder
|
||||
* Add restrictions for a generic entity.
|
||||
*/
|
||||
public function enforceEntityRestrictions($entityType, $query, $action = 'view')
|
||||
public function enforceEntityRestrictions(Entity $entity, Builder $query, string $action = 'view'): Builder
|
||||
{
|
||||
if (strtolower($entityType) === 'page') {
|
||||
if ($entity instanceof Page) {
|
||||
// Prevent drafts being visible to others.
|
||||
$query = $query->where(function ($query) {
|
||||
$query->where('draft', '=', false)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('draft', '=', true)
|
||||
->where('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
$this->enforceDraftVisibilityOnQuery($query);
|
||||
}
|
||||
|
||||
$this->currentAction = $action;
|
||||
return $this->entityRestrictionQuery($query);
|
||||
return $this->entityRestrictionQuery($query, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set as a polymorphic relation.
|
||||
* @param $query
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
* @param string $entityTypeColumn
|
||||
* @param string $action
|
||||
* @return QueryBuilder
|
||||
* @param Builder|\Illuminate\Database\Query\Builder $query
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
|
||||
public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
|
||||
{
|
||||
|
||||
$this->currentAction = $action;
|
||||
$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')
|
||||
$q = $query->where(function ($query) use ($tableDetails, $action) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
|
||||
$permissionQuery->select(['role_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('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
->where('action', '=', $action)
|
||||
->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where(function (QueryBuilder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$this->clean();
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add conditions to a query to filter the selection to related entities
|
||||
* where permissions are granted.
|
||||
* @param $entityType
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $entityIdColumn
|
||||
* @return mixed
|
||||
* where view permissions are granted.
|
||||
*/
|
||||
public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn)
|
||||
public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder
|
||||
{
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
$morphClass = app($entityClass)->getMorphClass();
|
||||
|
||||
$pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass();
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
|
||||
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
|
||||
$q = $query->where(function ($query) use ($tableDetails, $morphClass) {
|
||||
$query->where(function ($query) use (&$tableDetails, $morphClass) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $morphClass) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where('entity_type', '=', $pageMorphClass)
|
||||
->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('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
->where('entity_type', '=', $morphClass)
|
||||
->where('action', '=', 'view')
|
||||
->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where(function (QueryBuilder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||
});
|
||||
|
||||
$this->clean();
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user
|
||||
* @return \BookStack\Auth\User
|
||||
* Add the query for checking the given user id has permission
|
||||
* within the join_permissions table.
|
||||
* @param QueryBuilder|Builder $query
|
||||
*/
|
||||
private function currentUser()
|
||||
protected function addJointHasPermissionCheck($query, int $userIdToCheck)
|
||||
{
|
||||
if ($this->currentUserModel === false) {
|
||||
$query->where('has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('owned_by', '=', $userIdToCheck);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user
|
||||
*/
|
||||
private function currentUser(): User
|
||||
{
|
||||
if (is_null($this->currentUserModel)) {
|
||||
$this->currentUserModel = user();
|
||||
}
|
||||
|
||||
@@ -775,10 +659,9 @@ class PermissionService
|
||||
/**
|
||||
* Clean the cached user elements.
|
||||
*/
|
||||
private function clean()
|
||||
private function clean(): void
|
||||
{
|
||||
$this->currentUserModel = false;
|
||||
$this->userRoles = false;
|
||||
$this->isAdminUser = null;
|
||||
$this->currentUserModel = null;
|
||||
$this->userRoles = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class Role extends Model implements Loggable
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible roles
|
||||
* Get all visible roles.
|
||||
*/
|
||||
public static function visible(): Collection
|
||||
{
|
||||
@@ -104,7 +104,10 @@ class Role extends Model implements Loggable
|
||||
*/
|
||||
public static function restrictable(): Collection
|
||||
{
|
||||
return static::query()->where('system_name', '!=', 'admin')->get();
|
||||
return static::query()
|
||||
->where('system_name', '!=', 'admin')
|
||||
->orderBy('display_name', 'asc')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?php namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Actions\Favourite;
|
||||
use BookStack\Api\ApiToken;
|
||||
use BookStack\Entities\Tools\SlugGenerator;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Interfaces\Sluggable;
|
||||
use BookStack\Model;
|
||||
use BookStack\Notifications\ResetPassword;
|
||||
use BookStack\Uploads\Image;
|
||||
@@ -22,6 +25,7 @@ use Illuminate\Support\Collection;
|
||||
* Class User
|
||||
* @property string $id
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property string $email
|
||||
* @property string $password
|
||||
* @property Carbon $created_at
|
||||
@@ -30,8 +34,9 @@ use Illuminate\Support\Collection;
|
||||
* @property int $image_id
|
||||
* @property string $external_auth_id
|
||||
* @property string $system_name
|
||||
* @property Collection $roles
|
||||
*/
|
||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable
|
||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
|
||||
{
|
||||
use Authenticatable, CanResetPassword, Notifiable;
|
||||
|
||||
@@ -72,23 +77,21 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Returns the default public user.
|
||||
* @return User
|
||||
*/
|
||||
public static function getDefault()
|
||||
public static function getDefault(): User
|
||||
{
|
||||
if (!is_null(static::$defaultUser)) {
|
||||
return static::$defaultUser;
|
||||
}
|
||||
|
||||
static::$defaultUser = static::where('system_name', '=', 'public')->first();
|
||||
static::$defaultUser = static::query()->where('system_name', '=', 'public')->first();
|
||||
return static::$defaultUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is the default public user.
|
||||
* @return bool
|
||||
*/
|
||||
public function isDefault()
|
||||
public function isDefault(): bool
|
||||
{
|
||||
return $this->system_name === 'public';
|
||||
}
|
||||
@@ -115,12 +118,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Check if the user has a role.
|
||||
* @param $role
|
||||
* @return mixed
|
||||
*/
|
||||
public function hasSystemRole($role)
|
||||
public function hasSystemRole(string $roleSystemName): bool
|
||||
{
|
||||
return $this->roles->pluck('system_name')->contains($role);
|
||||
return $this->roles->pluck('system_name')->contains($roleSystemName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,9 +185,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Get the social account associated with this user.
|
||||
* @return HasMany
|
||||
*/
|
||||
public function socialAccounts()
|
||||
public function socialAccounts(): HasMany
|
||||
{
|
||||
return $this->hasMany(SocialAccount::class);
|
||||
}
|
||||
@@ -207,11 +207,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's avatar,
|
||||
* @param int $size
|
||||
* @return string
|
||||
* Returns a URL to the user's avatar
|
||||
*/
|
||||
public function getAvatar($size = 50)
|
||||
public function getAvatar(int $size = 50): string
|
||||
{
|
||||
$default = url('/user_avatar.png');
|
||||
$imageId = $this->image_id;
|
||||
@@ -229,9 +227,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Get the avatar for the user.
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function avatar()
|
||||
public function avatar(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
@@ -244,6 +241,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
return $this->hasMany(ApiToken::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the favourite instances for this user.
|
||||
*/
|
||||
public function favourites(): HasMany
|
||||
{
|
||||
return $this->hasMany(Favourite::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last activity time for this user.
|
||||
*/
|
||||
@@ -271,15 +276,13 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getProfileUrl(): string
|
||||
{
|
||||
return url('/user/' . $this->id);
|
||||
return url('/user/' . $this->slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a shortened version of the user's name.
|
||||
* @param int $chars
|
||||
* @return string
|
||||
*/
|
||||
public function getShortName($chars = 8)
|
||||
public function getShortName(int $chars = 8): string
|
||||
{
|
||||
if (mb_strlen($this->name) <= $chars) {
|
||||
return $this->name;
|
||||
@@ -310,4 +313,13 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
{
|
||||
return "({$this->id}) {$this->name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function refreshSlug(): string
|
||||
{
|
||||
$this->slug = app(SlugGenerator::class)->generate($this);
|
||||
return $this->slug;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,11 @@ use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\UserUpdateException;
|
||||
use BookStack\Uploads\Image;
|
||||
use BookStack\Uploads\UserAvatars;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Images;
|
||||
use Log;
|
||||
|
||||
class UserRepo
|
||||
@@ -45,6 +43,14 @@ class UserRepo
|
||||
return User::query()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by their slug.
|
||||
*/
|
||||
public function getBySlug(string $slug): User
|
||||
{
|
||||
return User::query()->where('slug', '=', $slug)->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions.
|
||||
*/
|
||||
@@ -159,7 +165,13 @@ class UserRepo
|
||||
'email_confirmed' => $emailConfirmed,
|
||||
'external_auth_id' => $data['external_auth_id'] ?? '',
|
||||
];
|
||||
return User::query()->forceCreate($details);
|
||||
|
||||
$user = new User();
|
||||
$user->forceFill($details);
|
||||
$user->refreshSlug();
|
||||
$user->save();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,16 +182,11 @@ class UserRepo
|
||||
{
|
||||
$user->socialAccounts()->delete();
|
||||
$user->apiTokens()->delete();
|
||||
$user->favourites()->delete();
|
||||
$user->delete();
|
||||
|
||||
// Delete user profile images
|
||||
$profileImages = Image::query()->where('type', '=', 'user')
|
||||
->where('uploaded_to', '=', $user->id)
|
||||
->get();
|
||||
|
||||
foreach ($profileImages as $image) {
|
||||
Images::destroy($image);
|
||||
}
|
||||
$this->userAvatar->destroyAllForUser($user);
|
||||
|
||||
if (!empty($newOwnerId)) {
|
||||
$newOwner = User::query()->find($newOwnerId);
|
||||
|
||||
@@ -19,13 +19,6 @@ return [
|
||||
// private configuration variables so should remain disabled in public.
|
||||
'debug' => env('APP_DEBUG', false),
|
||||
|
||||
// Set the default view type for various lists. Can be overridden by user preferences.
|
||||
// These will be used for public viewers and users that have not set a preference.
|
||||
'views' => [
|
||||
'books' => env('APP_VIEWS_BOOKS', 'list'),
|
||||
'bookshelves' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
|
||||
],
|
||||
|
||||
// The number of revisions to keep in the database.
|
||||
// Once this limit is reached older revisions will be deleted.
|
||||
// If set to false then a limit will not be enforced.
|
||||
@@ -63,7 +56,7 @@ return [
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
|
||||
// Locales available
|
||||
'locales' => ['en', 'ar', 'bg', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
|
||||
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
|
||||
|
||||
// Application Fallback Locale
|
||||
'fallback_locale' => 'en',
|
||||
@@ -122,6 +115,7 @@ return [
|
||||
BookStack\Providers\TranslationServiceProvider::class,
|
||||
|
||||
// BookStack custom service providers
|
||||
BookStack\Providers\ThemeServiceProvider::class,
|
||||
BookStack\Providers\AuthServiceProvider::class,
|
||||
BookStack\Providers\AppServiceProvider::class,
|
||||
BookStack\Providers\BroadcastServiceProvider::class,
|
||||
@@ -190,11 +184,8 @@ return [
|
||||
|
||||
// Custom BookStack
|
||||
'Activity' => BookStack\Facades\Activity::class,
|
||||
'Setting' => BookStack\Facades\Setting::class,
|
||||
'Views' => BookStack\Facades\Views::class,
|
||||
'Images' => BookStack\Facades\Images::class,
|
||||
'Permissions' => BookStack\Facades\Permissions::class,
|
||||
|
||||
'Theme' => BookStack\Facades\Theme::class,
|
||||
],
|
||||
|
||||
// Proxy configuration
|
||||
|
||||
@@ -85,6 +85,7 @@ return [
|
||||
'database' => 'bookstack-test',
|
||||
'username' => env('MYSQL_USER', 'bookstack-test'),
|
||||
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
|
||||
'port' => $mysql_port,
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
|
||||
@@ -38,7 +38,7 @@ return [
|
||||
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
|
||||
* Symbol, ZapfDingbats.
|
||||
*/
|
||||
"DOMPDF_FONT_DIR" => app_path('vendor/dompdf/dompdf/lib/fonts/'), //storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||
"DOMPDF_FONT_DIR" => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||
|
||||
/**
|
||||
* The location of the DOMPDF font cache directory
|
||||
@@ -219,7 +219,7 @@ return [
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_JAVASCRIPT" => true,
|
||||
"DOMPDF_ENABLE_JAVASCRIPT" => false,
|
||||
|
||||
/**
|
||||
* Enable remote file access
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
return [
|
||||
|
||||
// Mail driver to use.
|
||||
// Options: smtp, mail, sendmail, log
|
||||
// Options: smtp, sendmail, log, array
|
||||
'driver' => env('MAIL_DRIVER', 'smtp'),
|
||||
|
||||
// SMTP host address
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
$SAML2_IDP_AUTHNCONTEXT = env('SAML2_IDP_AUTHNCONTEXT', true);
|
||||
|
||||
return [
|
||||
|
||||
// Display name, shown to users, for SAML2 option
|
||||
@@ -139,6 +141,14 @@ return [
|
||||
// )
|
||||
// ),
|
||||
],
|
||||
'security' => [
|
||||
// SAML2 Authn context
|
||||
// When set to false no AuthContext will be sent in the AuthNRequest,
|
||||
// When set to true (Default) you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'.
|
||||
// Multiple forced values can be passed via a space separated array, For example:
|
||||
// SAML2_IDP_AUTHNCONTEXT="urn:federation:authentication:windows urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
|
||||
'requestedAuthnContext' => is_string($SAML2_IDP_AUTHNCONTEXT) ? explode(' ', $SAML2_IDP_AUTHNCONTEXT) : $SAML2_IDP_AUTHNCONTEXT,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -132,6 +132,8 @@ return [
|
||||
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
|
||||
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
|
||||
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
|
||||
'start_tls' => env('LDAP_START_TLS', false),
|
||||
'thumbnail_attribute' => env('LDAP_THUMBNAIL_ATTRIBUTE', null),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -59,7 +59,7 @@ return [
|
||||
// The session cookie path determines the path for which the cookie will
|
||||
// be regarded as available. Typically, this will be the root path of
|
||||
// your application but you are free to change this when necessary.
|
||||
'path' => '/',
|
||||
'path' => '/' . (explode('/', env('APP_URL', ''), 4)[3] ?? ''),
|
||||
|
||||
// Session Cookie Domain
|
||||
// Here you may change the domain of the cookie used to identify a session
|
||||
|
||||
@@ -24,4 +24,12 @@ return [
|
||||
'app-custom-head' => false,
|
||||
'registration-enabled' => false,
|
||||
|
||||
// User-level default settings
|
||||
'user' => [
|
||||
'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false),
|
||||
'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
|
||||
'bookshelf_view_type' =>env('APP_VIEWS_BOOKSHELF', 'grid'),
|
||||
'books_view_type' => env('APP_VIEWS_BOOKS', 'grid'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearViews extends Command
|
||||
@@ -36,7 +37,7 @@ class ClearViews extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
\Views::resetAll();
|
||||
View::clearAll();
|
||||
$this->comment('Views cleared');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace BookStack\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateUrl extends Command
|
||||
{
|
||||
@@ -60,22 +61,50 @@ class UpdateUrl extends Command
|
||||
"attachments" => ["path"],
|
||||
"pages" => ["html", "text", "markdown"],
|
||||
"images" => ["url"],
|
||||
"settings" => ["value"],
|
||||
"comments" => ["html", "text"],
|
||||
];
|
||||
|
||||
foreach ($columnsToUpdateByTable as $table => $columns) {
|
||||
foreach ($columns as $column) {
|
||||
$changeCount = $this->db->table($table)->update([
|
||||
$column => $this->db->raw("REPLACE({$column}, '{$oldUrl}', '{$newUrl}')")
|
||||
]);
|
||||
$changeCount = $this->replaceValueInTable($table, $column, $oldUrl, $newUrl);
|
||||
$this->info("Updated {$changeCount} rows in {$table}->{$column}");
|
||||
}
|
||||
}
|
||||
|
||||
$jsonColumnsToUpdateByTable = [
|
||||
"settings" => ["value"],
|
||||
];
|
||||
|
||||
foreach ($jsonColumnsToUpdateByTable as $table => $columns) {
|
||||
foreach ($columns as $column) {
|
||||
$oldJson = trim(json_encode($oldUrl), '"');
|
||||
$newJson = trim(json_encode($newUrl), '"');
|
||||
$changeCount = $this->replaceValueInTable($table, $column, $oldJson, $newJson);
|
||||
$this->info("Updated {$changeCount} JSON encoded rows in {$table}->{$column}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("URL update procedure complete.");
|
||||
$this->info('============================================================================');
|
||||
$this->info('Be sure to run "php artisan cache:clear" to clear any old URLs in the cache.');
|
||||
$this->info('============================================================================');
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a find+replace operations in the provided table and column.
|
||||
* Returns the count of rows changed.
|
||||
*/
|
||||
protected function replaceValueInTable(string $table, string $column, string $oldUrl, string $newUrl): int
|
||||
{
|
||||
$oldQuoted = $this->db->getPdo()->quote($oldUrl);
|
||||
$newQuoted = $this->db->getPdo()->quote($newUrl);
|
||||
return $this->db->table($table)->update([
|
||||
$column => $this->db->raw("REPLACE({$column}, {$oldQuoted}, {$newQuoted})")
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Warn the user of the dangers of this operation.
|
||||
* Returns a boolean indicating if they've accepted the warnings.
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<?php namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
@@ -49,7 +46,7 @@ abstract class BookChild extends Entity
|
||||
|
||||
// Update all child pages if a chapter
|
||||
if ($this instanceof Chapter) {
|
||||
foreach ($this->pages as $page) {
|
||||
foreach ($this->pages()->withTrashed()->get() as $page) {
|
||||
$page->changeBook($newBookId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use BookStack\Actions\Activity;
|
||||
use BookStack\Actions\Comment;
|
||||
use BookStack\Actions\Favourite;
|
||||
use BookStack\Actions\Tag;
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Auth\Permissions\EntityPermission;
|
||||
@@ -9,6 +10,9 @@ use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Entities\Tools\SearchIndex;
|
||||
use BookStack\Entities\Tools\SlugGenerator;
|
||||
use BookStack\Facades\Permissions;
|
||||
use BookStack\Interfaces\Favouritable;
|
||||
use BookStack\Interfaces\Sluggable;
|
||||
use BookStack\Interfaces\Viewable;
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use BookStack\Traits\HasOwner;
|
||||
@@ -37,7 +41,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @method static Builder withLastView()
|
||||
* @method static Builder withViewCount()
|
||||
*/
|
||||
abstract class Entity extends Model
|
||||
abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||
{
|
||||
use SoftDeletes;
|
||||
use HasCreatorAndUpdater;
|
||||
@@ -289,11 +293,29 @@ abstract class Entity extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and set a new URL slug for this model.
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function refreshSlug(): string
|
||||
{
|
||||
$this->slug = (new SlugGenerator)->generate($this);
|
||||
$this->slug = app(SlugGenerator::class)->generate($this);
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function favourites(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Favourite::class, 'favouritable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the entity is a favourite of the current user.
|
||||
*/
|
||||
public function isFavourite(): bool
|
||||
{
|
||||
return $this->favourites()
|
||||
->where('user_id', '=', user()->id)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class Page extends BookChild
|
||||
*/
|
||||
public function scopeVisible(Builder $query): Builder
|
||||
{
|
||||
$query = Permissions::enforceDraftVisiblityOnQuery($query);
|
||||
$query = Permissions::enforceDraftVisibilityOnQuery($query);
|
||||
return parent::scopeVisible($query);
|
||||
}
|
||||
|
||||
@@ -75,11 +75,23 @@ class Page extends BookChild
|
||||
|
||||
/**
|
||||
* Get the associated page revisions, ordered by created date.
|
||||
* @return mixed
|
||||
* Only provides actual saved page revision instances, Not drafts.
|
||||
*/
|
||||
public function revisions()
|
||||
public function revisions(): HasMany
|
||||
{
|
||||
return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc')->orderBy('id', 'desc');
|
||||
return $this->allRevisions()
|
||||
->where('type', '=', 'version')
|
||||
->orderBy('created_at', 'desc')
|
||||
->orderBy('id', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all revision instances assigned to this page.
|
||||
* Includes all types of revisions.
|
||||
*/
|
||||
public function allRevisions(): HasMany
|
||||
{
|
||||
return $this->hasMany(PageRevision::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
17
app/Entities/Queries/EntityQuery.php
Normal file
17
app/Entities/Queries/EntityQuery.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php namespace BookStack\Entities\Queries;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
|
||||
abstract class EntityQuery
|
||||
{
|
||||
protected function permissionService(): PermissionService
|
||||
{
|
||||
return app()->make(PermissionService::class);
|
||||
}
|
||||
|
||||
protected function entityProvider(): EntityProvider
|
||||
{
|
||||
return app()->make(EntityProvider::class);
|
||||
}
|
||||
}
|
||||
29
app/Entities/Queries/Popular.php
Normal file
29
app/Entities/Queries/Popular.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php namespace BookStack\Entities\Queries;
|
||||
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Popular extends EntityQuery
|
||||
{
|
||||
public function run(int $count, int $page, array $filterModels = null, string $action = 'view')
|
||||
{
|
||||
$query = $this->permissionService()
|
||||
->filterRestrictedEntityRelations(View::query(), 'views', 'viewable_id', 'viewable_type', $action)
|
||||
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
|
||||
if ($filterModels) {
|
||||
$query->whereIn('viewable_type', $this->entityProvider()->getMorphClasses($filterModels));
|
||||
}
|
||||
|
||||
return $query->with('viewable')
|
||||
->skip($count * ($page - 1))
|
||||
->take($count)
|
||||
->get()
|
||||
->pluck('viewable')
|
||||
->filter();
|
||||
}
|
||||
|
||||
}
|
||||
32
app/Entities/Queries/RecentlyViewed.php
Normal file
32
app/Entities/Queries/RecentlyViewed.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php namespace BookStack\Entities\Queries;
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class RecentlyViewed extends EntityQuery
|
||||
{
|
||||
public function run(int $count, int $page): Collection
|
||||
{
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = $this->permissionService()->filterRestrictedEntityRelations(
|
||||
View::query(),
|
||||
'views',
|
||||
'viewable_id',
|
||||
'viewable_type',
|
||||
'view'
|
||||
)
|
||||
->orderBy('views.updated_at', 'desc')
|
||||
->where('user_id', '=', user()->id);
|
||||
|
||||
return $query->with('viewable')
|
||||
->skip(($page - 1) * $count)
|
||||
->take($count)
|
||||
->get()
|
||||
->pluck('viewable')
|
||||
->filter();
|
||||
}
|
||||
}
|
||||
33
app/Entities/Queries/TopFavourites.php
Normal file
33
app/Entities/Queries/TopFavourites.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php namespace BookStack\Entities\Queries;
|
||||
|
||||
use BookStack\Actions\Favourite;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
|
||||
class TopFavourites extends EntityQuery
|
||||
{
|
||||
public function run(int $count, int $skip = 0)
|
||||
{
|
||||
$user = user();
|
||||
if (is_null($user) || $user->isDefault()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = $this->permissionService()
|
||||
->filterRestrictedEntityRelations(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type', 'view')
|
||||
->select('favourites.*')
|
||||
->leftJoin('views', function (JoinClause $join) {
|
||||
$join->on('favourites.favouritable_id', '=', 'views.viewable_id');
|
||||
$join->on('favourites.favouritable_type', '=', 'views.viewable_type');
|
||||
$join->where('views.user_id', '=', user()->id);
|
||||
})
|
||||
->orderBy('views.views', 'desc')
|
||||
->where('favourites.user_id', '=', user()->id);
|
||||
|
||||
return $query->with('favouritable')
|
||||
->skip($skip)
|
||||
->take($count)
|
||||
->get()
|
||||
->pluck('favouritable')
|
||||
->filter();
|
||||
}
|
||||
}
|
||||
@@ -177,25 +177,24 @@ class PageRepo
|
||||
// Hold the old details to compare later
|
||||
$oldHtml = $page->html;
|
||||
$oldName = $page->name;
|
||||
$oldMarkdown = $page->markdown;
|
||||
|
||||
$this->updateTemplateStatusAndContentFromInput($page, $input);
|
||||
$this->baseRepo->update($page, $input);
|
||||
|
||||
// Update with new details
|
||||
$page->revision_count++;
|
||||
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$page->markdown = '';
|
||||
}
|
||||
|
||||
$page->save();
|
||||
|
||||
// Remove all update drafts for this user & page.
|
||||
$this->getUserDraftQuery($page)->delete();
|
||||
|
||||
// Save a revision after updating
|
||||
$summary = $input['summary'] ?? null;
|
||||
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) {
|
||||
$summary = trim($input['summary'] ?? "");
|
||||
$htmlChanged = isset($input['html']) && $input['html'] !== $oldHtml;
|
||||
$nameChanged = isset($input['name']) && $input['name'] !== $oldName;
|
||||
$markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown;
|
||||
if ($htmlChanged || $nameChanged || $markdownChanged || $summary) {
|
||||
$this->savePageRevision($page, $summary);
|
||||
}
|
||||
|
||||
@@ -213,7 +212,7 @@ class PageRepo
|
||||
if (!empty($input['markdown'] ?? '')) {
|
||||
$pageContent->setNewMarkdown($input['markdown']);
|
||||
} else {
|
||||
$pageContent->setNewHTML($input['html']);
|
||||
$pageContent->setNewHTML($input['html'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,10 +223,6 @@ class PageRepo
|
||||
{
|
||||
$revision = new PageRevision($page->getAttributes());
|
||||
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$revision->markdown = '';
|
||||
}
|
||||
|
||||
$revision->page_id = $page->id;
|
||||
$revision->slug = $page->slug;
|
||||
$revision->book_slug = $page->book->slug;
|
||||
@@ -290,7 +285,13 @@ class PageRepo
|
||||
|
||||
$page->fill($revision->toArray());
|
||||
$content = new PageContent($page);
|
||||
$content->setNewHTML($revision->html);
|
||||
|
||||
if (!empty($revision->markdown)) {
|
||||
$content->setNewMarkdown($revision->markdown);
|
||||
} else {
|
||||
$content->setNewHTML($revision->html);
|
||||
}
|
||||
|
||||
$page->updated_by = user()->id;
|
||||
$page->refreshSlug();
|
||||
$page->save();
|
||||
|
||||
@@ -13,4 +13,4 @@ class CustomStrikeThroughExtension implements ExtensionInterface
|
||||
$environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
|
||||
$environment->addInlineRenderer(Strikethrough::class, new CustomStrikethroughRenderer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,4 @@ class CustomStrikethroughRenderer implements InlineRendererInterface
|
||||
|
||||
return new HtmlElement('s', $inline->getData('attributes', []), $htmlRenderer->renderInlines($inline->children()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
69
app/Entities/Tools/NextPreviousContentLocator.php
Normal file
69
app/Entities/Tools/NextPreviousContentLocator.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Finds the next or previous content of a book element (page or chapter).
|
||||
*/
|
||||
class NextPreviousContentLocator
|
||||
{
|
||||
protected $relativeBookItem;
|
||||
protected $flatTree;
|
||||
protected $currentIndex = null;
|
||||
|
||||
/**
|
||||
* NextPreviousContentLocator constructor.
|
||||
*/
|
||||
public function __construct(BookChild $relativeBookItem, Collection $bookTree)
|
||||
{
|
||||
$this->relativeBookItem = $relativeBookItem;
|
||||
$this->flatTree = $this->treeToFlatOrderedCollection($bookTree);
|
||||
$this->currentIndex = $this->getCurrentIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next logical entity within the book hierarchy.
|
||||
*/
|
||||
public function getNext(): ?Entity
|
||||
{
|
||||
return $this->flatTree->get($this->currentIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next logical entity within the book hierarchy.
|
||||
*/
|
||||
public function getPrevious(): ?Entity
|
||||
{
|
||||
return $this->flatTree->get($this->currentIndex - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of the current relative item.
|
||||
*/
|
||||
protected function getCurrentIndex(): ?int
|
||||
{
|
||||
$index = $this->flatTree->search(function (Entity $entity) {
|
||||
return get_class($entity) === get_class($this->relativeBookItem)
|
||||
&& $entity->id === $this->relativeBookItem->id;
|
||||
});
|
||||
return $index === false ? null : $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a book tree collection to a flattened version
|
||||
* where all items follow the expected order of user flow.
|
||||
*/
|
||||
protected function treeToFlatOrderedCollection(Collection $bookTree): Collection
|
||||
{
|
||||
$flatOrdered = collect();
|
||||
/** @var Entity $item */
|
||||
foreach ($bookTree->all() as $item) {
|
||||
$flatOrdered->push($item);
|
||||
$childPages = $item->visible_pages ?? [];
|
||||
$flatOrdered = $flatOrdered->concat($childPages);
|
||||
}
|
||||
return $flatOrdered;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,15 @@
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use BookStack\Util\HtmlContentFilter;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use DOMDocument;
|
||||
use DOMNodeList;
|
||||
use DOMXPath;
|
||||
use Illuminate\Support\Str;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Environment;
|
||||
use League\CommonMark\Extension\Table\TableExtension;
|
||||
@@ -28,6 +34,7 @@ class PageContent
|
||||
*/
|
||||
public function setNewHTML(string $html)
|
||||
{
|
||||
$html = $this->extractBase64Images($this->page, $html);
|
||||
$this->page->html = $this->formatHtml($html);
|
||||
$this->page->text = $this->toPlainText();
|
||||
$this->page->markdown = '';
|
||||
@@ -53,23 +60,70 @@ class PageContent
|
||||
$environment->addExtension(new TableExtension());
|
||||
$environment->addExtension(new TaskListExtension());
|
||||
$environment->addExtension(new CustomStrikeThroughExtension());
|
||||
$environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
|
||||
$converter = new CommonMarkConverter([], $environment);
|
||||
return $converter->convertToHtml($markdown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all base64 image data to saved images
|
||||
*/
|
||||
public function extractBase64Images(Page $page, string $htmlText): string
|
||||
{
|
||||
if (empty($htmlText) || strpos($htmlText, 'data:image') === false) {
|
||||
return $htmlText;
|
||||
}
|
||||
|
||||
$doc = $this->loadDocumentFromHtml($htmlText);
|
||||
$container = $doc->documentElement;
|
||||
$body = $container->childNodes->item(0);
|
||||
$childNodes = $body->childNodes;
|
||||
$xPath = new DOMXPath($doc);
|
||||
$imageRepo = app()->make(ImageRepo::class);
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
// Get all img elements with image data blobs
|
||||
$imageNodes = $xPath->query('//img[contains(@src, \'data:image\')]');
|
||||
foreach ($imageNodes as $imageNode) {
|
||||
$imageSrc = $imageNode->getAttribute('src');
|
||||
[$dataDefinition, $base64ImageData] = explode(',', $imageSrc, 2);
|
||||
$extension = strtolower(preg_split('/[\/;]/', $dataDefinition)[1] ?? 'png');
|
||||
|
||||
// Validate extension
|
||||
if (!in_array($extension, $allowedExtensions)) {
|
||||
$imageNode->setAttribute('src', '');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save image from data with a random name
|
||||
$imageName = 'embedded-image-' . Str::random(8) . '.' . $extension;
|
||||
try {
|
||||
$image = $imageRepo->saveNewFromData($imageName, base64_decode($base64ImageData), 'gallery', $page->id);
|
||||
$imageNode->setAttribute('src', $image->url);
|
||||
} catch (ImageUploadException $exception) {
|
||||
$imageNode->setAttribute('src', '');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate inner html as a string
|
||||
$html = '';
|
||||
foreach ($childNodes as $childNode) {
|
||||
$html .= $doc->saveHTML($childNode);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a page's html to be tagged correctly within the system.
|
||||
*/
|
||||
protected function formatHtml(string $htmlText): string
|
||||
{
|
||||
if ($htmlText == '') {
|
||||
if (empty($htmlText)) {
|
||||
return $htmlText;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
$doc = $this->loadDocumentFromHtml($htmlText);
|
||||
$container = $doc->documentElement;
|
||||
$body = $container->childNodes->item(0);
|
||||
$childNodes = $body->childNodes;
|
||||
@@ -108,7 +162,7 @@ class PageContent
|
||||
protected function updateLinks(DOMXPath $xpath, string $old, string $new)
|
||||
{
|
||||
$old = str_replace('"', '', $old);
|
||||
$matchingLinks = $xpath->query('//body//*//*[@href="'.$old.'"]');
|
||||
$matchingLinks = $xpath->query('//body//*//*[@href="' . $old . '"]');
|
||||
foreach ($matchingLinks as $domElem) {
|
||||
$domElem->setAttribute('href', $new);
|
||||
}
|
||||
@@ -161,12 +215,12 @@ class PageContent
|
||||
/**
|
||||
* Render the page for viewing
|
||||
*/
|
||||
public function render(bool $blankIncludes = false) : string
|
||||
public function render(bool $blankIncludes = false): string
|
||||
{
|
||||
$content = $this->page->html;
|
||||
|
||||
if (!config('app.allow_content_scripts')) {
|
||||
$content = $this->escapeScripts($content);
|
||||
$content = HtmlContentFilter::removeScripts($content);
|
||||
}
|
||||
|
||||
if ($blankIncludes) {
|
||||
@@ -187,9 +241,7 @@ class PageContent
|
||||
return [];
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($htmlContent, 'HTML-ENTITIES', 'UTF-8'));
|
||||
$doc = $this->loadDocumentFromHtml($htmlContent);
|
||||
$xPath = new DOMXPath($doc);
|
||||
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
|
||||
|
||||
@@ -229,7 +281,7 @@ class PageContent
|
||||
/**
|
||||
* Remove any page include tags within the given HTML.
|
||||
*/
|
||||
protected function blankPageIncludes(string $html) : string
|
||||
protected function blankPageIncludes(string $html): string
|
||||
{
|
||||
return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
|
||||
}
|
||||
@@ -237,7 +289,7 @@ class PageContent
|
||||
/**
|
||||
* Parse any include tags "{{@<page_id>#section}}" to be part of the page.
|
||||
*/
|
||||
protected function parsePageIncludes(string $html) : string
|
||||
protected function parsePageIncludes(string $html): string
|
||||
{
|
||||
$matches = [];
|
||||
preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
|
||||
@@ -280,9 +332,7 @@ class PageContent
|
||||
protected function fetchSectionOfPage(Page $page, string $sectionId): string
|
||||
{
|
||||
$topLevelTags = ['table', 'ul', 'ol'];
|
||||
$doc = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
||||
$doc = $this->loadDocumentFromHtml($page->html);
|
||||
|
||||
// Search included content for the id given and blank out if not exists.
|
||||
$matchingElem = $doc->getElementById($sectionId);
|
||||
@@ -307,63 +357,14 @@ class PageContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape script tags within HTML content.
|
||||
* Create and load a DOMDocument from the given html content.
|
||||
*/
|
||||
protected function escapeScripts(string $html) : string
|
||||
protected function loadDocumentFromHtml(string $html): DOMDocument
|
||||
{
|
||||
if (empty($html)) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$html = '<body>' . $html . '</body>';
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
$xPath = new DOMXPath($doc);
|
||||
|
||||
// Remove standard script tags
|
||||
$scriptElems = $xPath->query('//script');
|
||||
foreach ($scriptElems as $scriptElem) {
|
||||
$scriptElem->parentNode->removeChild($scriptElem);
|
||||
}
|
||||
|
||||
// Remove clickable links to JavaScript URI
|
||||
$badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
|
||||
foreach ($badLinks as $badLink) {
|
||||
$badLink->parentNode->removeChild($badLink);
|
||||
}
|
||||
|
||||
// Remove forms with calls to JavaScript URI
|
||||
$badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
|
||||
foreach ($badForms as $badForm) {
|
||||
$badForm->parentNode->removeChild($badForm);
|
||||
}
|
||||
|
||||
// Remove meta tag to prevent external redirects
|
||||
$metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
|
||||
foreach ($metaTags as $metaTag) {
|
||||
$metaTag->parentNode->removeChild($metaTag);
|
||||
}
|
||||
|
||||
// Remove data or JavaScript iFrames
|
||||
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
|
||||
foreach ($badIframes as $badIframe) {
|
||||
$badIframe->parentNode->removeChild($badIframe);
|
||||
}
|
||||
|
||||
// Remove 'on*' attributes
|
||||
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
|
||||
foreach ($onAttributes as $attr) {
|
||||
/** @var \DOMAttr $attr*/
|
||||
$attrName = $attr->nodeName;
|
||||
$attr->parentNode->removeAttribute($attrName);
|
||||
}
|
||||
|
||||
$html = '';
|
||||
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
|
||||
foreach ($topElems as $child) {
|
||||
$html .= $doc->saveHTML($child);
|
||||
}
|
||||
|
||||
return $html;
|
||||
return $doc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,5 +137,4 @@ class SearchOptions
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use Illuminate\Database\Connection;
|
||||
@@ -178,7 +179,7 @@ class SearchRunner
|
||||
}
|
||||
}
|
||||
|
||||
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
|
||||
return $this->permissionService->enforceEntityRestrictions($entity, $entitySelect, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,24 +271,29 @@ class SearchRunner
|
||||
|
||||
protected function filterCreatedBy(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') {
|
||||
return;
|
||||
$userSlug = $input === 'me' ? user()->slug : trim($input);
|
||||
$user = User::query()->where('slug', '=', $userSlug)->first(['id']);
|
||||
if ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
}
|
||||
if ($input === 'me') {
|
||||
$input = user()->id;
|
||||
}
|
||||
$query->where('created_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBy(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') {
|
||||
return;
|
||||
$userSlug = $input === 'me' ? user()->slug : trim($input);
|
||||
$user = User::query()->where('slug', '=', $userSlug)->first(['id']);
|
||||
if ($user) {
|
||||
$query->where('updated_by', '=', $user->id);
|
||||
}
|
||||
if ($input === 'me') {
|
||||
$input = user()->id;
|
||||
}
|
||||
|
||||
protected function filterOwnedBy(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$userSlug = $input === 'me' ? user()->slug : trim($input);
|
||||
$user = User::query()->where('slug', '=', $userSlug)->first(['id']);
|
||||
if ($user) {
|
||||
$query->where('owned_by', '=', $user->id);
|
||||
}
|
||||
$query->where('updated_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterInName(EloquentBuilder $query, Entity $model, $input)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Interfaces\Sluggable;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SlugGenerator
|
||||
@@ -10,11 +11,11 @@ class SlugGenerator
|
||||
* Generate a fresh slug for the given entity.
|
||||
* The slug will generated so it does not conflict within the same parent item.
|
||||
*/
|
||||
public function generate(Entity $entity): string
|
||||
public function generate(Sluggable $model): string
|
||||
{
|
||||
$slug = $this->formatNameAsSlug($entity->name);
|
||||
while ($this->slugInUse($slug, $entity)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
$slug = $this->formatNameAsSlug($model->name);
|
||||
while ($this->slugInUse($slug, $model)) {
|
||||
$slug .= '-' . Str::random(3);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
@@ -35,16 +36,16 @@ class SlugGenerator
|
||||
* Check if a slug is already in-use for this
|
||||
* type of model within the same parent.
|
||||
*/
|
||||
protected function slugInUse(string $slug, Entity $entity): bool
|
||||
protected function slugInUse(string $slug, Sluggable $model): bool
|
||||
{
|
||||
$query = $entity->newQuery()->where('slug', '=', $slug);
|
||||
$query = $model->newQuery()->where('slug', '=', $slug);
|
||||
|
||||
if ($entity instanceof BookChild) {
|
||||
$query->where('book_id', '=', $entity->book_id);
|
||||
if ($model instanceof BookChild) {
|
||||
$query->where('book_id', '=', $model->book_id);
|
||||
}
|
||||
|
||||
if ($entity->id) {
|
||||
$query->where('id', '!=', $entity->id);
|
||||
if ($model->id) {
|
||||
$query->where('id', '!=', $model->id);
|
||||
}
|
||||
|
||||
return $query->count() > 0;
|
||||
|
||||
@@ -151,6 +151,7 @@ class TrashCan
|
||||
protected function destroyPage(Page $page): int
|
||||
{
|
||||
$this->destroyCommonRelations($page);
|
||||
$page->allRevisions()->delete();
|
||||
|
||||
// Delete Attached Files
|
||||
$attachmentService = app(AttachmentService::class);
|
||||
@@ -273,11 +274,11 @@ class TrashCan
|
||||
$count++;
|
||||
};
|
||||
|
||||
if ($entity->isA('chapter') || $entity->isA('book')) {
|
||||
if ($entity instanceof Chapter || $entity instanceof Book) {
|
||||
$entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
|
||||
}
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity instanceof Book) {
|
||||
$entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
|
||||
}
|
||||
|
||||
@@ -286,19 +287,20 @@ class TrashCan
|
||||
|
||||
/**
|
||||
* Destroy the given entity.
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function destroyEntity(Entity $entity): int
|
||||
{
|
||||
if ($entity->isA('page')) {
|
||||
if ($entity instanceof Page) {
|
||||
return $this->destroyPage($entity);
|
||||
}
|
||||
if ($entity->isA('chapter')) {
|
||||
if ($entity instanceof Chapter) {
|
||||
return $this->destroyChapter($entity);
|
||||
}
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity instanceof Book) {
|
||||
return $this->destroyBook($entity);
|
||||
}
|
||||
if ($entity->isA('shelf')) {
|
||||
if ($entity instanceof Bookshelf) {
|
||||
return $this->destroyShelf($entity);
|
||||
}
|
||||
}
|
||||
@@ -316,6 +318,7 @@ class TrashCan
|
||||
$entity->jointPermissions()->delete();
|
||||
$entity->searchTerms()->delete();
|
||||
$entity->deletions()->delete();
|
||||
$entity->favourites()->delete();
|
||||
|
||||
if ($entity instanceof HasCoverImage && $entity->cover) {
|
||||
$imageService = app()->make(ImageService::class);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
class ApiAuthException extends UnauthorizedException {
|
||||
class ApiAuthException extends UnauthorizedException
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,49 +3,52 @@
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* A list of the exception types that should not be reported.
|
||||
* A list of the exception types that are not reported.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dontReport = [
|
||||
AuthorizationException::class,
|
||||
HttpException::class,
|
||||
ModelNotFoundException::class,
|
||||
ValidationException::class,
|
||||
NotFoundException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Report or log an exception.
|
||||
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
|
||||
* A list of the inputs that are never flashed for validation exceptions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Report or log an exception.
|
||||
*
|
||||
* @param Exception $exception
|
||||
* @return void
|
||||
*
|
||||
* @param \Exception $e
|
||||
* @return mixed
|
||||
* @throws Exception
|
||||
*/
|
||||
public function report(Exception $e)
|
||||
public function report(Exception $exception)
|
||||
{
|
||||
return parent::report($e);
|
||||
parent::report($exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $e
|
||||
* @param Exception $e
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function render($request, Exception $e)
|
||||
@@ -54,29 +57,6 @@ class Handler extends ExceptionHandler
|
||||
return $this->renderApiException($e);
|
||||
}
|
||||
|
||||
// Handle notify exceptions which will redirect to the
|
||||
// specified location then show a notification message.
|
||||
if ($this->isExceptionType($e, NotifyException::class)) {
|
||||
$message = $this->getOriginalMessage($e);
|
||||
if (!empty($message)) {
|
||||
session()->flash('error', $message);
|
||||
}
|
||||
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 ($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);
|
||||
}
|
||||
|
||||
// Handle 404 errors with a loaded session to enable showing user-specific information
|
||||
if ($this->isExceptionType($e, NotFoundHttpException::class)) {
|
||||
return \Route::respondWithRoute('fallback');
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
|
||||
@@ -115,35 +95,6 @@ class Handler extends ExceptionHandler
|
||||
return new JsonResponse($responseData, $code, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
||||
@@ -22,4 +22,4 @@ class JsonDebugException extends Exception
|
||||
{
|
||||
return response()->json($this->data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ class NotFoundException extends PrettyException
|
||||
|
||||
/**
|
||||
* NotFoundException constructor.
|
||||
* @param string $message
|
||||
*/
|
||||
public function __construct($message = 'Item not found')
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class NotifyException extends \Exception
|
||||
{
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
|
||||
class NotifyException extends Exception implements Responsable
|
||||
{
|
||||
public $message;
|
||||
public $redirectLocation;
|
||||
|
||||
@@ -15,4 +17,19 @@ class NotifyException extends \Exception
|
||||
$this->redirectLocation = $redirectLocation;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the response for this type of exception.
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function toResponse($request)
|
||||
{
|
||||
$message = $this->getMessage();
|
||||
|
||||
if (!empty($message)) {
|
||||
session()->flash('error', $message);
|
||||
}
|
||||
|
||||
return redirect($this->redirectLocation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,43 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class PrettyException extends \Exception
|
||||
{
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
|
||||
class PrettyException extends Exception implements Responsable
|
||||
{
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
protected $subtitle = null;
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
protected $details = null;
|
||||
|
||||
/**
|
||||
* Render a response for when this exception occurs.
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function toResponse($request)
|
||||
{
|
||||
$code = ($this->getCode() === 0) ? 500 : $this->getCode();
|
||||
return response()->view('errors.' . $code, [
|
||||
'message' => $this->getMessage(),
|
||||
'subtitle' => $this->subtitle,
|
||||
'details' => $this->details,
|
||||
], $code);
|
||||
}
|
||||
|
||||
public function setSubtitle(string $subtitle): self
|
||||
{
|
||||
$this->subtitle = $subtitle;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDetails(string $details): self
|
||||
{
|
||||
$this->details = $details;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@ class UnauthorizedException extends Exception
|
||||
{
|
||||
parent::__construct($message, $code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Setting extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'setting';
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use BookStack\Theming\ThemeService;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Images extends Facade
|
||||
class Theme extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
@@ -11,6 +12,6 @@ class Images extends Facade
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'images';
|
||||
return ThemeService::class;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Views extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'views';
|
||||
}
|
||||
}
|
||||
@@ -27,4 +27,4 @@ abstract class ApiController extends Controller
|
||||
{
|
||||
return $this->rules;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +25,4 @@ class ApiDocsController extends ApiController
|
||||
$docs = ApiDocsGenerator::generateConsideringCache();
|
||||
return response()->json($docs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -91,4 +91,4 @@ class BookApiController extends ApiController
|
||||
$this->bookRepo->destroy($book);
|
||||
return response('', 204);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,4 +112,4 @@ class BookshelfApiController extends ApiController
|
||||
$this->bookshelfRepo->destroy($shelf);
|
||||
return response('', 204);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ class PageApiController extends ApiController
|
||||
*
|
||||
* Any HTML content provided should be kept to a single-block depth of plain HTML
|
||||
* elements to remain compatible with the BookStack front-end and editors.
|
||||
* Any images included via base64 data URIs will be extracted and saved as gallery
|
||||
* images against the page during upload.
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
|
||||
@@ -14,16 +14,14 @@ use Illuminate\Validation\ValidationException;
|
||||
class AttachmentController extends Controller
|
||||
{
|
||||
protected $attachmentService;
|
||||
protected $attachment;
|
||||
protected $pageRepo;
|
||||
|
||||
/**
|
||||
* AttachmentController constructor.
|
||||
*/
|
||||
public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo)
|
||||
public function __construct(AttachmentService $attachmentService, PageRepo $pageRepo)
|
||||
{
|
||||
$this->attachmentService = $attachmentService;
|
||||
$this->attachment = $attachment;
|
||||
$this->pageRepo = $pageRepo;
|
||||
}
|
||||
|
||||
@@ -67,7 +65,7 @@ class AttachmentController extends Controller
|
||||
'file' => 'required|file'
|
||||
]);
|
||||
|
||||
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
|
||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||
$this->checkOwnablePermission('view', $attachment->page);
|
||||
$this->checkOwnablePermission('page-update', $attachment->page);
|
||||
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||
@@ -89,7 +87,7 @@ class AttachmentController extends Controller
|
||||
*/
|
||||
public function getUpdateForm(string $attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||
|
||||
$this->checkOwnablePermission('page-update', $attachment->page);
|
||||
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||
@@ -104,8 +102,8 @@ class AttachmentController extends Controller
|
||||
*/
|
||||
public function update(Request $request, string $attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
|
||||
|
||||
/** @var Attachment $attachment */
|
||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||
try {
|
||||
$this->validate($request, [
|
||||
'attachment_edit_name' => 'required|string|min:1|max:255',
|
||||
@@ -160,7 +158,7 @@ class AttachmentController extends Controller
|
||||
|
||||
$attachmentName = $request->get('attachment_link_name');
|
||||
$link = $request->get('attachment_link_url');
|
||||
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
|
||||
$this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
|
||||
|
||||
return view('attachments.manager-link-form', [
|
||||
'pageId' => $pageId,
|
||||
@@ -202,9 +200,10 @@ class AttachmentController extends Controller
|
||||
* @throws FileNotFoundException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function get(string $attachmentId)
|
||||
public function get(Request $request, string $attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
/** @var Attachment $attachment */
|
||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||
try {
|
||||
$page = $this->pageRepo->getById($attachment->uploaded_to);
|
||||
} catch (NotFoundException $exception) {
|
||||
@@ -217,8 +216,13 @@ class AttachmentController extends Controller
|
||||
return redirect($attachment->path);
|
||||
}
|
||||
|
||||
$fileName = $attachment->getFileName();
|
||||
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
|
||||
return $this->downloadResponse($attachmentContents, $attachment->getFileName());
|
||||
|
||||
if ($request->get('open') === 'true') {
|
||||
return $this->inlineDownloadResponse($attachmentContents, $fileName);
|
||||
}
|
||||
return $this->downloadResponse($attachmentContents, $fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,7 +231,8 @@ class AttachmentController extends Controller
|
||||
*/
|
||||
public function delete(string $attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
/** @var Attachment $attachment */
|
||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||
$this->checkOwnablePermission('attachment-delete', $attachment);
|
||||
$this->attachmentService->deleteFile($attachment);
|
||||
return response()->json(['message' => trans('entities.attachments_deleted')]);
|
||||
|
||||
@@ -20,6 +20,7 @@ class AuditLogController extends Controller
|
||||
'sort' => $request->get('sort', 'created_at'),
|
||||
'date_from' => $request->get('date_from', ''),
|
||||
'date_to' => $request->get('date_to', ''),
|
||||
'user' => $request->get('user', ''),
|
||||
];
|
||||
|
||||
$query = Activity::query()
|
||||
@@ -34,6 +35,9 @@ class AuditLogController extends Controller
|
||||
if ($listDetails['event']) {
|
||||
$query->where('type', '=', $listDetails['event']);
|
||||
}
|
||||
if ($listDetails['user']) {
|
||||
$query->where('user_id', '=', $listDetails['user']);
|
||||
}
|
||||
|
||||
if ($listDetails['date_from']) {
|
||||
$query->where('created_at', '>=', $listDetails['date_from']);
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\EmailConfirmationService;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
use BookStack\Exceptions\UserTokenExpiredException;
|
||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Exception;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -80,6 +83,8 @@ class ConfirmEmailController extends Controller
|
||||
$user->save();
|
||||
|
||||
auth()->login($user);
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
|
||||
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
|
||||
$this->showSuccessNotification(trans('auth.email_confirm_success'));
|
||||
$this->emailConfirmationService->deleteByUser($user);
|
||||
|
||||
|
||||
@@ -7,9 +7,12 @@ use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||
use BookStack\Exceptions\LoginAttemptException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
@@ -150,6 +153,7 @@ class LoginController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
|
||||
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
|
||||
return redirect()->intended($this->redirectPath());
|
||||
}
|
||||
@@ -196,4 +200,18 @@ class LoginController extends Controller
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the failed login response instance.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
protected function sendFailedLoginResponse(Request $request)
|
||||
{
|
||||
throw ValidationException::withMessages([
|
||||
$this->username() => [trans('auth.failed')],
|
||||
])->redirectTo('/login');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\RegistrationService;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@@ -93,6 +96,8 @@ class RegisterController extends Controller
|
||||
try {
|
||||
$user = $this->registrationService->registerUser($userData);
|
||||
auth()->login($user);
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
|
||||
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
|
||||
} catch (UserRegistrationException $exception) {
|
||||
if ($exception->getMessage()) {
|
||||
$this->showErrorNotification($exception->getMessage());
|
||||
@@ -117,5 +122,4 @@ class RegisterController extends Controller
|
||||
'password' => Hash::make($data['password']),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -82,5 +82,4 @@ class Saml2Controller extends Controller
|
||||
|
||||
return redirect()->intended();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\RegistrationService;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||
|
||||
@@ -31,12 +32,11 @@ class SocialController extends Controller
|
||||
$this->registrationService = $registrationService;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Redirect to the relevant social site.
|
||||
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
|
||||
* @throws SocialDriverNotConfigured
|
||||
*/
|
||||
public function getSocialLogin(string $socialDriver)
|
||||
public function login(string $socialDriver)
|
||||
{
|
||||
session()->put('social-callback', 'login');
|
||||
return $this->socialAuthService->startLogIn($socialDriver);
|
||||
@@ -47,7 +47,7 @@ class SocialController extends Controller
|
||||
* @throws SocialDriverNotConfigured
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function socialRegister(string $socialDriver)
|
||||
public function register(string $socialDriver)
|
||||
{
|
||||
$this->registrationService->ensureRegistrationAllowed();
|
||||
session()->put('social-callback', 'register');
|
||||
@@ -60,7 +60,7 @@ class SocialController extends Controller
|
||||
* @throws SocialDriverNotConfigured
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function socialCallback(Request $request, string $socialDriver)
|
||||
public function callback(Request $request, string $socialDriver)
|
||||
{
|
||||
if (!session()->has('social-callback')) {
|
||||
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
|
||||
@@ -99,7 +99,7 @@ class SocialController extends Controller
|
||||
/**
|
||||
* Detach a social account from a user.
|
||||
*/
|
||||
public function detachSocialAccount(string $socialDriver)
|
||||
public function detach(string $socialDriver)
|
||||
{
|
||||
$this->socialAuthService->detachSocialAccount($socialDriver);
|
||||
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));
|
||||
@@ -113,7 +113,7 @@ class SocialController extends Controller
|
||||
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
|
||||
{
|
||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
|
||||
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
|
||||
$socialAccount = $this->socialAuthService->newSocialAccount($socialDriver, $socialUser);
|
||||
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
|
||||
|
||||
// Create an array of the user data to create a new user instance
|
||||
@@ -130,6 +130,8 @@ class SocialController extends Controller
|
||||
|
||||
$user = $this->registrationService->registerUser($userData, $socialAccount, $emailVerified);
|
||||
auth()->login($user);
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $user);
|
||||
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
|
||||
|
||||
$this->showSuccessNotification(trans('auth.register_success'));
|
||||
return redirect('/');
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\UserInviteService;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\UserTokenExpiredException;
|
||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Exception;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -68,6 +71,8 @@ class UserInviteController extends Controller
|
||||
$user->save();
|
||||
|
||||
auth()->login($user);
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
|
||||
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
|
||||
$this->showSuccessNotification(trans('auth.user_invite_success', ['appName' => setting('app-name')]));
|
||||
$this->inviteService->deleteByUser($user);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use Activity;
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
@@ -11,7 +12,6 @@ use BookStack\Exceptions\ImageUploadException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
use Views;
|
||||
|
||||
class BookController extends Controller
|
||||
{
|
||||
@@ -30,7 +30,7 @@ class BookController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$view = setting()->getForCurrentUser('books_view_type', config('app.views.books'));
|
||||
$view = setting()->getForCurrentUser('books_view_type');
|
||||
$sort = setting()->getForCurrentUser('books_sort', 'name');
|
||||
$order = setting()->getForCurrentUser('books_sort_order', 'asc');
|
||||
|
||||
@@ -112,7 +112,7 @@ class BookController extends Controller
|
||||
$bookChildren = (new BookContents($book))->getTree(true);
|
||||
$bookParentShelves = $book->shelves()->visible()->get();
|
||||
|
||||
Views::add($book);
|
||||
View::incrementFor($book);
|
||||
if ($request->has('shelf')) {
|
||||
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Entities\Tools\ShelfContext;
|
||||
@@ -32,7 +33,7 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$view = setting()->getForCurrentUser('bookshelves_view_type', config('app.views.bookshelves', 'grid'));
|
||||
$view = setting()->getForCurrentUser('bookshelves_view_type');
|
||||
$sort = setting()->getForCurrentUser('bookshelves_sort', 'name');
|
||||
$order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc');
|
||||
$sortOptions = [
|
||||
@@ -101,15 +102,26 @@ class BookshelfController extends Controller
|
||||
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('book-view', $shelf);
|
||||
|
||||
Views::add($shelf);
|
||||
$sort = setting()->getForCurrentUser('shelf_books_sort', 'default');
|
||||
$order = setting()->getForCurrentUser('shelf_books_sort_order', 'asc');
|
||||
|
||||
$sortedVisibleShelfBooks = $shelf->visibleBooks()->get()
|
||||
->sortBy($sort === 'default' ? 'pivot.order' : $sort, SORT_REGULAR, $order === 'desc')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
View::incrementFor($shelf);
|
||||
$this->entityContextManager->setShelfContext($shelf->id);
|
||||
$view = setting()->getForCurrentUser('bookshelf_view_type', config('app.views.books'));
|
||||
$view = setting()->getForCurrentUser('bookshelf_view_type');
|
||||
|
||||
$this->setPageTitle($shelf->getShortName());
|
||||
return view('shelves.show', [
|
||||
'shelf' => $shelf,
|
||||
'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,
|
||||
'view' => $view,
|
||||
'activity' => Activity::entityActivity($shelf, 20, 1)
|
||||
'activity' => Activity::entityActivity($shelf, 20, 1),
|
||||
'order' => $order,
|
||||
'sort' => $sort
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Repos\ChapterRepo;
|
||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\MoveOperationException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
use Views;
|
||||
|
||||
class ChapterController extends Controller
|
||||
{
|
||||
@@ -64,7 +65,8 @@ class ChapterController extends Controller
|
||||
|
||||
$sidebarTree = (new BookContents($chapter->book))->getTree();
|
||||
$pages = $chapter->getVisiblePages();
|
||||
Views::add($chapter);
|
||||
$nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree);
|
||||
View::incrementFor($chapter);
|
||||
|
||||
$this->setPageTitle($chapter->getShortName());
|
||||
return view('chapters.show', [
|
||||
@@ -72,7 +74,9 @@ class ChapterController extends Controller
|
||||
'chapter' => $chapter,
|
||||
'current' => $chapter,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'pages' => $pages
|
||||
'pages' => $pages,
|
||||
'next' => $nextPreviousLocator->getNext(),
|
||||
'previous' => $nextPreviousLocator->getPrevious(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use BookStack\Facades\Activity;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\HasCreatorAndUpdater;
|
||||
use BookStack\Model;
|
||||
use finfo;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
@@ -121,6 +122,20 @@ abstract class Controller extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file download response that provides the file with a content-type
|
||||
* correct for the file, in a way so the browser can show the content in browser.
|
||||
*/
|
||||
protected function inlineDownloadResponse(string $content, string $fileName): Response
|
||||
{
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->buffer($content) ?: 'application/octet-stream';
|
||||
return response()->make($content, 200, [
|
||||
'Content-Type' => $mime,
|
||||
'Content-Disposition' => 'inline; filename="' . $fileName . '"'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a positive, successful notification to the user on next view load.
|
||||
*/
|
||||
@@ -159,6 +174,6 @@ abstract class Controller extends BaseController
|
||||
*/
|
||||
protected function getImageValidationRules(): string
|
||||
{
|
||||
return 'image_extension|no_double_extension|mimes:jpeg,png,gif,webp';
|
||||
return 'image_extension|mimes:jpeg,png,gif,webp';
|
||||
}
|
||||
}
|
||||
|
||||
95
app/Http/Controllers/FavouriteController.php
Normal file
95
app/Http/Controllers/FavouriteController.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Queries\TopFavourites;
|
||||
use BookStack\Interfaces\Favouritable;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FavouriteController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show a listing of all favourite items for the current user.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$viewCount = 20;
|
||||
$page = intval($request->get('page', 1));
|
||||
$favourites = (new TopFavourites)->run($viewCount + 1, (($page - 1) * $viewCount));
|
||||
|
||||
$hasMoreLink = ($favourites->count() > $viewCount) ? url("/favourites?page=" . ($page+1)) : null;
|
||||
|
||||
return view('common.detailed-listing-with-more', [
|
||||
'title' => trans('entities.my_favourites'),
|
||||
'entities' => $favourites->slice(0, $viewCount),
|
||||
'hasMoreLink' => $hasMoreLink,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new item as a favourite.
|
||||
*/
|
||||
public function add(Request $request)
|
||||
{
|
||||
$favouritable = $this->getValidatedModelFromRequest($request);
|
||||
$favouritable->favourites()->firstOrCreate([
|
||||
'user_id' => user()->id,
|
||||
]);
|
||||
|
||||
$this->showSuccessNotification(trans('activities.favourite_add_notification', [
|
||||
'name' => $favouritable->name,
|
||||
]));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item as a favourite.
|
||||
*/
|
||||
public function remove(Request $request)
|
||||
{
|
||||
$favouritable = $this->getValidatedModelFromRequest($request);
|
||||
$favouritable->favourites()->where([
|
||||
'user_id' => user()->id,
|
||||
])->delete();
|
||||
|
||||
$this->showSuccessNotification(trans('activities.favourite_remove_notification', [
|
||||
'name' => $favouritable->name,
|
||||
]));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getValidatedModelFromRequest(Request $request): Favouritable
|
||||
{
|
||||
$modelInfo = $this->validate($request, [
|
||||
'type' => 'required|string',
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
if (!class_exists($modelInfo['type'])) {
|
||||
throw new \Exception('Model not found');
|
||||
}
|
||||
|
||||
/** @var Model $model */
|
||||
$model = new $modelInfo['type'];
|
||||
if (! $model instanceof Favouritable) {
|
||||
throw new \Exception('Model not favouritable');
|
||||
}
|
||||
|
||||
$modelInstance = $model->newQuery()
|
||||
->where('id', '=', $modelInfo['id'])
|
||||
->first(['id', 'name']);
|
||||
|
||||
$inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
|
||||
if (is_null($modelInstance) || $inaccessibleEntity) {
|
||||
throw new \Exception('Model instance not found');
|
||||
}
|
||||
|
||||
return $modelInstance;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
use Activity;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Queries\RecentlyViewed;
|
||||
use BookStack\Entities\Queries\TopFavourites;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\BookRepo;
|
||||
use BookStack\Entities\Repos\BookshelfRepo;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
||||
class HomeController extends Controller
|
||||
@@ -32,12 +33,13 @@ class HomeController extends Controller
|
||||
|
||||
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
|
||||
$recents = $this->isSignedIn() ?
|
||||
Views::getUserRecentlyViewed(12*$recentFactor, 1)
|
||||
(new RecentlyViewed)->run(12*$recentFactor, 1)
|
||||
: Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
|
||||
$favourites = (new TopFavourites)->run(6);
|
||||
$recentlyUpdatedPages = Page::visible()->with('book')
|
||||
->where('draft', false)
|
||||
->orderBy('updated_at', 'desc')
|
||||
->take(12)
|
||||
->take($favourites->count() > 0 ? 6 : 12)
|
||||
->get();
|
||||
|
||||
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
|
||||
@@ -51,12 +53,13 @@ class HomeController extends Controller
|
||||
'recents' => $recents,
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||
'draftPages' => $draftPages,
|
||||
'favourites' => $favourites,
|
||||
];
|
||||
|
||||
// Add required list ordering & sorting for books & shelves views.
|
||||
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
|
||||
$key = $homepageOption;
|
||||
$view = setting()->getForCurrentUser($key . '_view_type', config('app.views.' . $key));
|
||||
$view = setting()->getForCurrentUser($key . '_view_type');
|
||||
$sort = setting()->getForCurrentUser($key . '_sort', 'name');
|
||||
$order = setting()->getForCurrentUser($key . '_sort_order', 'asc');
|
||||
|
||||
@@ -105,20 +108,21 @@ class HomeController extends Controller
|
||||
*/
|
||||
public function customHeadContent()
|
||||
{
|
||||
return view('partials.custom-head-content');
|
||||
return view('partials.custom-head');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the view for /robots.txt
|
||||
* @return $this
|
||||
*/
|
||||
public function getRobots()
|
||||
{
|
||||
$sitePublic = setting('app-public', false);
|
||||
$allowRobots = config('app.allow_robots');
|
||||
|
||||
if ($allowRobots === null) {
|
||||
$allowRobots = $sitePublic;
|
||||
}
|
||||
|
||||
return response()
|
||||
->view('common.robots', ['allowRobots' => $allowRobots])
|
||||
->header('Content-Type', 'text/plain');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers\Images;
|
||||
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Uploads\Image;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
@@ -27,12 +28,15 @@ class ImageController extends Controller
|
||||
|
||||
/**
|
||||
* Provide an image file from storage.
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function showImage(string $path)
|
||||
{
|
||||
$path = storage_path('uploads/images/' . $path);
|
||||
if (!file_exists($path)) {
|
||||
abort(404);
|
||||
throw (new NotFoundException(trans('errors.image_not_found')))
|
||||
->setSubtitle(trans('errors.image_not_found_subtitle'))
|
||||
->setDetails(trans('errors.image_not_found_details'));
|
||||
}
|
||||
|
||||
return response()->file($path);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Entities\Tools\PageEditActivity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\PageRepo;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
use Views;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
@@ -142,7 +142,9 @@ class PageController extends Controller
|
||||
$page->load(['comments.createdBy']);
|
||||
}
|
||||
|
||||
Views::add($page);
|
||||
$nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
|
||||
|
||||
View::incrementFor($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
return view('pages.show', [
|
||||
'page' => $page,
|
||||
@@ -150,7 +152,9 @@ class PageController extends Controller
|
||||
'current' => $page,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'commentsEnabled' => $commentsEnabled,
|
||||
'pageNav' => $pageNav
|
||||
'pageNav' => $pageNav,
|
||||
'next' => $nextPreviousLocator->getNext(),
|
||||
'previous' => $nextPreviousLocator->getPrevious(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -243,8 +247,8 @@ class PageController extends Controller
|
||||
|
||||
$updateTime = $draft->updated_at->timestamp;
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => trans('entities.pages_edit_draft_save_at'),
|
||||
'status' => 'success',
|
||||
'message' => trans('entities.pages_edit_draft_save_at'),
|
||||
'timestamp' => $updateTime
|
||||
]);
|
||||
}
|
||||
@@ -267,7 +271,7 @@ class PageController extends Controller
|
||||
{
|
||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
|
||||
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
|
||||
return view('pages.delete', [
|
||||
'book' => $page->book,
|
||||
'page' => $page,
|
||||
@@ -283,7 +287,7 @@ class PageController extends Controller
|
||||
{
|
||||
$page = $this->pageRepo->getById($pageId);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
|
||||
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
|
||||
return view('pages.delete', [
|
||||
'book' => $page->book,
|
||||
'page' => $page,
|
||||
@@ -295,7 +299,6 @@ class PageController extends Controller
|
||||
* Remove the specified page from storage.
|
||||
* @throws NotFoundException
|
||||
* @throws Throwable
|
||||
* @throws NotifyException
|
||||
*/
|
||||
public function destroy(string $bookSlug, string $pageSlug)
|
||||
{
|
||||
@@ -311,7 +314,6 @@ class PageController extends Controller
|
||||
/**
|
||||
* Remove the specified draft page from storage.
|
||||
* @throws NotFoundException
|
||||
* @throws NotifyException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function destroyDraft(string $bookSlug, int $pageId)
|
||||
@@ -340,9 +342,9 @@ class PageController extends Controller
|
||||
->paginate(20)
|
||||
->setPath(url('/pages/recently-updated'));
|
||||
|
||||
return view('pages.detailed-listing', [
|
||||
return view('common.detailed-listing-paginated', [
|
||||
'title' => trans('entities.recently_updated_pages'),
|
||||
'pages' => $pages
|
||||
'entities' => $pages
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -380,7 +382,7 @@ class PageController extends Controller
|
||||
try {
|
||||
$parent = $this->pageRepo->move($page, $entitySelection);
|
||||
} catch (Exception $exception) {
|
||||
if ($exception instanceof PermissionsException) {
|
||||
if ($exception instanceof PermissionsException) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
@@ -424,7 +426,7 @@ class PageController extends Controller
|
||||
try {
|
||||
$pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
|
||||
} catch (Exception $exception) {
|
||||
if ($exception instanceof PermissionsException) {
|
||||
if ($exception instanceof PermissionsException) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
@@ -445,7 +447,7 @@ class PageController extends Controller
|
||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
return view('pages.permissions', [
|
||||
'page' => $page,
|
||||
'page' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Actions\ViewService;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Queries\Popular;
|
||||
use BookStack\Entities\Tools\SearchRunner;
|
||||
use BookStack\Entities\Tools\ShelfContext;
|
||||
use BookStack\Entities\Tools\SearchOptions;
|
||||
@@ -12,16 +9,13 @@ use Illuminate\Http\Request;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
protected $viewService;
|
||||
protected $searchRunner;
|
||||
protected $entityContextManager;
|
||||
|
||||
public function __construct(
|
||||
ViewService $viewService,
|
||||
SearchRunner $searchRunner,
|
||||
ShelfContext $entityContextManager
|
||||
) {
|
||||
$this->viewService = $viewService;
|
||||
$this->searchRunner = $searchRunner;
|
||||
$this->entityContextManager = $entityContextManager;
|
||||
}
|
||||
@@ -85,7 +79,7 @@ class SearchController extends Controller
|
||||
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
|
||||
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
|
||||
} else {
|
||||
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
|
||||
$entities = (new Popular)->run(20, 0, $entityTypes, $permission);
|
||||
}
|
||||
|
||||
return view('search.entity-ajax-list', ['entities' => $entities]);
|
||||
|
||||
47
app/Http/Controllers/StatusController.php
Normal file
47
app/Http/Controllers/StatusController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StatusController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Show the system status as a simple json page.
|
||||
*/
|
||||
public function show()
|
||||
{
|
||||
$statuses = [
|
||||
'database' => $this->trueWithoutError(function () {
|
||||
return DB::table('migrations')->count() > 0;
|
||||
}),
|
||||
'cache' => $this->trueWithoutError(function () {
|
||||
$rand = Str::random();
|
||||
Cache::set('status_test', $rand);
|
||||
return Cache::get('status_test') === $rand;
|
||||
}),
|
||||
'session' => $this->trueWithoutError(function () {
|
||||
$rand = Str::random();
|
||||
Session::put('status_test', $rand);
|
||||
return Session::get('status_test') === $rand;
|
||||
}),
|
||||
];
|
||||
|
||||
$hasError = in_array(false, $statuses);
|
||||
return response()->json($statuses, $hasError ? 500 : 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the callable passed returns true and does not throw an exception.
|
||||
*/
|
||||
protected function trueWithoutError(callable $test): bool
|
||||
{
|
||||
try {
|
||||
return $test() === true;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,5 +140,4 @@ class UserApiTokenController extends Controller
|
||||
$token = ApiToken::query()->where('user_id', '=', $user->id)->where('id', '=', $tokenId)->firstOrFail();
|
||||
return [$user, $token];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@ use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Auth\Access\UserInviteService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\UserUpdateException;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
@@ -61,7 +64,7 @@ class UserController extends Controller
|
||||
/**
|
||||
* Store a newly created user in storage.
|
||||
* @throws UserUpdateException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
@@ -90,6 +93,7 @@ class UserController extends Controller
|
||||
$user->external_auth_id = $request->get('external_auth_id');
|
||||
}
|
||||
|
||||
$user->refreshSlug();
|
||||
$user->save();
|
||||
|
||||
if ($sendInvite) {
|
||||
@@ -132,8 +136,8 @@ class UserController extends Controller
|
||||
/**
|
||||
* Update the specified user in storage.
|
||||
* @throws UserUpdateException
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws ImageUploadException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function update(Request $request, int $id)
|
||||
{
|
||||
@@ -157,6 +161,11 @@ class UserController extends Controller
|
||||
$user->email = $request->get('email');
|
||||
}
|
||||
|
||||
// Refresh the slug if the user's name has changed
|
||||
if ($user->isDirty('name')) {
|
||||
$user->refreshSlug();
|
||||
}
|
||||
|
||||
// Role updates
|
||||
if (userCan('users-manage') && $request->filled('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
@@ -216,7 +225,7 @@ class UserController extends Controller
|
||||
|
||||
/**
|
||||
* Remove the specified user from storage.
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroy(Request $request, int $id)
|
||||
{
|
||||
@@ -243,25 +252,6 @@ class UserController extends Controller
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the user profile page
|
||||
*/
|
||||
public function showProfilePage($id)
|
||||
{
|
||||
$user = $this->userRepo->getById($id);
|
||||
|
||||
$userActivity = $this->userRepo->getActivity($user);
|
||||
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5);
|
||||
$assetCounts = $this->userRepo->getAssetCounts($user);
|
||||
|
||||
return view('users.profile', [
|
||||
'user' => $user,
|
||||
'activity' => $userActivity,
|
||||
'recentlyCreated' => $recentlyCreated,
|
||||
'assetCounts' => $assetCounts
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's preferred book-list display setting.
|
||||
*/
|
||||
@@ -310,7 +300,7 @@ class UserController extends Controller
|
||||
*/
|
||||
public function changeSort(Request $request, string $id, string $type)
|
||||
{
|
||||
$validSortTypes = ['books', 'bookshelves'];
|
||||
$validSortTypes = ['books', 'bookshelves', 'shelf_books'];
|
||||
if (!in_array($type, $validSortTypes)) {
|
||||
return redirect()->back(500);
|
||||
}
|
||||
@@ -353,7 +343,7 @@ class UserController extends Controller
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $userId);
|
||||
|
||||
$sort = $request->get('sort');
|
||||
if (!in_array($sort, ['name', 'created_at', 'updated_at'])) {
|
||||
if (!in_array($sort, ['name', 'created_at', 'updated_at', 'default'])) {
|
||||
$sort = 'name';
|
||||
}
|
||||
|
||||
|
||||
25
app/Http/Controllers/UserProfileController.php
Normal file
25
app/Http/Controllers/UserProfileController.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Auth\UserRepo;
|
||||
|
||||
class UserProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user profile page
|
||||
*/
|
||||
public function show(UserRepo $repo, string $slug)
|
||||
{
|
||||
$user = $repo->getBySlug($slug);
|
||||
|
||||
$userActivity = $repo->getActivity($user);
|
||||
$recentlyCreated = $repo->getRecentlyCreated($user, 5);
|
||||
$assetCounts = $repo->getAssetCounts($user);
|
||||
|
||||
return view('users.profile', [
|
||||
'user' => $user,
|
||||
'activity' => $userActivity,
|
||||
'recentlyCreated' => $recentlyCreated,
|
||||
'assetCounts' => $assetCounts
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,8 @@ class Kernel extends HttpKernel
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\BookStack\Http\Middleware\VerifyCsrfToken::class,
|
||||
\BookStack\Http\Middleware\RunThemeActions::class,
|
||||
\BookStack\Http\Middleware\Localization::class,
|
||||
\BookStack\Http\Middleware\GlobalViewData::class,
|
||||
],
|
||||
'api' => [
|
||||
\BookStack\Http\Middleware\ThrottleApiRequests::class,
|
||||
|
||||
@@ -33,4 +33,4 @@ trait ChecksForEmailConfirmation
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php namespace BookStack\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Class GlobalViewData
|
||||
* Sets up data that is accessible to any view rendered by the web routes.
|
||||
*/
|
||||
class GlobalViewData
|
||||
{
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
view()->share('signedIn', auth()->check());
|
||||
view()->share('currentUser', user());
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ class Localization
|
||||
protected $localeMap = [
|
||||
'ar' => 'ar',
|
||||
'bg' => 'bg_BG',
|
||||
'bs' => 'bs_BA',
|
||||
'ca' => 'ca',
|
||||
'da' => 'da_DK',
|
||||
'de' => 'de_DE',
|
||||
'de_informal' => 'de_DE',
|
||||
@@ -26,13 +28,16 @@ class Localization
|
||||
'es_AR' => 'es_AR',
|
||||
'fr' => 'fr_FR',
|
||||
'he' => 'he_IL',
|
||||
'hr' => 'hr_HR',
|
||||
'id' => 'id_ID',
|
||||
'it' => 'it_IT',
|
||||
'ja' => 'ja',
|
||||
'ko' => 'ko_KR',
|
||||
'lv' => 'lv_LV',
|
||||
'nl' => 'nl_NL',
|
||||
'nb' => 'nb_NO',
|
||||
'pl' => 'pl_PL',
|
||||
'pt' => 'pl_PT',
|
||||
'pt' => 'pt_PT',
|
||||
'pt_BR' => 'pt_BR',
|
||||
'ru' => 'ru',
|
||||
'sk' => 'sk_SK',
|
||||
@@ -57,12 +62,7 @@ class Localization
|
||||
$defaultLang = config('app.locale');
|
||||
config()->set('app.default_locale', $defaultLang);
|
||||
|
||||
if (user()->isDefault() && config('app.auto_detect_locale')) {
|
||||
$locale = $this->autoDetectLocale($request, $defaultLang);
|
||||
} else {
|
||||
$locale = setting()->getUser(user(), 'language', $defaultLang);
|
||||
}
|
||||
|
||||
$locale = $this->getUserLocale($request, $defaultLang);
|
||||
config()->set('app.lang', str_replace('_', '-', $this->getLocaleIso($locale)));
|
||||
|
||||
// Set text direction
|
||||
@@ -76,14 +76,29 @@ class Localization
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the locale specifically for the currently logged in user if available.
|
||||
*/
|
||||
protected function getUserLocale(Request $request, string $default): string
|
||||
{
|
||||
try {
|
||||
$user = user();
|
||||
} catch (\Exception $exception) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if ($user->isDefault() && config('app.auto_detect_locale')) {
|
||||
return $this->autoDetectLocale($request, $default);
|
||||
}
|
||||
|
||||
return setting()->getUser($user, 'language', $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Autodetect the visitors locale by matching locales in their headers
|
||||
* against the locales supported by BookStack.
|
||||
* @param Request $request
|
||||
* @param string $default
|
||||
* @return string
|
||||
*/
|
||||
protected function autoDetectLocale(Request $request, string $default)
|
||||
protected function autoDetectLocale(Request $request, string $default): string
|
||||
{
|
||||
$availableLocales = config('app.locales');
|
||||
foreach ($request->getLanguages() as $lang) {
|
||||
@@ -96,10 +111,8 @@ class Localization
|
||||
|
||||
/**
|
||||
* Get the ISO version of a BookStack language name
|
||||
* @param string $locale
|
||||
* @return string
|
||||
*/
|
||||
public function getLocaleIso(string $locale)
|
||||
public function getLocaleIso(string $locale): string
|
||||
{
|
||||
return $this->localeMap[$locale] ?? $locale;
|
||||
}
|
||||
@@ -107,7 +120,6 @@ class Localization
|
||||
/**
|
||||
* Set the system date locale for localized date formatting.
|
||||
* Will try both the standard locale name and the UTF8 variant.
|
||||
* @param string $locale
|
||||
*/
|
||||
protected function setSystemDateLocale(string $locale)
|
||||
{
|
||||
|
||||
29
app/Http/Middleware/RunThemeActions.php
Normal file
29
app/Http/Middleware/RunThemeActions.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Closure;
|
||||
|
||||
class RunThemeActions
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$earlyResponse = Theme::dispatch(ThemeEvents::WEB_MIDDLEWARE_BEFORE, $request);
|
||||
if (!is_null($earlyResponse)) {
|
||||
return $earlyResponse;
|
||||
}
|
||||
|
||||
$response = $next($request);
|
||||
$response = Theme::dispatch(ThemeEvents::WEB_MIDDLEWARE_AFTER, $request, $response) ?? $response;
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -14,5 +14,4 @@ class ThrottleApiRequests extends Middleware
|
||||
{
|
||||
return (int) config('api.requests_per_minute');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
11
app/Interfaces/Favouritable.php
Normal file
11
app/Interfaces/Favouritable.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php namespace BookStack\Interfaces;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
interface Favouritable
|
||||
{
|
||||
/**
|
||||
* Get the related favourite instances.
|
||||
*/
|
||||
public function favourites(): MorphMany;
|
||||
}
|
||||
@@ -8,4 +8,4 @@ interface Loggable
|
||||
* Get the string descriptor for this item.
|
||||
*/
|
||||
public function logDescriptor(): string;
|
||||
}
|
||||
}
|
||||
|
||||
23
app/Interfaces/Sluggable.php
Normal file
23
app/Interfaces/Sluggable.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php namespace BookStack\Interfaces;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* Interface Sluggable
|
||||
*
|
||||
* Assigned to models that can have slugs.
|
||||
* Must have the below properties.
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @method Builder newQuery
|
||||
*/
|
||||
interface Sluggable
|
||||
{
|
||||
|
||||
/**
|
||||
* Regenerate the slug for this model.
|
||||
*/
|
||||
public function refreshSlug(): string;
|
||||
|
||||
}
|
||||
11
app/Interfaces/Viewable.php
Normal file
11
app/Interfaces/Viewable.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php namespace BookStack\Interfaces;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
interface Viewable
|
||||
{
|
||||
/**
|
||||
* Get all view instances for this viewable model.
|
||||
*/
|
||||
public function views(): MorphMany;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
use Blade;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\BreadcrumbsViewComposer;
|
||||
@@ -8,9 +9,11 @@ use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Settings\Setting;
|
||||
use BookStack\Settings\SettingService;
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Socialite\Contracts\Factory as SocialiteFactory;
|
||||
use Schema;
|
||||
use URL;
|
||||
|
||||
@@ -59,7 +62,11 @@ 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'));
|
||||
return new SettingService($app->make(Setting::class), $app->make(Repository::class));
|
||||
});
|
||||
|
||||
$this->app->singleton(SocialAuthService::class, function($app) {
|
||||
return new SocialAuthService($app->make(SocialiteFactory::class));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user