mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-05 08:39:55 +03:00
Compare commits
182 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0333185b6d | ||
|
|
83f89f64e8 | ||
|
|
135022136a | ||
|
|
12f96bb1a4 | ||
|
|
678314a0c5 | ||
|
|
0887c39694 | ||
|
|
078e8e7dc3 | ||
|
|
038015f852 | ||
|
|
7c12920dc8 | ||
|
|
895f656897 | ||
|
|
31dbf132b9 | ||
|
|
b5281bc9ca | ||
|
|
3625f12abe | ||
|
|
55d61fceb2 | ||
|
|
2325a307a5 | ||
|
|
d2b49084b0 | ||
|
|
8594f42584 | ||
|
|
dd7463259a | ||
|
|
d23b24b8db | ||
|
|
1c859e94e0 | ||
|
|
981807220c | ||
|
|
a2231c3604 | ||
|
|
622adc5450 | ||
|
|
95e496d16f | ||
|
|
883e18f7c4 | ||
|
|
c5aad29c72 | ||
|
|
ea62fe6004 | ||
|
|
5ae9ed1e22 | ||
|
|
b6be8a2bb9 | ||
|
|
65dd7ad1e9 | ||
|
|
f991948c49 | ||
|
|
ee6a2339b6 | ||
|
|
fd26f54b99 | ||
|
|
11a1a6fb16 | ||
|
|
882c609296 | ||
|
|
77ad819970 | ||
|
|
2835e5be93 | ||
|
|
856fca8289 | ||
|
|
48d0095aa2 | ||
|
|
176a0dcd59 | ||
|
|
94b0f70bfa | ||
|
|
36d7ff77a9 | ||
|
|
fb16ac326f | ||
|
|
5947f59a04 | ||
|
|
1843d80fb7 | ||
|
|
6252b46395 | ||
|
|
20ecaa5c5a | ||
|
|
08b2a77d41 | ||
|
|
3e8e9a23cf | ||
|
|
1253711c7d | ||
|
|
963d8f4693 | ||
|
|
0de4d6d223 | ||
|
|
06f694bad2 | ||
|
|
58b83b64c8 | ||
|
|
dfe4cde6ee | ||
|
|
41689a1e65 | ||
|
|
2ae8026903 | ||
|
|
dcb36b27a0 | ||
|
|
83082c32ef | ||
|
|
1e112f78d8 | ||
|
|
9283f28e31 | ||
|
|
7f5fc9fbe3 | ||
|
|
ce566bea2a | ||
|
|
63ce3c9add | ||
|
|
f0470afb4c | ||
|
|
f8e6172582 | ||
|
|
7a8505f812 | ||
|
|
9806907d53 | ||
|
|
2b3726702d | ||
|
|
2b46b00f29 | ||
|
|
536ad14276 | ||
|
|
a318775cfc | ||
|
|
9e0b8a9fb6 | ||
|
|
7c692ec588 | ||
|
|
da0dc7292c | ||
|
|
045710ea08 | ||
|
|
c6ad16dba6 | ||
|
|
4ea1f0c633 | ||
|
|
f5077c17f4 | ||
|
|
c73773930e | ||
|
|
1782618c64 | ||
|
|
a01bb92989 | ||
|
|
a2bcf765a8 | ||
|
|
130dc05517 | ||
|
|
572d8b3700 | ||
|
|
e0d9380055 | ||
|
|
15647a0409 | ||
|
|
e88dbe4db3 | ||
|
|
84c501bcf4 | ||
|
|
c8b6f622f4 | ||
|
|
ef211a76ae | ||
|
|
d11144d9e2 | ||
|
|
f96b0ea5f3 | ||
|
|
b4e29d2b7d | ||
|
|
2732d8961f | ||
|
|
b2f863e1f1 | ||
|
|
1df7497c09 | ||
|
|
d29a2a647a | ||
|
|
43f32f6d5a | ||
|
|
921131f999 | ||
|
|
0cde2704d0 | ||
|
|
db4093d523 | ||
|
|
049d6ba5b2 | ||
|
|
e33b587b87 | ||
|
|
c8be6ee8a6 | ||
|
|
46e6e239dc | ||
|
|
eb653bda16 | ||
|
|
9e1c8ec82a | ||
|
|
2cd7a48044 | ||
|
|
d089623aac | ||
|
|
8d7febe482 | ||
|
|
815f8d79ed | ||
|
|
b62dab32e0 | ||
|
|
9d15688a43 | ||
|
|
033b163675 | ||
|
|
6eadf3efb3 | ||
|
|
f83cc83877 | ||
|
|
17215431ca | ||
|
|
90c543064b | ||
|
|
a709fd04b5 | ||
|
|
4a1d060eb9 | ||
|
|
e17cdab420 | ||
|
|
2d074caf72 | ||
|
|
99202b3bb8 | ||
|
|
73eac83afe | ||
|
|
c11f795c1d | ||
|
|
262f863981 | ||
|
|
a4c94390a1 | ||
|
|
7e6e1fca76 | ||
|
|
aaa2205df1 | ||
|
|
4aed3f8558 | ||
|
|
7b4086107c | ||
|
|
585bd0cc45 | ||
|
|
f18e2784be | ||
|
|
f88e6d1520 | ||
|
|
872961ef7c | ||
|
|
bbd8d63652 | ||
|
|
af39ff15ac | ||
|
|
aae3cd69d7 | ||
|
|
2d3df955ae | ||
|
|
8b5747eae2 | ||
|
|
6c699f7fab | ||
|
|
ac6eceb0e5 | ||
|
|
a2a2f3a4dd | ||
|
|
6db64763fe | ||
|
|
c9beacbfbf | ||
|
|
53f3cca85d | ||
|
|
ed08bbcecc | ||
|
|
2aace16704 | ||
|
|
ade66dcf2f | ||
|
|
d3eaaf6457 | ||
|
|
941217d9fb | ||
|
|
4239d4c54d | ||
|
|
8d91f4369b | ||
|
|
722aa04577 | ||
|
|
2d0abc4164 | ||
|
|
c3f7b39a0f | ||
|
|
de97ebf9b7 | ||
|
|
f492a660a8 | ||
|
|
ef11100863 | ||
|
|
1a26b47782 | ||
|
|
cb0d674a71 | ||
|
|
4d094331cf | ||
|
|
2312d07bb5 | ||
|
|
fbd388ba4c | ||
|
|
d3ca23b195 | ||
|
|
553954ad18 | ||
|
|
d8c45f5746 | ||
|
|
edc7c12edf | ||
|
|
a72bd75e3a | ||
|
|
31f1dca8a8 | ||
|
|
819ec55b1b | ||
|
|
dba506a20e | ||
|
|
d0de4fd8f9 | ||
|
|
00eedafbfd | ||
|
|
6e18620a0a | ||
|
|
fe54c7f27a | ||
|
|
65830b428c | ||
|
|
b438e0187c | ||
|
|
8614775c14 | ||
|
|
4cbd1a9eb5 | ||
|
|
07626669da |
@@ -42,7 +42,7 @@ APP_TIMEZONE=UTC
|
||||
# overrides can be made. Defaults to disabled.
|
||||
APP_THEME=false
|
||||
|
||||
# Trusted Proxies
|
||||
# Trusted proxies
|
||||
# Used to indicate trust of systems that proxy to the application so
|
||||
# certain header values (Such as "X-Forwarded-For") can be used from the
|
||||
# incoming proxy request to provide origin detail.
|
||||
@@ -58,6 +58,13 @@ DB_DATABASE=database_database
|
||||
DB_USERNAME=database_username
|
||||
DB_PASSWORD=database_user_password
|
||||
|
||||
# MySQL specific connection options
|
||||
# Path to Certificate Authority (CA) certificate file for your MySQL instance.
|
||||
# When this option is used host name identity verification will be performed
|
||||
# which checks the hostname, used by the client, against names within the
|
||||
# certificate itself (Common Name or Subject Alternative Name).
|
||||
MYSQL_ATTR_SSL_CA="/path/to/ca.pem"
|
||||
|
||||
# Mail system to use
|
||||
# Can be 'smtp' or 'sendmail'
|
||||
MAIL_DRIVER=smtp
|
||||
@@ -216,6 +223,7 @@ LDAP_DUMP_USER_DETAILS=false
|
||||
LDAP_USER_TO_GROUPS=false
|
||||
LDAP_GROUP_ATTRIBUTE="memberOf"
|
||||
LDAP_REMOVE_FROM_GROUPS=false
|
||||
LDAP_DUMP_USER_GROUPS=false
|
||||
|
||||
# SAML authentication configuration
|
||||
# Refer to https://www.bookstackapp.com/docs/admin/saml2-auth/
|
||||
@@ -297,6 +305,11 @@ RECYCLE_BIN_LIFETIME=30
|
||||
# Maximum file size, in megabytes, that can be uploaded to the system.
|
||||
FILE_UPLOAD_SIZE_LIMIT=50
|
||||
|
||||
# Export Page Size
|
||||
# Primarily used to determine page size of PDF exports.
|
||||
# Can be 'a4' or 'letter'.
|
||||
EXPORT_PAGE_SIZE=a4
|
||||
|
||||
# Allow <script> tags in page content
|
||||
# Note, if set to 'true' the page editor may still escape scripts.
|
||||
ALLOW_CONTENT_SCRIPTS=false
|
||||
@@ -319,6 +332,13 @@ ALLOW_UNTRUSTED_SERVER_FETCHING=false
|
||||
# Setting this option will also auto-adjust cookies to be SameSite=None.
|
||||
ALLOWED_IFRAME_HOSTS=null
|
||||
|
||||
# A list of sources/hostnames that can be loaded within iframes within BookStack.
|
||||
# Space separated if multiple. BookStack host domain is auto-inferred.
|
||||
# Can be set to a lone "*" to allow all sources for iframe content (Not advised).
|
||||
# Defaults to a set of common services.
|
||||
# Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
|
||||
ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com"
|
||||
|
||||
# The default and maximum item-counts for listing API requests.
|
||||
API_DEFAULT_ITEM_COUNT=100
|
||||
API_MAX_ITEM_COUNT=500
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/api_request.yml
vendored
1
.github/ISSUE_TEMPLATE/api_request.yml
vendored
@@ -1,6 +1,5 @@
|
||||
name: New API Endpoint or API Ability
|
||||
description: Request a new endpoint or API feature be added
|
||||
title: "[API Request]: "
|
||||
labels: [":nut_and_bolt: API Request"]
|
||||
body:
|
||||
- type: textarea
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,5 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve or fix things
|
||||
title: "[Bug Report]: "
|
||||
labels: [":bug: Bug"]
|
||||
body:
|
||||
- type: textarea
|
||||
@@ -36,6 +35,15 @@ body:
|
||||
description: Provide any additional context and screenshots here to help us solve this issue
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: browserdetails
|
||||
attributes:
|
||||
label: Browser Details
|
||||
description: |
|
||||
If this is an issue that occurs when using the BookStack interface, please provide details of the browser used which presents the reported issue.
|
||||
placeholder: (eg. Firefox 97 (64-bit) on Windows 11)
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: bsversion
|
||||
attributes:
|
||||
|
||||
40
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
40
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,6 +1,5 @@
|
||||
name: Feature Request
|
||||
description: Request a new language to be added to CrowdIn for you to translate
|
||||
title: "[Feature Request]: "
|
||||
description: Request a new feature or idea to be added to BookStack
|
||||
labels: [":hammer: Feature Request"]
|
||||
body:
|
||||
- type: textarea
|
||||
@@ -13,8 +12,41 @@ body:
|
||||
- type: textarea
|
||||
id: benefits
|
||||
attributes:
|
||||
label: Describe the benefits this feature would bring to BookStack users
|
||||
description: Explain the measurable benefits this feature would achieve for existing BookStack users
|
||||
label: Describe the benefits this would bring to existing BookStack users
|
||||
description: |
|
||||
Explain the measurable benefits this feature would achieve for existing BookStack users.
|
||||
These benefits should details outcomes in terms of what this request solves/achieves, and should not be specific to implementation.
|
||||
This helps us understand the core desired goal so that a variety of potential implementations could be explored.
|
||||
This field is important. Lack if input here may lead to early issue closure.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: already_achieved
|
||||
attributes:
|
||||
label: Can the goal of this request already be achieved via other means?
|
||||
description: |
|
||||
Yes/No. If yes, please describe how the requested approach fits in with the existing method.
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: confirm-search
|
||||
attributes:
|
||||
label: Have you searched for an existing open/closed issue?
|
||||
description: |
|
||||
To help us keep these issues under control, please ensure you have first [searched our issue list](https://github.com/BookStackApp/BookStack/issues?q=is%3Aissue) for any existing issues that cover the fundemental benefit/goal of your request.
|
||||
options:
|
||||
- label: I have searched for existing issues and none cover my fundemental request
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: existing_usage
|
||||
attributes:
|
||||
label: How long have you been using BookStack?
|
||||
options:
|
||||
- Not using yet, just scoping
|
||||
- 0 to 6 months
|
||||
- 6 months to 1 year
|
||||
- 1 to 5 years
|
||||
- Over 5 years
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/language_request.yml
vendored
1
.github/ISSUE_TEMPLATE/language_request.yml
vendored
@@ -1,6 +1,5 @@
|
||||
name: Language Request
|
||||
description: Request a new language to be added to CrowdIn for you to translate
|
||||
title: "[Language Request]: "
|
||||
labels: [":earth_africa: Translations"]
|
||||
assignees:
|
||||
- ssddanbrown
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/support_request.yml
vendored
1
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -1,6 +1,5 @@
|
||||
name: Support Request
|
||||
description: Request support for a specific problem you have not been able to solve yourself
|
||||
title: "[Support Request]: "
|
||||
labels: [":dog2: Support"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
|
||||
34
.github/translators.txt
vendored
34
.github/translators.txt
vendored
@@ -158,7 +158,7 @@ HenrijsS :: Latvian
|
||||
Pascal R-B (pborgner) :: German
|
||||
Boris (Ginfred) :: Russian
|
||||
Jonas Anker Rasmussen (jonasanker) :: Danish
|
||||
Gerwin de Keijzer (gdekeijzer) :: Dutch; German; German Informal
|
||||
Gerwin de Keijzer (gdekeijzer) :: Dutch; German Informal; German
|
||||
kometchtech :: Japanese
|
||||
Auri (Atalonica) :: Catalan
|
||||
Francesco Franchina (ffranchina) :: Italian
|
||||
@@ -206,3 +206,35 @@ Thiago Rafael Pereira de Carvalho (thiago.rafael) :: Portuguese, Brazilian
|
||||
Ken Roger Bolgnes (kenbo124) :: Norwegian Bokmal
|
||||
Nguyen Hung Phuong (hnwolf) :: Vietnamese
|
||||
Umut ERGENE (umutergene67) :: Turkish
|
||||
Tomáš Batelka (Vofy) :: Czech
|
||||
Mundo Racional (ismael.mesquita) :: Portuguese, Brazilian
|
||||
Zarik (3apuk) :: Russian
|
||||
Ali Shaatani (a.shaatani) :: Arabic
|
||||
ChacMaster :: Portuguese, Brazilian
|
||||
Saeed (saeed205) :: Persian
|
||||
Julesdevops :: French
|
||||
peter cerny (posli.to.semka) :: Slovak
|
||||
Pavel Karlin (pavelkarlin) :: Russian
|
||||
SmokingCrop :: Dutch
|
||||
Maciej Lebiest (Szwendacz) :: Polish
|
||||
DiscordDigital :: German; German Informal
|
||||
Gábor Marton (dodver) :: Hungarian
|
||||
Jasell :: Swedish
|
||||
Ghost_chu (ghostchu) :: Chinese Simplified
|
||||
Ravid Shachar (ravidshachar) :: Hebrew
|
||||
Helga Guchshenskaya (guchshenskaya) :: Russian
|
||||
daniel chou (chou0214) :: Chinese Traditional
|
||||
Manolis PATRIARCHE (m.patriarche) :: French
|
||||
Mohammed Haboubi (haboubi92) :: Arabic
|
||||
roncallyt :: Portuguese, Brazilian
|
||||
goegol :: Dutch
|
||||
msevgen :: Turkish
|
||||
Khroners :: French
|
||||
MASOUD HOSSEINY (masoudme) :: Persian
|
||||
Thomerson Roncally (roncallyt) :: Portuguese, Brazilian
|
||||
metaarch :: Bulgarian
|
||||
Xabi (xabikip) :: Basque
|
||||
pedromcsousa :: Portuguese
|
||||
Nir Louk (looknear) :: Hebrew
|
||||
Alex (qianmengnet) :: Chinese Simplified
|
||||
stothew :: German
|
||||
|
||||
11
.github/workflows/phpstan.yml
vendored
11
.github/workflows/phpstan.yml
vendored
@@ -1,19 +1,14 @@
|
||||
name: phpstan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- l10n_master
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10n_master
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.3']
|
||||
php: ['7.4']
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
|
||||
11
.github/workflows/phpunit.yml
vendored
11
.github/workflows/phpunit.yml
vendored
@@ -1,19 +1,14 @@
|
||||
name: phpunit
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- l10n_master
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10n_master
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.3', '7.4', '8.0', '8.1']
|
||||
php: ['7.4', '8.0', '8.1']
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
|
||||
11
.github/workflows/test-migrations.yml
vendored
11
.github/workflows/test-migrations.yml
vendored
@@ -1,19 +1,14 @@
|
||||
name: test-migrations
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- l10n_master
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10n_master
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.3', '7.4', '8.0', '8.1']
|
||||
php: ['7.4', '8.0', '8.1']
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Model;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -22,31 +21,16 @@ class DispatchWebhookJob implements ShouldQueue
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* @var Webhook
|
||||
*/
|
||||
protected $webhook;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $event;
|
||||
protected Webhook $webhook;
|
||||
protected string $event;
|
||||
protected User $initiator;
|
||||
protected int $initiatedTime;
|
||||
|
||||
/**
|
||||
* @var string|Loggable
|
||||
*/
|
||||
protected $detail;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
protected $initiator;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $initiatedTime;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
@@ -68,45 +52,31 @@ class DispatchWebhookJob implements ShouldQueue
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$response = Http::asJson()
|
||||
->withOptions(['allow_redirects' => ['strict' => true]])
|
||||
->timeout(3)
|
||||
->post($this->webhook->endpoint, $this->buildWebhookData());
|
||||
$themeResponse = Theme::dispatch(ThemeEvents::WEBHOOK_CALL_BEFORE, $this->event, $this->webhook, $this->detail, $this->initiator, $this->initiatedTime);
|
||||
$webhookData = $themeResponse ?? WebhookFormatter::getDefault($this->event, $this->webhook, $this->detail, $this->initiator, $this->initiatedTime)->format();
|
||||
$lastError = null;
|
||||
|
||||
if ($response->failed()) {
|
||||
try {
|
||||
$response = Http::asJson()
|
||||
->withOptions(['allow_redirects' => ['strict' => true]])
|
||||
->timeout($this->webhook->timeout)
|
||||
->post($this->webhook->endpoint, $webhookData);
|
||||
} catch (\Exception $exception) {
|
||||
$lastError = $exception->getMessage();
|
||||
Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with error \"{$lastError}\"");
|
||||
}
|
||||
|
||||
if (isset($response) && $response->failed()) {
|
||||
$lastError = "Response status from endpoint was {$response->status()}";
|
||||
Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with status {$response->status()}");
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildWebhookData(): array
|
||||
{
|
||||
$textParts = [
|
||||
$this->initiator->name,
|
||||
trans('activities.' . $this->event),
|
||||
];
|
||||
|
||||
if ($this->detail instanceof Entity) {
|
||||
$textParts[] = '"' . $this->detail->name . '"';
|
||||
$this->webhook->last_called_at = now();
|
||||
if ($lastError) {
|
||||
$this->webhook->last_errored_at = now();
|
||||
$this->webhook->last_error = $lastError;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'event' => $this->event,
|
||||
'text' => implode(' ', $textParts),
|
||||
'triggered_at' => Carbon::createFromTimestampUTC($this->initiatedTime)->toISOString(),
|
||||
'triggered_by' => $this->initiator->attributesToArray(),
|
||||
'triggered_by_profile_url' => $this->initiator->getProfileUrl(),
|
||||
'webhook_id' => $this->webhook->id,
|
||||
'webhook_name' => $this->webhook->name,
|
||||
];
|
||||
|
||||
if (method_exists($this->detail, 'getUrl')) {
|
||||
$data['url'] = $this->detail->getUrl();
|
||||
}
|
||||
|
||||
if ($this->detail instanceof Model) {
|
||||
$data['related_item'] = $this->detail->attributesToArray();
|
||||
}
|
||||
|
||||
return $data;
|
||||
$this->webhook->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -14,13 +15,22 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property string $endpoint
|
||||
* @property Collection $trackedEvents
|
||||
* @property bool $active
|
||||
* @property int $timeout
|
||||
* @property string $last_error
|
||||
* @property Carbon $last_called_at
|
||||
* @property Carbon $last_errored_at
|
||||
*/
|
||||
class Webhook extends Model implements Loggable
|
||||
{
|
||||
protected $fillable = ['name', 'endpoint'];
|
||||
protected $fillable = ['name', 'endpoint', 'timeout'];
|
||||
|
||||
use HasFactory;
|
||||
|
||||
protected $casts = [
|
||||
'last_called_at' => 'datetime',
|
||||
'last_errored_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Define the tracked event relation a webhook.
|
||||
*/
|
||||
|
||||
124
app/Actions/WebhookFormatter.php
Normal file
124
app/Actions/WebhookFormatter.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class WebhookFormatter
|
||||
{
|
||||
protected Webhook $webhook;
|
||||
protected string $event;
|
||||
protected User $initiator;
|
||||
protected int $initiatedTime;
|
||||
|
||||
/**
|
||||
* @var string|Loggable
|
||||
*/
|
||||
protected $detail;
|
||||
|
||||
/**
|
||||
* @var array{condition: callable(string, Model):bool, format: callable(Model):void}[]
|
||||
*/
|
||||
protected $modelFormatters = [];
|
||||
|
||||
public function __construct(string $event, Webhook $webhook, $detail, User $initiator, int $initiatedTime)
|
||||
{
|
||||
$this->webhook = $webhook;
|
||||
$this->event = $event;
|
||||
$this->initiator = $initiator;
|
||||
$this->initiatedTime = $initiatedTime;
|
||||
$this->detail = is_object($detail) ? clone $detail : $detail;
|
||||
}
|
||||
|
||||
public function format(): array
|
||||
{
|
||||
$data = [
|
||||
'event' => $this->event,
|
||||
'text' => $this->formatText(),
|
||||
'triggered_at' => Carbon::createFromTimestampUTC($this->initiatedTime)->toISOString(),
|
||||
'triggered_by' => $this->initiator->attributesToArray(),
|
||||
'triggered_by_profile_url' => $this->initiator->getProfileUrl(),
|
||||
'webhook_id' => $this->webhook->id,
|
||||
'webhook_name' => $this->webhook->name,
|
||||
];
|
||||
|
||||
if (method_exists($this->detail, 'getUrl')) {
|
||||
$data['url'] = $this->detail->getUrl();
|
||||
}
|
||||
|
||||
if ($this->detail instanceof Model) {
|
||||
$data['related_item'] = $this->formatModel();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(string, Model):bool $condition
|
||||
* @param callable(Model):void $format
|
||||
*/
|
||||
public function addModelFormatter(callable $condition, callable $format): void
|
||||
{
|
||||
$this->modelFormatters[] = [
|
||||
'condition' => $condition,
|
||||
'format' => $format,
|
||||
];
|
||||
}
|
||||
|
||||
public function addDefaultModelFormatters(): void
|
||||
{
|
||||
// Load entity owner, creator, updater details
|
||||
$this->addModelFormatter(
|
||||
fn ($event, $model) => ($model instanceof Entity),
|
||||
fn ($model) => $model->load(['ownedBy', 'createdBy', 'updatedBy'])
|
||||
);
|
||||
|
||||
// Load revision detail for page update and create events
|
||||
$this->addModelFormatter(
|
||||
fn ($event, $model) => ($model instanceof Page && ($event === ActivityType::PAGE_CREATE || $event === ActivityType::PAGE_UPDATE)),
|
||||
fn ($model) => $model->load('currentRevision')
|
||||
);
|
||||
}
|
||||
|
||||
protected function formatModel(): array
|
||||
{
|
||||
/** @var Model $model */
|
||||
$model = $this->detail;
|
||||
$model->unsetRelations();
|
||||
|
||||
foreach ($this->modelFormatters as $formatter) {
|
||||
if ($formatter['condition']($this->event, $model)) {
|
||||
$formatter['format']($model);
|
||||
}
|
||||
}
|
||||
|
||||
return $model->toArray();
|
||||
}
|
||||
|
||||
protected function formatText(): string
|
||||
{
|
||||
$textParts = [
|
||||
$this->initiator->name,
|
||||
trans('activities.' . $this->event),
|
||||
];
|
||||
|
||||
if ($this->detail instanceof Entity) {
|
||||
$textParts[] = '"' . $this->detail->name . '"';
|
||||
}
|
||||
|
||||
return implode(' ', $textParts);
|
||||
}
|
||||
|
||||
public static function getDefault(string $event, Webhook $webhook, $detail, User $initiator, int $initiatedTime): self
|
||||
{
|
||||
$instance = new self($event, $webhook, $detail, $initiator, $initiatedTime);
|
||||
$instance->addDefaultModelFormatters();
|
||||
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@
|
||||
namespace BookStack\Api;
|
||||
|
||||
use BookStack\Http\Controllers\Api\ApiController;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionMethod;
|
||||
@@ -100,11 +102,37 @@ class ApiDocsGenerator
|
||||
$this->controllerClasses[$className] = $class;
|
||||
}
|
||||
|
||||
$rules = $class->getValdationRules()[$methodName] ?? [];
|
||||
$rules = collect($class->getValidationRules()[$methodName] ?? [])->map(function ($validations) {
|
||||
return array_map(function ($validation) {
|
||||
return $this->getValidationAsString($validation);
|
||||
}, $validations);
|
||||
})->toArray();
|
||||
|
||||
return empty($rules) ? null : $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given validation message to a readable string.
|
||||
*/
|
||||
protected function getValidationAsString($validation): string
|
||||
{
|
||||
if (is_string($validation)) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
if (is_object($validation) && method_exists($validation, '__toString')) {
|
||||
return strval($validation);
|
||||
}
|
||||
|
||||
if ($validation instanceof Password) {
|
||||
return 'min:8';
|
||||
}
|
||||
|
||||
$class = get_class($validation);
|
||||
|
||||
throw new Exception("Cannot provide string representation of rule for class: {$class}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse out the description text from a class method comment.
|
||||
*/
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace BookStack\Api;
|
||||
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ListingResponseBuilder
|
||||
@@ -12,6 +14,11 @@ class ListingResponseBuilder
|
||||
protected $request;
|
||||
protected $fields;
|
||||
|
||||
/**
|
||||
* @var array<callable>
|
||||
*/
|
||||
protected $resultModifiers = [];
|
||||
|
||||
protected $filterOperators = [
|
||||
'eq' => '=',
|
||||
'ne' => '!=',
|
||||
@@ -24,6 +31,7 @@ class ListingResponseBuilder
|
||||
|
||||
/**
|
||||
* ListingResponseBuilder constructor.
|
||||
* The given fields will be forced visible within the model results.
|
||||
*/
|
||||
public function __construct(Builder $query, Request $request, array $fields)
|
||||
{
|
||||
@@ -35,12 +43,16 @@ class ListingResponseBuilder
|
||||
/**
|
||||
* Get the response from this builder.
|
||||
*/
|
||||
public function toResponse()
|
||||
public function toResponse(): JsonResponse
|
||||
{
|
||||
$filteredQuery = $this->filterQuery($this->query);
|
||||
|
||||
$total = $filteredQuery->count();
|
||||
$data = $this->fetchData($filteredQuery);
|
||||
$data = $this->fetchData($filteredQuery)->each(function ($model) {
|
||||
foreach ($this->resultModifiers as $modifier) {
|
||||
$modifier($model);
|
||||
}
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
@@ -49,7 +61,17 @@ class ListingResponseBuilder
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the data to return in the response.
|
||||
* Add a callback to modify each element of the results.
|
||||
*
|
||||
* @param (callable(Model)) $modifier
|
||||
*/
|
||||
public function modifyResults($modifier): void
|
||||
{
|
||||
$this->resultModifiers[] = $modifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the data to return within the response.
|
||||
*/
|
||||
protected function fetchData(Builder $query): Collection
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace BookStack\Auth\Access\Guards;
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use BookStack\Auth\Access\RegistrationService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\JsonDebugException;
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||
use BookStack\Exceptions\LoginAttemptException;
|
||||
@@ -15,7 +16,7 @@ use Illuminate\Support\Str;
|
||||
|
||||
class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
{
|
||||
protected $ldapService;
|
||||
protected LdapService $ldapService;
|
||||
|
||||
/**
|
||||
* LdapSessionGuard constructor.
|
||||
@@ -59,8 +60,9 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
* @param array $credentials
|
||||
* @param bool $remember
|
||||
*
|
||||
* @throws LdapException*@throws \BookStack\Exceptions\JsonDebugException
|
||||
* @throws LoginAttemptException
|
||||
* @throws LdapException
|
||||
* @throws JsonDebugException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
@@ -84,7 +86,7 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
try {
|
||||
$user = $this->createNewFromLdapAndCreds($userDetails, $credentials);
|
||||
} catch (UserRegistrationException $exception) {
|
||||
throw new LoginAttemptException($exception->message);
|
||||
throw new LoginAttemptException($exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,17 @@ use Illuminate\Support\Facades\Log;
|
||||
*/
|
||||
class LdapService
|
||||
{
|
||||
protected $ldap;
|
||||
protected $groupSyncService;
|
||||
protected Ldap $ldap;
|
||||
protected GroupSyncService $groupSyncService;
|
||||
protected UserAvatars $userAvatars;
|
||||
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
protected $ldapConnection;
|
||||
protected $userAvatars;
|
||||
protected $config;
|
||||
protected $enabled;
|
||||
|
||||
protected array $config;
|
||||
protected bool $enabled;
|
||||
|
||||
/**
|
||||
* LdapService constructor.
|
||||
@@ -274,6 +279,7 @@ class LdapService
|
||||
* Get the groups a user is a part of on ldap.
|
||||
*
|
||||
* @throws LdapException
|
||||
* @throws JsonDebugException
|
||||
*/
|
||||
public function getUserGroups(string $userName): array
|
||||
{
|
||||
@@ -285,8 +291,17 @@ class LdapService
|
||||
}
|
||||
|
||||
$userGroups = $this->groupFilter($user);
|
||||
$allGroups = $this->getGroupsRecursive($userGroups, []);
|
||||
|
||||
return $this->getGroupsRecursive($userGroups, []);
|
||||
if ($this->config['dump_user_groups']) {
|
||||
throw new JsonDebugException([
|
||||
'details_from_ldap' => $user,
|
||||
'parsed_direct_user_groups' => $userGroups,
|
||||
'parsed_recursive_user_groups' => $allGroups,
|
||||
]);
|
||||
}
|
||||
|
||||
return $allGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -369,6 +384,7 @@ class LdapService
|
||||
* Sync the LDAP groups to the user roles for the current user.
|
||||
*
|
||||
* @throws LdapException
|
||||
* @throws JsonDebugException
|
||||
*/
|
||||
public function syncGroups(User $user, string $username)
|
||||
{
|
||||
|
||||
9
app/Auth/Access/Oidc/OidcException.php
Normal file
9
app/Auth/Access/Oidc/OidcException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access\Oidc;
|
||||
|
||||
use Exception;
|
||||
|
||||
class OidcException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace BookStack\Auth\Access\Oidc;
|
||||
|
||||
class OidcIssuerDiscoveryException extends \Exception
|
||||
use Exception;
|
||||
|
||||
class OidcIssuerDiscoveryException extends Exception
|
||||
{
|
||||
}
|
||||
|
||||
@@ -60,8 +60,11 @@ class OidcJwtSigningKey
|
||||
*/
|
||||
protected function loadFromJwkArray(array $jwk)
|
||||
{
|
||||
if ($jwk['alg'] !== 'RS256') {
|
||||
throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$jwk['alg']}");
|
||||
// 'alg' is optional for a JWK, but we will still attempt to validate if
|
||||
// it exists otherwise presume it will be compatible.
|
||||
$alg = $jwk['alg'] ?? null;
|
||||
if ($jwk['kty'] !== 'RSA' || !(is_null($alg) || $alg === 'RS256')) {
|
||||
throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$alg}");
|
||||
}
|
||||
|
||||
if (empty($jwk['use'])) {
|
||||
|
||||
@@ -164,7 +164,9 @@ class OidcProviderSettings
|
||||
protected function filterKeys(array $keys): array
|
||||
{
|
||||
return array_filter($keys, function (array $key) {
|
||||
return $key['kty'] === 'RSA' && $key['use'] === 'sig' && $key['alg'] === 'RS256';
|
||||
$alg = $key['alg'] ?? null;
|
||||
|
||||
return $key['kty'] === 'RSA' && $key['use'] === 'sig' && (is_null($alg) || $alg === 'RS256');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,12 @@ use BookStack\Auth\Access\LoginService;
|
||||
use BookStack\Auth\Access\RegistrationService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\JsonDebugException;
|
||||
use BookStack\Exceptions\OpenIdConnectException;
|
||||
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use function config;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
|
||||
use Psr\Http\Client\ClientInterface as HttpClient;
|
||||
use function trans;
|
||||
use function url;
|
||||
@@ -25,9 +23,9 @@ use function url;
|
||||
*/
|
||||
class OidcService
|
||||
{
|
||||
protected $registrationService;
|
||||
protected $loginService;
|
||||
protected $httpClient;
|
||||
protected RegistrationService $registrationService;
|
||||
protected LoginService $loginService;
|
||||
protected HttpClient $httpClient;
|
||||
|
||||
/**
|
||||
* OpenIdService constructor.
|
||||
@@ -42,6 +40,8 @@ class OidcService
|
||||
/**
|
||||
* Initiate an authorization flow.
|
||||
*
|
||||
* @throws OidcException
|
||||
*
|
||||
* @return array{url: string, state: string}
|
||||
*/
|
||||
public function login(): array
|
||||
@@ -57,14 +57,15 @@ class OidcService
|
||||
|
||||
/**
|
||||
* Process the Authorization response from the authorization server and
|
||||
* return the matching, or new if registration active, user matched to
|
||||
* the authorization server.
|
||||
* Returns null if not authenticated.
|
||||
* return the matching, or new if registration active, user matched to the
|
||||
* authorization server. Throws if the user cannot be auth if not authenticated.
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws JsonDebugException
|
||||
* @throws OidcException
|
||||
* @throws StoppedAuthenticationException
|
||||
* @throws IdentityProviderException
|
||||
*/
|
||||
public function processAuthorizeResponse(?string $authorizationCode): ?User
|
||||
public function processAuthorizeResponse(?string $authorizationCode): User
|
||||
{
|
||||
$settings = $this->getProviderSettings();
|
||||
$provider = $this->getProvider($settings);
|
||||
@@ -78,8 +79,7 @@ class OidcService
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OidcIssuerDiscoveryException
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws OidcException
|
||||
*/
|
||||
protected function getProviderSettings(): OidcProviderSettings
|
||||
{
|
||||
@@ -100,7 +100,11 @@ class OidcService
|
||||
|
||||
// Run discovery
|
||||
if ($config['discover'] ?? false) {
|
||||
$settings->discoverFromIssuer($this->httpClient, Cache::store(null), 15);
|
||||
try {
|
||||
$settings->discoverFromIssuer($this->httpClient, Cache::store(null), 15);
|
||||
} catch (OidcIssuerDiscoveryException $exception) {
|
||||
throw new OidcException('OIDC Discovery Error: ' . $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$settings->validate();
|
||||
@@ -161,9 +165,8 @@ class OidcService
|
||||
* Processes a received access token for a user. Login the user when
|
||||
* they exist, optionally registering them automatically.
|
||||
*
|
||||
* @throws OpenIdConnectException
|
||||
* @throws OidcException
|
||||
* @throws JsonDebugException
|
||||
* @throws UserRegistrationException
|
||||
* @throws StoppedAuthenticationException
|
||||
*/
|
||||
protected function processAccessTokenCallback(OidcAccessToken $accessToken, OidcProviderSettings $settings): User
|
||||
@@ -182,28 +185,28 @@ class OidcService
|
||||
try {
|
||||
$idToken->validate($settings->clientId);
|
||||
} catch (OidcInvalidTokenException $exception) {
|
||||
throw new OpenIdConnectException("ID token validate failed with error: {$exception->getMessage()}");
|
||||
throw new OidcException("ID token validate failed with error: {$exception->getMessage()}");
|
||||
}
|
||||
|
||||
$userDetails = $this->getUserDetails($idToken);
|
||||
$isLoggedIn = auth()->check();
|
||||
|
||||
if (empty($userDetails['email'])) {
|
||||
throw new OpenIdConnectException(trans('errors.oidc_no_email_address'));
|
||||
throw new OidcException(trans('errors.oidc_no_email_address'));
|
||||
}
|
||||
|
||||
if ($isLoggedIn) {
|
||||
throw new OpenIdConnectException(trans('errors.oidc_already_logged_in'), '/login');
|
||||
throw new OidcException(trans('errors.oidc_already_logged_in'));
|
||||
}
|
||||
|
||||
$user = $this->registrationService->findOrRegister(
|
||||
$userDetails['name'],
|
||||
$userDetails['email'],
|
||||
$userDetails['external_id']
|
||||
);
|
||||
|
||||
if ($user === null) {
|
||||
throw new OpenIdConnectException(trans('errors.oidc_user_not_registered', ['name' => $userDetails['external_id']]), '/login');
|
||||
try {
|
||||
$user = $this->registrationService->findOrRegister(
|
||||
$userDetails['name'],
|
||||
$userDetails['email'],
|
||||
$userDetails['external_id']
|
||||
);
|
||||
} catch (UserRegistrationException $exception) {
|
||||
throw new OidcException($exception->getMessage());
|
||||
}
|
||||
|
||||
$this->loginService->login($user, 'oidc');
|
||||
|
||||
@@ -96,7 +96,8 @@ class RegistrationService
|
||||
}
|
||||
|
||||
// Create the user
|
||||
$newUser = $this->userRepo->registerNew($userData, $emailConfirmed);
|
||||
$newUser = $this->userRepo->createWithoutActivity($userData, $emailConfirmed);
|
||||
$newUser->attachDefaultRole();
|
||||
|
||||
// Assign social account if given
|
||||
if ($socialAccount) {
|
||||
|
||||
39
app/Auth/Queries/AllUsersPaginatedAndSorted.php
Normal file
39
app/Auth/Queries/AllUsersPaginatedAndSorted.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Queries;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions in a paginated format.
|
||||
* Note: Due to the use of email search this should only be used when
|
||||
* user is assumed to be trusted. (Admin users).
|
||||
* Email search can be abused to extract email addresses.
|
||||
*/
|
||||
class AllUsersPaginatedAndSorted
|
||||
{
|
||||
/**
|
||||
* @param array{sort: string, order: string, search: string} $sortData
|
||||
*/
|
||||
public function run(int $count, array $sortData): LengthAwarePaginator
|
||||
{
|
||||
$sort = $sortData['sort'];
|
||||
|
||||
$query = User::query()->select(['*'])
|
||||
->scopes(['withLastActivityAt'])
|
||||
->with(['roles', 'avatar'])
|
||||
->withCount('mfaValues')
|
||||
->orderBy($sort, $sortData['order']);
|
||||
|
||||
if ($sortData['search']) {
|
||||
$term = '%' . $sortData['search'] . '%';
|
||||
$query->where(function ($query) use ($term) {
|
||||
$query->where('name', 'like', $term)
|
||||
->orWhere('email', 'like', $term);
|
||||
});
|
||||
}
|
||||
|
||||
return $query->paginate($count);
|
||||
}
|
||||
}
|
||||
30
app/Auth/Queries/UserContentCounts.php
Normal file
30
app/Auth/Queries/UserContentCounts.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Queries;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
|
||||
/**
|
||||
* Get asset created counts for the given user.
|
||||
*/
|
||||
class UserContentCounts
|
||||
{
|
||||
/**
|
||||
* @return array{pages: int, chapters: int, books: int, shelves: int}
|
||||
*/
|
||||
public function run(User $user): array
|
||||
{
|
||||
$createdBy = ['created_by' => $user->id];
|
||||
|
||||
return [
|
||||
'pages' => Page::visible()->where($createdBy)->count(),
|
||||
'chapters' => Chapter::visible()->where($createdBy)->count(),
|
||||
'books' => Book::visible()->where($createdBy)->count(),
|
||||
'shelves' => Bookshelf::visible()->where($createdBy)->count(),
|
||||
];
|
||||
}
|
||||
}
|
||||
37
app/Auth/Queries/UserRecentlyCreatedContent.php
Normal file
37
app/Auth/Queries/UserRecentlyCreatedContent.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Queries;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
/**
|
||||
* Get the recently created content for the provided user.
|
||||
*/
|
||||
class UserRecentlyCreatedContent
|
||||
{
|
||||
/**
|
||||
* @return array{pages: Collection, chapters: Collection, books: Collection, shelves: Collection}
|
||||
*/
|
||||
public function run(User $user, int $count): array
|
||||
{
|
||||
$query = function (Builder $query) use ($user, $count) {
|
||||
return $query->orderBy('created_at', 'desc')
|
||||
->where('created_by', '=', $user->id)
|
||||
->take($count)
|
||||
->get();
|
||||
};
|
||||
|
||||
return [
|
||||
'pages' => $query(Page::visible()->where('draft', '=', false)),
|
||||
'chapters' => $query(Chapter::visible()),
|
||||
'books' => $query(Book::visible()),
|
||||
'shelves' => $query(Bookshelf::visible()),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ class Role extends Model implements Loggable
|
||||
|
||||
protected $fillable = ['display_name', 'description', 'external_auth_id'];
|
||||
|
||||
protected $hidden = ['pivot'];
|
||||
|
||||
/**
|
||||
* The roles that belong to the role.
|
||||
*/
|
||||
|
||||
@@ -72,22 +72,20 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',
|
||||
'created_at', 'updated_at', 'image_id',
|
||||
'created_at', 'updated_at', 'image_id', 'roles', 'avatar', 'user_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* This holds the user's permissions when loaded.
|
||||
*
|
||||
* @var ?Collection
|
||||
*/
|
||||
protected $permissions;
|
||||
protected ?Collection $permissions;
|
||||
|
||||
/**
|
||||
* This holds the default user when loaded.
|
||||
*
|
||||
* @var null|User
|
||||
*/
|
||||
protected static $defaultUser = null;
|
||||
protected static ?User $defaultUser = null;
|
||||
|
||||
/**
|
||||
* Returns the default public user.
|
||||
@@ -146,7 +144,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function attachDefaultRole(): void
|
||||
{
|
||||
$roleId = setting('registration-role');
|
||||
$roleId = intval(setting('registration-role'));
|
||||
if ($roleId && $this->roles()->where('id', '=', $roleId)->count() === 0) {
|
||||
$this->roles()->attach($roleId);
|
||||
}
|
||||
|
||||
@@ -2,30 +2,29 @@
|
||||
|
||||
namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\UserInviteService;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Exceptions\UserUpdateException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Uploads\UserAvatars;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UserRepo
|
||||
{
|
||||
protected $userAvatar;
|
||||
protected UserAvatars $userAvatar;
|
||||
protected UserInviteService $inviteService;
|
||||
|
||||
/**
|
||||
* UserRepo constructor.
|
||||
*/
|
||||
public function __construct(UserAvatars $userAvatar)
|
||||
public function __construct(UserAvatars $userAvatar, UserInviteService $inviteService)
|
||||
{
|
||||
$this->userAvatar = $userAvatar;
|
||||
$this->inviteService = $inviteService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,70 +52,164 @@ class UserRepo
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions.
|
||||
* Create a new basic instance of user with the given pre-validated data.
|
||||
*
|
||||
* @param array{name: string, email: string, password: ?string, external_auth_id: ?string, language: ?string, roles: ?array} $data
|
||||
*/
|
||||
public function getAllUsers(): Collection
|
||||
public function createWithoutActivity(array $data, bool $emailConfirmed = false): User
|
||||
{
|
||||
return User::query()->with('roles', 'avatar')->orderBy('name', 'asc')->get();
|
||||
}
|
||||
$user = new User();
|
||||
$user->name = $data['name'];
|
||||
$user->email = $data['email'];
|
||||
$user->password = bcrypt(empty($data['password']) ? Str::random(32) : $data['password']);
|
||||
$user->email_confirmed = $emailConfirmed;
|
||||
$user->external_auth_id = $data['external_auth_id'] ?? '';
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions in a paginated format.
|
||||
* Note: Due to the use of email search this should only be used when
|
||||
* user is assumed to be trusted. (Admin users).
|
||||
* Email search can be abused to extract email addresses.
|
||||
*/
|
||||
public function getAllUsersPaginatedAndSorted(int $count, array $sortData): LengthAwarePaginator
|
||||
{
|
||||
$sort = $sortData['sort'];
|
||||
$user->refreshSlug();
|
||||
$user->save();
|
||||
|
||||
$query = User::query()->select(['*'])
|
||||
->scopes(['withLastActivityAt'])
|
||||
->with(['roles', 'avatar'])
|
||||
->withCount('mfaValues')
|
||||
->orderBy($sort, $sortData['order']);
|
||||
|
||||
if ($sortData['search']) {
|
||||
$term = '%' . $sortData['search'] . '%';
|
||||
$query->where(function ($query) use ($term) {
|
||||
$query->where('name', 'like', $term)
|
||||
->orWhere('email', 'like', $term);
|
||||
});
|
||||
if (!empty($data['language'])) {
|
||||
setting()->putUser($user, 'language', $data['language']);
|
||||
}
|
||||
|
||||
return $query->paginate($count);
|
||||
}
|
||||
if (isset($data['roles'])) {
|
||||
$this->setUserRoles($user, $data['roles']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user and attaches a role to them.
|
||||
*/
|
||||
public function registerNew(array $data, bool $emailConfirmed = false): User
|
||||
{
|
||||
$user = $this->create($data, $emailConfirmed);
|
||||
$user->attachDefaultRole();
|
||||
$this->downloadAndAssignUserAvatar($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a user to a system-level role.
|
||||
* As per "createWithoutActivity" but records a "create" activity.
|
||||
*
|
||||
* @throws NotFoundException
|
||||
* @param array{name: string, email: string, password: ?string, external_auth_id: ?string, language: ?string, roles: ?array} $data
|
||||
*/
|
||||
public function attachSystemRole(User $user, string $systemRoleName)
|
||||
public function create(array $data, bool $sendInvite = false): User
|
||||
{
|
||||
$role = Role::getSystemRole($systemRoleName);
|
||||
if (is_null($role)) {
|
||||
throw new NotFoundException("Role '{$systemRoleName}' not found");
|
||||
$user = $this->createWithoutActivity($data, true);
|
||||
|
||||
if ($sendInvite) {
|
||||
$this->inviteService->sendInvitation($user);
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::USER_CREATE, $user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given user with the given data.
|
||||
*
|
||||
* @param array{name: ?string, email: ?string, external_auth_id: ?string, password: ?string, roles: ?array<int>, language: ?string} $data
|
||||
*
|
||||
* @throws UserUpdateException
|
||||
*/
|
||||
public function update(User $user, array $data, bool $manageUsersAllowed): User
|
||||
{
|
||||
if (!empty($data['name'])) {
|
||||
$user->name = $data['name'];
|
||||
$user->refreshSlug();
|
||||
}
|
||||
|
||||
if (!empty($data['email']) && $manageUsersAllowed) {
|
||||
$user->email = $data['email'];
|
||||
}
|
||||
|
||||
if (!empty($data['external_auth_id']) && $manageUsersAllowed) {
|
||||
$user->external_auth_id = $data['external_auth_id'];
|
||||
}
|
||||
|
||||
if (isset($data['roles']) && $manageUsersAllowed) {
|
||||
$this->setUserRoles($user, $data['roles']);
|
||||
}
|
||||
|
||||
if (!empty($data['password'])) {
|
||||
$user->password = bcrypt($data['password']);
|
||||
}
|
||||
|
||||
if (!empty($data['language'])) {
|
||||
setting()->putUser($user, 'language', $data['language']);
|
||||
}
|
||||
|
||||
$user->save();
|
||||
Activity::add(ActivityType::USER_UPDATE, $user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given user from storage, Delete all related content.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroy(User $user, ?int $newOwnerId = null)
|
||||
{
|
||||
$this->ensureDeletable($user);
|
||||
|
||||
$user->socialAccounts()->delete();
|
||||
$user->apiTokens()->delete();
|
||||
$user->favourites()->delete();
|
||||
$user->mfaValues()->delete();
|
||||
$user->delete();
|
||||
|
||||
// Delete user profile images
|
||||
$this->userAvatar->destroyAllForUser($user);
|
||||
|
||||
if (!empty($newOwnerId)) {
|
||||
$newOwner = User::query()->find($newOwnerId);
|
||||
if (!is_null($newOwner)) {
|
||||
$this->migrateOwnership($user, $newOwner);
|
||||
}
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::USER_DELETE, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotifyException
|
||||
*/
|
||||
protected function ensureDeletable(User $user): void
|
||||
{
|
||||
if ($this->isOnlyAdmin($user)) {
|
||||
throw new NotifyException(trans('errors.users_cannot_delete_only_admin'), $user->getEditUrl());
|
||||
}
|
||||
|
||||
if ($user->system_name === 'public') {
|
||||
throw new NotifyException(trans('errors.users_cannot_delete_guest'), $user->getEditUrl());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate ownership of items in the system from one user to another.
|
||||
*/
|
||||
protected function migrateOwnership(User $fromUser, User $toUser)
|
||||
{
|
||||
$entities = (new EntityProvider())->all();
|
||||
foreach ($entities as $instance) {
|
||||
$instance->newQuery()->where('owned_by', '=', $fromUser->id)
|
||||
->update(['owned_by' => $toUser->id]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an avatar image for a user and set it as their avatar.
|
||||
* Returns early if avatars disabled or not set in config.
|
||||
*/
|
||||
protected function downloadAndAssignUserAvatar(User $user): void
|
||||
{
|
||||
try {
|
||||
$this->userAvatar->fetchAndAssignToUser($user);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to save user avatar image');
|
||||
}
|
||||
$user->attachRole($role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the give user is the only admin.
|
||||
*/
|
||||
public function isOnlyAdmin(User $user): bool
|
||||
protected function isOnlyAdmin(User $user): bool
|
||||
{
|
||||
if (!$user->hasSystemRole('admin')) {
|
||||
return false;
|
||||
@@ -135,7 +228,7 @@ class UserRepo
|
||||
*
|
||||
* @throws UserUpdateException
|
||||
*/
|
||||
public function setUserRoles(User $user, array $roles)
|
||||
protected function setUserRoles(User $user, array $roles)
|
||||
{
|
||||
if ($this->demotingLastAdmin($user, $roles)) {
|
||||
throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl());
|
||||
@@ -159,117 +252,4 @@ class UserRepo
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new basic instance of user.
|
||||
*/
|
||||
public function create(array $data, bool $emailConfirmed = false): User
|
||||
{
|
||||
$details = [
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password']),
|
||||
'email_confirmed' => $emailConfirmed,
|
||||
'external_auth_id' => $data['external_auth_id'] ?? '',
|
||||
];
|
||||
|
||||
$user = new User();
|
||||
$user->forceFill($details);
|
||||
$user->refreshSlug();
|
||||
$user->save();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given user from storage, Delete all related content.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroy(User $user, ?int $newOwnerId = null)
|
||||
{
|
||||
$user->socialAccounts()->delete();
|
||||
$user->apiTokens()->delete();
|
||||
$user->favourites()->delete();
|
||||
$user->mfaValues()->delete();
|
||||
$user->delete();
|
||||
|
||||
// Delete user profile images
|
||||
$this->userAvatar->destroyAllForUser($user);
|
||||
|
||||
if (!empty($newOwnerId)) {
|
||||
$newOwner = User::query()->find($newOwnerId);
|
||||
if (!is_null($newOwner)) {
|
||||
$this->migrateOwnership($user, $newOwner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate ownership of items in the system from one user to another.
|
||||
*/
|
||||
protected function migrateOwnership(User $fromUser, User $toUser)
|
||||
{
|
||||
$entities = (new EntityProvider())->all();
|
||||
foreach ($entities as $instance) {
|
||||
$instance->newQuery()->where('owned_by', '=', $fromUser->id)
|
||||
->update(['owned_by' => $toUser->id]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recently created content for this given user.
|
||||
*/
|
||||
public function getRecentlyCreated(User $user, int $count = 20): array
|
||||
{
|
||||
$query = function (Builder $query) use ($user, $count) {
|
||||
return $query->orderBy('created_at', 'desc')
|
||||
->where('created_by', '=', $user->id)
|
||||
->take($count)
|
||||
->get();
|
||||
};
|
||||
|
||||
return [
|
||||
'pages' => $query(Page::visible()->where('draft', '=', false)),
|
||||
'chapters' => $query(Chapter::visible()),
|
||||
'books' => $query(Book::visible()),
|
||||
'shelves' => $query(Bookshelf::visible()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset created counts for the give user.
|
||||
*/
|
||||
public function getAssetCounts(User $user): array
|
||||
{
|
||||
$createdBy = ['created_by' => $user->id];
|
||||
|
||||
return [
|
||||
'pages' => Page::visible()->where($createdBy)->count(),
|
||||
'chapters' => Chapter::visible()->where($createdBy)->count(),
|
||||
'books' => Book::visible()->where($createdBy)->count(),
|
||||
'shelves' => Bookshelf::visible()->where($createdBy)->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles in the system that are assignable to a user.
|
||||
*/
|
||||
public function getAllRoles(): Collection
|
||||
{
|
||||
return Role::query()->orderBy('display_name', 'asc')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an avatar image for a user and set it as their avatar.
|
||||
* Returns early if avatars disabled or not set in config.
|
||||
*/
|
||||
public function downloadAndAssignUserAvatar(User $user): void
|
||||
{
|
||||
try {
|
||||
$this->userAvatar->fetchAndAssignToUser($user);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to save user avatar image');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,13 @@ return [
|
||||
// Space separated if multiple. BookStack host domain is auto-inferred.
|
||||
'iframe_hosts' => env('ALLOWED_IFRAME_HOSTS', null),
|
||||
|
||||
// A list of sources/hostnames that can be loaded within iframes within BookStack.
|
||||
// Space separated if multiple. BookStack host domain is auto-inferred.
|
||||
// Can be set to a lone "*" to allow all sources for iframe content (Not advised).
|
||||
// Defaults to a set of common services.
|
||||
// Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
|
||||
'iframe_sources' => env('ALLOWED_IFRAME_SOURCES', 'https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com'),
|
||||
|
||||
// Application timezone for back-end date functions.
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
|
||||
@@ -64,7 +71,7 @@ return [
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
|
||||
// Locales available
|
||||
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'et', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', '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', 'et', 'eu', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', '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',
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
$dompdfPaperSizeMap = [
|
||||
'a4' => 'a4',
|
||||
'letter' => 'letter',
|
||||
];
|
||||
|
||||
return [
|
||||
|
||||
@@ -150,7 +154,7 @@ return [
|
||||
*
|
||||
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
|
||||
*/
|
||||
'default_paper_size' => 'a4',
|
||||
'default_paper_size' => $dompdfPaperSizeMap[env('EXPORT_PAGE_SIZE', 'a4')] ?? 'a4',
|
||||
|
||||
/**
|
||||
* The default font family.
|
||||
|
||||
@@ -119,6 +119,7 @@ return [
|
||||
'ldap' => [
|
||||
'server' => env('LDAP_SERVER', false),
|
||||
'dump_user_details' => env('LDAP_DUMP_USER_DETAILS', false),
|
||||
'dump_user_groups' => env('LDAP_DUMP_USER_GROUPS', false),
|
||||
'dn' => env('LDAP_DN', false),
|
||||
'pass' => env('LDAP_PASS', false),
|
||||
'base_dn' => env('LDAP_BASE_DN', false),
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
$snappyPaperSizeMap = [
|
||||
'a4' => 'A4',
|
||||
'letter' => 'Letter',
|
||||
];
|
||||
|
||||
return [
|
||||
'pdf' => [
|
||||
@@ -14,7 +18,8 @@ return [
|
||||
'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false),
|
||||
'timeout' => false,
|
||||
'options' => [
|
||||
'outline' => true,
|
||||
'outline' => true,
|
||||
'page-size' => $snappyPaperSizeMap[env('EXPORT_PAGE_SIZE', 'a4')] ?? 'A4',
|
||||
],
|
||||
'env' => [],
|
||||
],
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
use Symfony\Component\Console\Command\Command as SymfonyCommand;
|
||||
@@ -19,7 +22,8 @@ class CreateAdmin extends Command
|
||||
protected $signature = 'bookstack:create-admin
|
||||
{--email= : The email address for the new admin user}
|
||||
{--name= : The name of the new admin user}
|
||||
{--password= : The password to assign to the new admin user}';
|
||||
{--password= : The password to assign to the new admin user}
|
||||
{--external-auth-id= : The external authentication system id for the new admin user (SAML2/LDAP/OIDC)}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -42,28 +46,35 @@ class CreateAdmin extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
* @throws NotFoundException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$details = $this->options();
|
||||
$details = $this->snakeCaseOptions();
|
||||
|
||||
if (empty($details['email'])) {
|
||||
$details['email'] = $this->ask('Please specify an email address for the new admin user');
|
||||
}
|
||||
|
||||
if (empty($details['name'])) {
|
||||
$details['name'] = $this->ask('Please specify a name for the new admin user');
|
||||
}
|
||||
|
||||
if (empty($details['password'])) {
|
||||
$details['password'] = $this->ask('Please specify a password for the new admin user (8 characters min)');
|
||||
if (empty($details['external_auth_id'])) {
|
||||
$details['password'] = $this->ask('Please specify a password for the new admin user (8 characters min)');
|
||||
} else {
|
||||
$details['password'] = Str::random(32);
|
||||
}
|
||||
}
|
||||
|
||||
$validator = Validator::make($details, [
|
||||
'email' => ['required', 'email', 'min:5', new Unique('users', 'email')],
|
||||
'name' => ['required', 'min:2'],
|
||||
'password' => ['required', Password::default()],
|
||||
'email' => ['required', 'email', 'min:5', new Unique('users', 'email')],
|
||||
'name' => ['required', 'min:2'],
|
||||
'password' => ['required_without:external_auth_id', Password::default()],
|
||||
'external_auth_id' => ['required_without:password'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@@ -74,9 +85,8 @@ class CreateAdmin extends Command
|
||||
return SymfonyCommand::FAILURE;
|
||||
}
|
||||
|
||||
$user = $this->userRepo->create($validator->validated());
|
||||
$this->userRepo->attachSystemRole($user, 'admin');
|
||||
$this->userRepo->downloadAndAssignUserAvatar($user);
|
||||
$user = $this->userRepo->createWithoutActivity($validator->validated());
|
||||
$user->attachRole(Role::getSystemRole('admin'));
|
||||
$user->email_confirmed = true;
|
||||
$user->save();
|
||||
|
||||
@@ -84,4 +94,14 @@ class CreateAdmin extends Command
|
||||
|
||||
return SymfonyCommand::SUCCESS;
|
||||
}
|
||||
|
||||
protected function snakeCaseOptions(): array
|
||||
{
|
||||
$returnOpts = [];
|
||||
foreach ($this->options() as $key => $value) {
|
||||
$returnOpts[str_replace('-', '_', $key)] = $value;
|
||||
}
|
||||
|
||||
return $returnOpts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ class DeleteUsers extends Command
|
||||
*/
|
||||
protected $signature = 'bookstack:delete-users';
|
||||
|
||||
protected $user;
|
||||
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
@@ -26,9 +24,8 @@ class DeleteUsers extends Command
|
||||
*/
|
||||
protected $description = 'Delete users that are not "admin" or system users';
|
||||
|
||||
public function __construct(User $user, UserRepo $userRepo)
|
||||
public function __construct(UserRepo $userRepo)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -38,8 +35,8 @@ class DeleteUsers extends Command
|
||||
$confirm = $this->ask('This will delete all users from the system that are not "admin" or system users. Are you sure you want to continue? (Type "yes" to continue)');
|
||||
$numDeleted = 0;
|
||||
if (strtolower(trim($confirm)) === 'yes') {
|
||||
$totalUsers = $this->user->count();
|
||||
$users = $this->user->where('system_name', '=', null)->with('roles')->get();
|
||||
$totalUsers = User::query()->count();
|
||||
$users = User::query()->whereNull('system_name')->with('roles')->get();
|
||||
foreach ($users as $user) {
|
||||
if ($user->hasSystemRole('admin')) {
|
||||
// don't delete users with "admin" role
|
||||
|
||||
@@ -59,7 +59,7 @@ class Deletion extends Model implements Loggable
|
||||
/**
|
||||
* Get a URL for this specific deletion.
|
||||
*/
|
||||
public function getUrl($path): string
|
||||
public function getUrl(string $path = 'restore'): string
|
||||
{
|
||||
return url("/settings/recycle-bin/{$this->id}/" . ltrim($path, '/'));
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property string $slug
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property Carbon $deleted_at
|
||||
* @property int $created_by
|
||||
* @property int $updated_by
|
||||
* @property bool $restricted
|
||||
|
||||
@@ -10,19 +10,22 @@ use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
/**
|
||||
* Class Page.
|
||||
*
|
||||
* @property int $chapter_id
|
||||
* @property string $html
|
||||
* @property string $markdown
|
||||
* @property string $text
|
||||
* @property bool $template
|
||||
* @property bool $draft
|
||||
* @property int $revision_count
|
||||
* @property Chapter $chapter
|
||||
* @property Collection $attachments
|
||||
* @property int $chapter_id
|
||||
* @property string $html
|
||||
* @property string $markdown
|
||||
* @property string $text
|
||||
* @property bool $template
|
||||
* @property bool $draft
|
||||
* @property int $revision_count
|
||||
* @property Chapter $chapter
|
||||
* @property Collection $attachments
|
||||
* @property Collection $revisions
|
||||
* @property PageRevision $currentRevision
|
||||
*/
|
||||
class Page extends BookChild
|
||||
{
|
||||
@@ -82,6 +85,19 @@ class Page extends BookChild
|
||||
->orderBy('id', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current revision for the page if existing.
|
||||
*
|
||||
* @return PageRevision|null
|
||||
*/
|
||||
public function currentRevision(): HasOne
|
||||
{
|
||||
return $this->hasOne(PageRevision::class)
|
||||
->where('type', '=', 'version')
|
||||
->orderBy('created_at', 'desc')
|
||||
->orderBy('id', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all revision instances assigned to this page.
|
||||
* Includes all types of revisions.
|
||||
@@ -117,16 +133,6 @@ class Page extends BookChild
|
||||
return url('/' . implode('/', $parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current revision for the page if existing.
|
||||
*
|
||||
* @return PageRevision|null
|
||||
*/
|
||||
public function getCurrentRevision()
|
||||
{
|
||||
return $this->revisions()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this page for JSON display.
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
/**
|
||||
* Class PageRevision.
|
||||
*
|
||||
* @property mixed $id
|
||||
* @property int $page_id
|
||||
* @property string $slug
|
||||
* @property string $book_slug
|
||||
@@ -27,6 +28,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
class PageRevision extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
|
||||
protected $hidden = ['html', 'markdown', 'restricted', 'text'];
|
||||
|
||||
/**
|
||||
* Get the user that created the page revision.
|
||||
@@ -46,19 +48,10 @@ class PageRevision extends Model
|
||||
|
||||
/**
|
||||
* Get the url for this revision.
|
||||
*
|
||||
* @param null|string $path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = null)
|
||||
public function getUrl(string $path = ''): string
|
||||
{
|
||||
$url = $this->page->getUrl() . '/revisions/' . $this->id;
|
||||
if ($path) {
|
||||
return $url . '/' . trim($path, '/');
|
||||
}
|
||||
|
||||
return $url;
|
||||
return $this->page->getUrl('/revisions/' . $this->id . '/' . ltrim($path, '/'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\TrashCan;
|
||||
use BookStack\Exceptions\MoveOperationException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Facades\Activity;
|
||||
use Exception;
|
||||
|
||||
@@ -85,15 +86,19 @@ class ChapterRepo
|
||||
* 'book:<id>' (book:5).
|
||||
*
|
||||
* @throws MoveOperationException
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function move(Chapter $chapter, string $parentIdentifier): Book
|
||||
{
|
||||
/** @var Book $parent */
|
||||
$parent = $this->findParentByIdentifier($parentIdentifier);
|
||||
if (is_null($parent)) {
|
||||
throw new MoveOperationException('Book to move chapter into not found');
|
||||
}
|
||||
|
||||
if (!userCan('chapter-create', $parent)) {
|
||||
throw new PermissionsException('User does not have permission to create a chapter within the chosen book');
|
||||
}
|
||||
|
||||
$chapter->changeBook($parent->id);
|
||||
$chapter->rebuildPermissions();
|
||||
Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
|
||||
|
||||
@@ -328,7 +328,7 @@ class PageRepo
|
||||
public function move(Page $page, string $parentIdentifier): Entity
|
||||
{
|
||||
$parent = $this->findParentByIdentifier($parentIdentifier);
|
||||
if ($parent === null) {
|
||||
if (is_null($parent)) {
|
||||
throw new MoveOperationException('Book or chapter to move page into not found');
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Exceptions\SortOperationException;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class BookContents
|
||||
@@ -107,111 +106,209 @@ class BookContents
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the books content using the given map.
|
||||
* The map is a single-dimension collection of objects in the following format:
|
||||
* {
|
||||
* +"id": "294" (ID of item)
|
||||
* +"sort": 1 (Sort order index)
|
||||
* +"parentChapter": false (ID of parent chapter, as string, or false)
|
||||
* +"type": "page" (Entity type of item)
|
||||
* +"book": "1" (Id of book to place item in)
|
||||
* }.
|
||||
*
|
||||
* Sort the books content using the given sort map.
|
||||
* Returns a list of books that were involved in the operation.
|
||||
*
|
||||
* @throws SortOperationException
|
||||
* @returns Book[]
|
||||
*/
|
||||
public function sortUsingMap(Collection $sortMap): Collection
|
||||
public function sortUsingMap(BookSortMap $sortMap): array
|
||||
{
|
||||
// Load models into map
|
||||
$this->loadModelsIntoSortMap($sortMap);
|
||||
$booksInvolved = $this->getBooksInvolvedInSort($sortMap);
|
||||
$modelMap = $this->loadModelsFromSortMap($sortMap);
|
||||
|
||||
// Sort our changes from our map to be chapters first
|
||||
// Since they need to be process to ensure book alignment for child page changes.
|
||||
$sortMapItems = $sortMap->all();
|
||||
usort($sortMapItems, function (BookSortMapItem $itemA, BookSortMapItem $itemB) {
|
||||
$aScore = $itemA->type === 'page' ? 2 : 1;
|
||||
$bScore = $itemB->type === 'page' ? 2 : 1;
|
||||
|
||||
return $aScore - $bScore;
|
||||
});
|
||||
|
||||
// Perform the sort
|
||||
$sortMap->each(function ($mapItem) {
|
||||
$this->applySortUpdates($mapItem);
|
||||
});
|
||||
foreach ($sortMapItems as $item) {
|
||||
$this->applySortUpdates($item, $modelMap);
|
||||
}
|
||||
|
||||
// Update permissions and activity.
|
||||
$booksInvolved->each(function (Book $book) {
|
||||
/** @var Book[] $booksInvolved */
|
||||
$booksInvolved = array_values(array_filter($modelMap, function (string $key) {
|
||||
return strpos($key, 'book:') === 0;
|
||||
}, ARRAY_FILTER_USE_KEY));
|
||||
|
||||
// Update permissions of books involved
|
||||
foreach ($booksInvolved as $book) {
|
||||
$book->rebuildPermissions();
|
||||
});
|
||||
}
|
||||
|
||||
return $booksInvolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the given sort map item, detect changes for the related model
|
||||
* and update it if required.
|
||||
* and update it if required. Changes where permissions are lacking will
|
||||
* be skipped and not throw an error.
|
||||
*
|
||||
* @param array<string, Entity> $modelMap
|
||||
*/
|
||||
protected function applySortUpdates(\stdClass $sortMapItem)
|
||||
protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
|
||||
{
|
||||
/** @var BookChild $model */
|
||||
$model = $sortMapItem->model;
|
||||
$model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;
|
||||
if (!$model) {
|
||||
return;
|
||||
}
|
||||
|
||||
$priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
|
||||
$bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
|
||||
$chapterChanged = ($model instanceof Page) && intval($model->chapter_id) !== $sortMapItem->parentChapter;
|
||||
$priorityChanged = $model->priority !== $sortMapItem->sort;
|
||||
$bookChanged = $model->book_id !== $sortMapItem->parentBookId;
|
||||
$chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;
|
||||
|
||||
// Stop if there's no change
|
||||
if (!$priorityChanged && !$bookChanged && !$chapterChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentParentKey = 'book:' . $model->book_id;
|
||||
if ($model instanceof Page && $model->chapter_id) {
|
||||
$currentParentKey = 'chapter:' . $model->chapter_id;
|
||||
}
|
||||
|
||||
$currentParent = $modelMap[$currentParentKey] ?? null;
|
||||
/** @var Book $newBook */
|
||||
$newBook = $modelMap['book:' . $sortMapItem->parentBookId] ?? null;
|
||||
/** @var ?Chapter $newChapter */
|
||||
$newChapter = $sortMapItem->parentChapterId ? ($modelMap['chapter:' . $sortMapItem->parentChapterId] ?? null) : null;
|
||||
|
||||
if (!$this->isSortChangePermissible($sortMapItem, $model, $currentParent, $newBook, $newChapter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Action the required changes
|
||||
if ($bookChanged) {
|
||||
$model->changeBook($sortMapItem->book);
|
||||
$model->changeBook($newBook->id);
|
||||
}
|
||||
|
||||
if ($chapterChanged) {
|
||||
$model->chapter_id = intval($sortMapItem->parentChapter);
|
||||
$model->save();
|
||||
$model->chapter_id = $newChapter->id ?? 0;
|
||||
}
|
||||
|
||||
if ($priorityChanged) {
|
||||
$model->priority = intval($sortMapItem->sort);
|
||||
$model->priority = $sortMapItem->sort;
|
||||
}
|
||||
|
||||
if ($chapterChanged || $priorityChanged) {
|
||||
$model->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has permissions to apply the given sorting change.
|
||||
* Is quite complex since items can gain a different parent change. Acts as a:
|
||||
* - Update of old parent element (Change of content/order).
|
||||
* - Update of sorted/moved element.
|
||||
* - Deletion of element (Relative to parent upon move).
|
||||
* - Creation of element within parent (Upon move to new parent).
|
||||
*/
|
||||
protected function isSortChangePermissible(BookSortMapItem $sortMapItem, BookChild $model, ?Entity $currentParent, ?Entity $newBook, ?Entity $newChapter): bool
|
||||
{
|
||||
// Stop if we can't see the current parent or new book.
|
||||
if (!$currentParent || !$newBook) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasNewParent = $newBook->id !== $model->book_id || ($model instanceof Page && $model->chapter_id !== ($sortMapItem->parentChapterId ?? 0));
|
||||
if ($model instanceof Chapter) {
|
||||
$hasPermission = userCan('book-update', $currentParent)
|
||||
&& userCan('book-update', $newBook)
|
||||
&& userCan('chapter-update', $model)
|
||||
&& (!$hasNewParent || userCan('chapter-create', $newBook))
|
||||
&& (!$hasNewParent || userCan('chapter-delete', $model));
|
||||
|
||||
if (!$hasPermission) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($model instanceof Page) {
|
||||
$parentPermission = ($currentParent instanceof Chapter) ? 'chapter-update' : 'book-update';
|
||||
$hasCurrentParentPermission = userCan($parentPermission, $currentParent);
|
||||
|
||||
// This needs to check if there was an intended chapter location in the original sort map
|
||||
// rather than inferring from the $newChapter since that variable may be null
|
||||
// due to other reasons (Visibility).
|
||||
$newParent = $sortMapItem->parentChapterId ? $newChapter : $newBook;
|
||||
if (!$newParent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasPageEditPermission = userCan('page-update', $model);
|
||||
$newParentInRightLocation = ($newParent instanceof Book || $newParent->book_id === $newBook->id);
|
||||
$newParentPermission = ($newParent instanceof Chapter) ? 'chapter-update' : 'book-update';
|
||||
$hasNewParentPermission = userCan($newParentPermission, $newParent);
|
||||
|
||||
$hasDeletePermissionIfMoving = (!$hasNewParent || userCan('page-delete', $model));
|
||||
$hasCreatePermissionIfMoving = (!$hasNewParent || userCan('page-create', $newParent));
|
||||
|
||||
$hasPermission = $hasCurrentParentPermission
|
||||
&& $newParentInRightLocation
|
||||
&& $hasNewParentPermission
|
||||
&& $hasPageEditPermission
|
||||
&& $hasDeletePermissionIfMoving
|
||||
&& $hasCreatePermissionIfMoving;
|
||||
|
||||
if (!$hasPermission) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load models from the database into the given sort map.
|
||||
*/
|
||||
protected function loadModelsIntoSortMap(Collection $sortMap): void
|
||||
{
|
||||
$keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) {
|
||||
return $sortMapItem->type . ':' . $sortMapItem->id;
|
||||
});
|
||||
$pageIds = $sortMap->where('type', '=', 'page')->pluck('id');
|
||||
$chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id');
|
||||
|
||||
$pages = Page::visible()->whereIn('id', $pageIds)->get();
|
||||
$chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
|
||||
|
||||
foreach ($pages as $page) {
|
||||
$sortItem = $keyMap->get('page:' . $page->id);
|
||||
$sortItem->model = $page;
|
||||
}
|
||||
|
||||
foreach ($chapters as $chapter) {
|
||||
$sortItem = $keyMap->get('chapter:' . $chapter->id);
|
||||
$sortItem->model = $chapter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the books involved in a sort.
|
||||
* The given sort map should have its models loaded first.
|
||||
*
|
||||
* @throws SortOperationException
|
||||
* @return array<string, Entity>
|
||||
*/
|
||||
protected function getBooksInvolvedInSort(Collection $sortMap): Collection
|
||||
protected function loadModelsFromSortMap(BookSortMap $sortMap): array
|
||||
{
|
||||
$bookIdsInvolved = collect([$this->book->id]);
|
||||
$bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book'));
|
||||
$bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id'));
|
||||
$bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
|
||||
$modelMap = [];
|
||||
$ids = [
|
||||
'chapter' => [],
|
||||
'page' => [],
|
||||
'book' => [],
|
||||
];
|
||||
|
||||
$books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
|
||||
|
||||
if (count($books) !== count($bookIdsInvolved)) {
|
||||
throw new SortOperationException('Could not find all books requested in sort operation');
|
||||
foreach ($sortMap->all() as $sortMapItem) {
|
||||
$ids[$sortMapItem->type][] = $sortMapItem->id;
|
||||
$ids['book'][] = $sortMapItem->parentBookId;
|
||||
if ($sortMapItem->parentChapterId) {
|
||||
$ids['chapter'][] = $sortMapItem->parentChapterId;
|
||||
}
|
||||
}
|
||||
|
||||
return $books;
|
||||
$pages = Page::visible()->whereIn('id', array_unique($ids['page']))->get(Page::$listAttributes);
|
||||
/** @var Page $page */
|
||||
foreach ($pages as $page) {
|
||||
$modelMap['page:' . $page->id] = $page;
|
||||
$ids['book'][] = $page->book_id;
|
||||
if ($page->chapter_id) {
|
||||
$ids['chapter'][] = $page->chapter_id;
|
||||
}
|
||||
}
|
||||
|
||||
$chapters = Chapter::visible()->whereIn('id', array_unique($ids['chapter']))->get();
|
||||
/** @var Chapter $chapter */
|
||||
foreach ($chapters as $chapter) {
|
||||
$modelMap['chapter:' . $chapter->id] = $chapter;
|
||||
$ids['book'][] = $chapter->book_id;
|
||||
}
|
||||
|
||||
$books = Book::visible()->whereIn('id', array_unique($ids['book']))->get();
|
||||
/** @var Book $book */
|
||||
foreach ($books as $book) {
|
||||
$modelMap['book:' . $book->id] = $book;
|
||||
}
|
||||
|
||||
return $modelMap;
|
||||
}
|
||||
}
|
||||
|
||||
44
app/Entities/Tools/BookSortMap.php
Normal file
44
app/Entities/Tools/BookSortMap.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
|
||||
class BookSortMap
|
||||
{
|
||||
/**
|
||||
* @var BookSortMapItem[]
|
||||
*/
|
||||
protected $mapData = [];
|
||||
|
||||
public function addItem(BookSortMapItem $mapItem): void
|
||||
{
|
||||
$this->mapData[] = $mapItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BookSortMapItem[]
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->mapData;
|
||||
}
|
||||
|
||||
public static function fromJson(string $json): self
|
||||
{
|
||||
$map = new BookSortMap();
|
||||
$mapData = json_decode($json);
|
||||
|
||||
foreach ($mapData as $mapDataItem) {
|
||||
$item = new BookSortMapItem(
|
||||
intval($mapDataItem->id),
|
||||
intval($mapDataItem->sort),
|
||||
$mapDataItem->parentChapter ? intval($mapDataItem->parentChapter) : null,
|
||||
$mapDataItem->type,
|
||||
intval($mapDataItem->book)
|
||||
);
|
||||
|
||||
$map->addItem($item);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
40
app/Entities/Tools/BookSortMapItem.php
Normal file
40
app/Entities/Tools/BookSortMapItem.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
|
||||
class BookSortMapItem
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $sort;
|
||||
|
||||
/**
|
||||
* @var ?int
|
||||
*/
|
||||
public $parentChapterId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $parentBookId;
|
||||
|
||||
public function __construct(int $id, int $sort, ?int $parentChapterId, string $type, int $parentBookId)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->sort = $sort;
|
||||
$this->parentChapterId = $parentChapterId;
|
||||
$this->type = $type;
|
||||
$this->parentBookId = $parentBookId;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use BookStack\Util\CspService;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMXPath;
|
||||
@@ -15,16 +16,18 @@ use Throwable;
|
||||
|
||||
class ExportFormatter
|
||||
{
|
||||
protected $imageService;
|
||||
protected $pdfGenerator;
|
||||
protected ImageService $imageService;
|
||||
protected PdfGenerator $pdfGenerator;
|
||||
protected CspService $cspService;
|
||||
|
||||
/**
|
||||
* ExportService constructor.
|
||||
*/
|
||||
public function __construct(ImageService $imageService, PdfGenerator $pdfGenerator)
|
||||
public function __construct(ImageService $imageService, PdfGenerator $pdfGenerator, CspService $cspService)
|
||||
{
|
||||
$this->imageService = $imageService;
|
||||
$this->pdfGenerator = $pdfGenerator;
|
||||
$this->cspService = $cspService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,8 +40,9 @@ class ExportFormatter
|
||||
{
|
||||
$page->html = (new PageContent($page))->render();
|
||||
$pageHtml = view('pages.export', [
|
||||
'page' => $page,
|
||||
'format' => 'html',
|
||||
'page' => $page,
|
||||
'format' => 'html',
|
||||
'cspContent' => $this->cspService->getCspMetaTagValue(),
|
||||
])->render();
|
||||
|
||||
return $this->containHtml($pageHtml);
|
||||
@@ -56,9 +60,10 @@ class ExportFormatter
|
||||
$page->html = (new PageContent($page))->render();
|
||||
});
|
||||
$html = view('chapters.export', [
|
||||
'chapter' => $chapter,
|
||||
'pages' => $pages,
|
||||
'format' => 'html',
|
||||
'chapter' => $chapter,
|
||||
'pages' => $pages,
|
||||
'format' => 'html',
|
||||
'cspContent' => $this->cspService->getCspMetaTagValue(),
|
||||
])->render();
|
||||
|
||||
return $this->containHtml($html);
|
||||
@@ -76,6 +81,7 @@ class ExportFormatter
|
||||
'book' => $book,
|
||||
'bookChildren' => $bookTree,
|
||||
'format' => 'html',
|
||||
'cspContent' => $this->cspService->getCspMetaTagValue(),
|
||||
])->render();
|
||||
|
||||
return $this->containHtml($html);
|
||||
@@ -92,6 +98,7 @@ class ExportFormatter
|
||||
$html = view('pages.export', [
|
||||
'page' => $page,
|
||||
'format' => 'pdf',
|
||||
'engine' => $this->pdfGenerator->getActiveEngine(),
|
||||
])->render();
|
||||
|
||||
return $this->htmlToPdf($html);
|
||||
@@ -113,6 +120,7 @@ class ExportFormatter
|
||||
'chapter' => $chapter,
|
||||
'pages' => $pages,
|
||||
'format' => 'pdf',
|
||||
'engine' => $this->pdfGenerator->getActiveEngine(),
|
||||
])->render();
|
||||
|
||||
return $this->htmlToPdf($html);
|
||||
@@ -130,6 +138,7 @@ class ExportFormatter
|
||||
'book' => $book,
|
||||
'bookChildren' => $bookTree,
|
||||
'format' => 'pdf',
|
||||
'engine' => $this->pdfGenerator->getActiveEngine(),
|
||||
])->render();
|
||||
|
||||
return $this->htmlToPdf($html);
|
||||
@@ -144,10 +153,31 @@ class ExportFormatter
|
||||
{
|
||||
$html = $this->containHtml($html);
|
||||
$html = $this->replaceIframesWithLinks($html);
|
||||
$html = $this->openDetailElements($html);
|
||||
|
||||
return $this->pdfGenerator->fromHtml($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Within the given HTML content, Open any detail blocks.
|
||||
*/
|
||||
protected function openDetailElements(string $html): string
|
||||
{
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
$xPath = new DOMXPath($doc);
|
||||
|
||||
$details = $xPath->query('//details');
|
||||
/** @var DOMElement $detail */
|
||||
foreach ($details as $detail) {
|
||||
$detail->setAttribute('open', 'open');
|
||||
}
|
||||
|
||||
return $doc->saveHTML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Within the given HTML content, replace any iframe elements
|
||||
* with anchor links within paragraph blocks.
|
||||
@@ -296,7 +326,7 @@ class ExportFormatter
|
||||
$text .= $this->pageToMarkdown($page) . "\n\n";
|
||||
}
|
||||
|
||||
return $text;
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,12 +338,12 @@ class ExportFormatter
|
||||
$text = '# ' . $book->name . "\n\n";
|
||||
foreach ($bookTree as $bookChild) {
|
||||
if ($bookChild instanceof Chapter) {
|
||||
$text .= $this->chapterToMarkdown($bookChild);
|
||||
$text .= $this->chapterToMarkdown($bookChild) . "\n\n";
|
||||
} else {
|
||||
$text .= $this->pageToMarkdown($bookChild);
|
||||
$text .= $this->pageToMarkdown($bookChild) . "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $text;
|
||||
return trim($text);
|
||||
}
|
||||
}
|
||||
|
||||
28
app/Entities/Tools/Markdown/CheckboxConverter.php
Normal file
28
app/Entities/Tools/Markdown/CheckboxConverter.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools\Markdown;
|
||||
|
||||
use League\HTMLToMarkdown\Converter\ConverterInterface;
|
||||
use League\HTMLToMarkdown\ElementInterface;
|
||||
|
||||
class CheckboxConverter implements ConverterInterface
|
||||
{
|
||||
public function convert(ElementInterface $element): string
|
||||
{
|
||||
if (strtolower($element->getAttribute('type')) === 'checkbox') {
|
||||
$isChecked = $element->getAttribute('checked') === 'checked';
|
||||
|
||||
return $isChecked ? ' [x] ' : ' [ ] ';
|
||||
}
|
||||
|
||||
return $element->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSupportedTags(): array
|
||||
{
|
||||
return ['input'];
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ class HtmlToMarkdown
|
||||
$environment->addConverter(new CustomParagraphConverter());
|
||||
$environment->addConverter(new PreformattedConverter());
|
||||
$environment->addConverter(new TextConverter());
|
||||
$environment->addConverter(new CheckboxConverter());
|
||||
|
||||
return $environment;
|
||||
}
|
||||
|
||||
@@ -109,15 +109,35 @@ class PageContent
|
||||
|
||||
/**
|
||||
* Convert all inline base64 content to uploaded image files.
|
||||
* Regex is used to locate the start of data-uri definitions then
|
||||
* manual looping over content is done to parse the whole data uri.
|
||||
* Attempting to capture the whole data uri using regex can cause PHP
|
||||
* PCRE limits to be hit with larger, multi-MB, files.
|
||||
*/
|
||||
protected function extractBase64ImagesFromMarkdown(string $markdown)
|
||||
{
|
||||
$matches = [];
|
||||
preg_match_all('/!\[.*?]\(.*?(data:image\/.*?)[)"\s]/', $markdown, $matches);
|
||||
$contentLength = strlen($markdown);
|
||||
$replacements = [];
|
||||
preg_match_all('/!\[.*?]\(.*?(data:image\/.{1,6};base64,)/', $markdown, $matches, PREG_OFFSET_CAPTURE);
|
||||
|
||||
foreach ($matches[1] as $base64Match) {
|
||||
$newUrl = $this->base64ImageUriToUploadedImageUrl($base64Match);
|
||||
$markdown = str_replace($base64Match, $newUrl, $markdown);
|
||||
foreach ($matches[1] as $base64MatchPair) {
|
||||
[$dataUri, $index] = $base64MatchPair;
|
||||
|
||||
for ($i = strlen($dataUri) + $index; $i < $contentLength; $i++) {
|
||||
$char = $markdown[$i];
|
||||
if ($char === ')' || $char === ' ' || $char === "\n" || $char === '"') {
|
||||
break;
|
||||
}
|
||||
$dataUri .= $char;
|
||||
}
|
||||
|
||||
$newUrl = $this->base64ImageUriToUploadedImageUrl($dataUri);
|
||||
$replacements[] = [$dataUri, $newUrl];
|
||||
}
|
||||
|
||||
foreach ($replacements as [$dataUri, $newUrl]) {
|
||||
$markdown = str_replace($dataUri, $newUrl, $markdown);
|
||||
}
|
||||
|
||||
return $markdown;
|
||||
@@ -219,6 +239,9 @@ class PageContent
|
||||
$html .= $doc->saveHTML($childNode);
|
||||
}
|
||||
|
||||
// Perform required string-level tweaks
|
||||
$html = str_replace(' ', ' ', $html);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,15 @@ use Barryvdh\Snappy\Facades\SnappyPdf;
|
||||
|
||||
class PdfGenerator
|
||||
{
|
||||
const ENGINE_DOMPDF = 'dompdf';
|
||||
const ENGINE_WKHTML = 'wkhtml';
|
||||
|
||||
/**
|
||||
* Generate PDF content from the given HTML content.
|
||||
*/
|
||||
public function fromHtml(string $html): string
|
||||
{
|
||||
$useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true;
|
||||
|
||||
if ($useWKHTML) {
|
||||
if ($this->getActiveEngine() === self::ENGINE_WKHTML) {
|
||||
$pdf = SnappyPDF::loadHTML($html);
|
||||
$pdf->setOption('print-media-type', true);
|
||||
} else {
|
||||
@@ -23,4 +24,15 @@ class PdfGenerator
|
||||
|
||||
return $pdf->output();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently active PDF engine.
|
||||
* Returns the value of an `ENGINE_` const on this class.
|
||||
*/
|
||||
public function getActiveEngine(): string
|
||||
{
|
||||
$useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true;
|
||||
|
||||
return $useWKHTML ? self::ENGINE_WKHTML : self::ENGINE_DOMPDF;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,12 @@ class TrashCan
|
||||
{
|
||||
/**
|
||||
* Send a shelf to the recycle bin.
|
||||
*
|
||||
* @throws NotifyException
|
||||
*/
|
||||
public function softDestroyShelf(Bookshelf $shelf)
|
||||
{
|
||||
$this->ensureDeletable($shelf);
|
||||
Deletion::createForEntity($shelf);
|
||||
$shelf->delete();
|
||||
}
|
||||
@@ -36,6 +39,7 @@ class TrashCan
|
||||
*/
|
||||
public function softDestroyBook(Book $book)
|
||||
{
|
||||
$this->ensureDeletable($book);
|
||||
Deletion::createForEntity($book);
|
||||
|
||||
foreach ($book->pages as $page) {
|
||||
@@ -57,6 +61,7 @@ class TrashCan
|
||||
public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
|
||||
{
|
||||
if ($recordDelete) {
|
||||
$this->ensureDeletable($chapter);
|
||||
Deletion::createForEntity($chapter);
|
||||
}
|
||||
|
||||
@@ -77,19 +82,47 @@ class TrashCan
|
||||
public function softDestroyPage(Page $page, bool $recordDelete = true)
|
||||
{
|
||||
if ($recordDelete) {
|
||||
$this->ensureDeletable($page);
|
||||
Deletion::createForEntity($page);
|
||||
}
|
||||
|
||||
// Check if set as custom homepage & remove setting if not used or throw error if active
|
||||
$customHome = setting('app-homepage', '0:');
|
||||
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
|
||||
if (setting('app-homepage-type') === 'page') {
|
||||
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the given entity is deletable.
|
||||
* Is not for permissions, but logical conditions within the application.
|
||||
* Will throw if not deletable.
|
||||
*
|
||||
* @throws NotifyException
|
||||
*/
|
||||
protected function ensureDeletable(Entity $entity): void
|
||||
{
|
||||
$customHomeId = intval(explode(':', setting('app-homepage', '0:'))[0]);
|
||||
$customHomeActive = setting('app-homepage-type') === 'page';
|
||||
$removeCustomHome = false;
|
||||
|
||||
// Check custom homepage usage for pages
|
||||
if ($entity instanceof Page && $entity->id === $customHomeId) {
|
||||
if ($customHomeActive) {
|
||||
throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
|
||||
}
|
||||
setting()->remove('app-homepage');
|
||||
$removeCustomHome = true;
|
||||
}
|
||||
|
||||
$page->delete();
|
||||
// Check custom homepage usage within chapters or books
|
||||
if ($entity instanceof Chapter || $entity instanceof Book) {
|
||||
if ($entity->pages()->where('id', '=', $customHomeId)->exists()) {
|
||||
if ($customHomeActive) {
|
||||
throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
|
||||
}
|
||||
$removeCustomHome = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($removeCustomHome) {
|
||||
setting()->remove('app-homepage');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -101,6 +101,10 @@ class Handler extends ExceptionHandler
|
||||
$code = $e->status;
|
||||
}
|
||||
|
||||
if (method_exists($e, 'getStatus')) {
|
||||
$code = $e->getStatus();
|
||||
}
|
||||
|
||||
$responseData['error']['code'] = $code;
|
||||
|
||||
return new JsonResponse($responseData, $code, $headers);
|
||||
|
||||
@@ -3,23 +3,25 @@
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class JsonDebugException extends Exception
|
||||
{
|
||||
protected $data;
|
||||
protected array $data;
|
||||
|
||||
/**
|
||||
* JsonDebugException constructor.
|
||||
*/
|
||||
public function __construct($data)
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Covert this exception into a response.
|
||||
*/
|
||||
public function render()
|
||||
public function render(): JsonResponse
|
||||
{
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
@@ -9,17 +9,24 @@ class NotifyException extends Exception implements Responsable
|
||||
{
|
||||
public $message;
|
||||
public $redirectLocation;
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* NotifyException constructor.
|
||||
*/
|
||||
public function __construct(string $message, string $redirectLocation = '/')
|
||||
public function __construct(string $message, string $redirectLocation = '/', int $status = 500)
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->redirectLocation = $redirectLocation;
|
||||
$this->status = $status;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the desired status code for this exception.
|
||||
*/
|
||||
public function getStatus(): int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the response for this type of exception.
|
||||
*
|
||||
@@ -29,6 +36,11 @@ class NotifyException extends Exception implements Responsable
|
||||
{
|
||||
$message = $this->getMessage();
|
||||
|
||||
// Front-end JSON handling. API-side handling managed via handler.
|
||||
if ($request->wantsJson()) {
|
||||
return response()->json(['error' => $message], 403);
|
||||
}
|
||||
|
||||
if (!empty($message)) {
|
||||
session()->flash('error', $message);
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
class OpenIdConnectException extends NotifyException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class SortOperationException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -15,10 +15,14 @@ abstract class ApiController extends Controller
|
||||
* Provide a paginated listing JSON response in a standard format
|
||||
* taking into account any pagination parameters passed by the user.
|
||||
*/
|
||||
protected function apiListingResponse(Builder $query, array $fields): JsonResponse
|
||||
protected function apiListingResponse(Builder $query, array $fields, array $modifiers = []): JsonResponse
|
||||
{
|
||||
$listing = new ListingResponseBuilder($query, request(), $fields);
|
||||
|
||||
foreach ($modifiers as $modifier) {
|
||||
$listing->modifyResults($modifier);
|
||||
}
|
||||
|
||||
return $listing->toResponse();
|
||||
}
|
||||
|
||||
@@ -26,7 +30,7 @@ abstract class ApiController extends Controller
|
||||
* Get the validation rules for this controller.
|
||||
* Defaults to a $rules property but can be a rules() method.
|
||||
*/
|
||||
public function getValdationRules(): array
|
||||
public function getValidationRules(): array
|
||||
{
|
||||
if (method_exists($this, 'rules')) {
|
||||
return $this->rules();
|
||||
|
||||
168
app/Http/Controllers/Api/UserApiController.php
Normal file
168
app/Http/Controllers/Api/UserApiController.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Api;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\UserUpdateException;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
|
||||
class UserApiController extends ApiController
|
||||
{
|
||||
protected $userRepo;
|
||||
|
||||
protected $fieldsToExpose = [
|
||||
'email', 'created_at', 'updated_at', 'last_activity_at', 'external_auth_id',
|
||||
];
|
||||
|
||||
public function __construct(UserRepo $userRepo)
|
||||
{
|
||||
$this->userRepo = $userRepo;
|
||||
|
||||
// Checks for all endpoints in this controller
|
||||
$this->middleware(function ($request, $next) {
|
||||
$this->checkPermission('users-manage');
|
||||
$this->preventAccessInDemoMode();
|
||||
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
|
||||
protected function rules(int $userId = null): array
|
||||
{
|
||||
return [
|
||||
'create' => [
|
||||
'name' => ['required', 'min:2'],
|
||||
'email' => [
|
||||
'required', 'min:2', 'email', new Unique('users', 'email'),
|
||||
],
|
||||
'external_auth_id' => ['string'],
|
||||
'language' => ['string'],
|
||||
'password' => [Password::default()],
|
||||
'roles' => ['array'],
|
||||
'roles.*' => ['integer'],
|
||||
'send_invite' => ['boolean'],
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['min:2'],
|
||||
'email' => [
|
||||
'min:2',
|
||||
'email',
|
||||
(new Unique('users', 'email'))->ignore($userId ?? null),
|
||||
],
|
||||
'external_auth_id' => ['string'],
|
||||
'language' => ['string'],
|
||||
'password' => [Password::default()],
|
||||
'roles' => ['array'],
|
||||
'roles.*' => ['integer'],
|
||||
],
|
||||
'delete' => [
|
||||
'migrate_ownership_id' => ['integer', 'exists:users,id'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a listing of users in the system.
|
||||
* Requires permission to manage users.
|
||||
*/
|
||||
public function list()
|
||||
{
|
||||
$users = User::query()->select(['*'])
|
||||
->scopes('withLastActivityAt')
|
||||
->with(['avatar']);
|
||||
|
||||
return $this->apiListingResponse($users, [
|
||||
'id', 'name', 'slug', 'email', 'external_auth_id',
|
||||
'created_at', 'updated_at', 'last_activity_at',
|
||||
], [Closure::fromCallable([$this, 'listFormatter'])]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user in the system.
|
||||
* Requires permission to manage users.
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$data = $this->validate($request, $this->rules()['create']);
|
||||
$sendInvite = ($data['send_invite'] ?? false) === true;
|
||||
|
||||
$user = null;
|
||||
DB::transaction(function () use ($data, $sendInvite, &$user) {
|
||||
$user = $this->userRepo->create($data, $sendInvite);
|
||||
});
|
||||
|
||||
$this->singleFormatter($user);
|
||||
|
||||
return response()->json($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* View the details of a single user.
|
||||
* Requires permission to manage users.
|
||||
*/
|
||||
public function read(string $id)
|
||||
{
|
||||
$user = $this->userRepo->getById($id);
|
||||
$this->singleFormatter($user);
|
||||
|
||||
return response()->json($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing user in the system.
|
||||
* Requires permission to manage users.
|
||||
*
|
||||
* @throws UserUpdateException
|
||||
*/
|
||||
public function update(Request $request, string $id)
|
||||
{
|
||||
$data = $this->validate($request, $this->rules($id)['update']);
|
||||
$user = $this->userRepo->getById($id);
|
||||
$this->userRepo->update($user, $data, userCan('users-manage'));
|
||||
$this->singleFormatter($user);
|
||||
|
||||
return response()->json($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user from the system.
|
||||
* Can optionally accept a user id via `migrate_ownership_id` to indicate
|
||||
* who should be the new owner of their related content.
|
||||
* Requires permission to manage users.
|
||||
*/
|
||||
public function delete(Request $request, string $id)
|
||||
{
|
||||
$user = $this->userRepo->getById($id);
|
||||
$newOwnerId = $request->get('migrate_ownership_id', null);
|
||||
|
||||
$this->userRepo->destroy($user, $newOwnerId);
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given user model for single-result display.
|
||||
*/
|
||||
protected function singleFormatter(User $user)
|
||||
{
|
||||
$this->listFormatter($user);
|
||||
$user->load('roles:id,display_name');
|
||||
$user->makeVisible(['roles']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given user model for a listing multi-result display.
|
||||
*/
|
||||
protected function listFormatter(User $user)
|
||||
{
|
||||
$user->makeVisible($this->fieldsToExpose);
|
||||
$user->setAttribute('profile_url', $user->getProfileUrl());
|
||||
$user->setAttribute('edit_url', $user->getEditUrl());
|
||||
$user->setAttribute('avatar_url', $user->getAvatar());
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ class MfaBackupCodesController extends Controller
|
||||
|
||||
$downloadUrl = 'data:application/octet-stream;base64,' . base64_encode(implode("\n\n", $codes));
|
||||
|
||||
$this->setPageTitle(trans('auth.mfa_gen_backup_codes_title'));
|
||||
|
||||
return view('mfa.backup-codes-generate', [
|
||||
'codes' => $codes,
|
||||
'downloadUrl' => $downloadUrl,
|
||||
|
||||
@@ -21,6 +21,8 @@ class MfaController extends Controller
|
||||
->get(['id', 'method'])
|
||||
->groupBy('method');
|
||||
|
||||
$this->setPageTitle(trans('auth.mfa_setup'));
|
||||
|
||||
return view('mfa.setup', [
|
||||
'userMethods' => $userMethods,
|
||||
]);
|
||||
|
||||
@@ -34,6 +34,8 @@ class MfaTotpController extends Controller
|
||||
$qrCodeUrl = $totp->generateUrl($totpSecret, $this->currentOrLastAttemptedUser());
|
||||
$svg = $totp->generateQrCodeSvg($qrCodeUrl);
|
||||
|
||||
$this->setPageTitle(trans('auth.mfa_gen_totp_title'));
|
||||
|
||||
return view('mfa.totp-generate', [
|
||||
'url' => $qrCodeUrl,
|
||||
'svg' => $svg,
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Auth\Access\Oidc\OidcException;
|
||||
use BookStack\Auth\Access\Oidc\OidcService;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class OidcController extends Controller
|
||||
{
|
||||
protected $oidcService;
|
||||
protected OidcService $oidcService;
|
||||
|
||||
/**
|
||||
* OpenIdController constructor.
|
||||
@@ -24,7 +25,14 @@ class OidcController extends Controller
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
$loginDetails = $this->oidcService->login();
|
||||
try {
|
||||
$loginDetails = $this->oidcService->login();
|
||||
} catch (OidcException $exception) {
|
||||
$this->showErrorNotification($exception->getMessage());
|
||||
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
session()->flash('oidc_state', $loginDetails['state']);
|
||||
|
||||
return redirect($loginDetails['url']);
|
||||
@@ -45,7 +53,13 @@ class OidcController extends Controller
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
$this->oidcService->processAuthorizeResponse($request->query('code'));
|
||||
try {
|
||||
$this->oidcService->processAuthorizeResponse($request->query('code'));
|
||||
} catch (OidcException $oidcException) {
|
||||
$this->showErrorNotification($oidcException->getMessage());
|
||||
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
return redirect()->intended();
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Repos\BookRepo;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Exceptions\SortOperationException;
|
||||
use BookStack\Entities\Tools\BookSortMap;
|
||||
use BookStack\Facades\Activity;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -59,20 +58,14 @@ class BookSortController extends Controller
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
$sortMap = collect(json_decode($request->get('sort-tree')));
|
||||
$sortMap = BookSortMap::fromJson($request->get('sort-tree'));
|
||||
$bookContents = new BookContents($book);
|
||||
$booksInvolved = collect();
|
||||
|
||||
try {
|
||||
$booksInvolved = $bookContents->sortUsingMap($sortMap);
|
||||
} catch (SortOperationException $exception) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
$booksInvolved = $bookContents->sortUsingMap($sortMap);
|
||||
|
||||
// Rebuild permissions and add activity for involved books.
|
||||
$booksInvolved->each(function (Book $book) {
|
||||
Activity::add(ActivityType::BOOK_SORT, $book);
|
||||
});
|
||||
foreach ($booksInvolved as $bookInvolved) {
|
||||
Activity::add(ActivityType::BOOK_SORT, $bookInvolved);
|
||||
}
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use BookStack\Entities\Tools\NextPreviousContentLocator;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\MoveOperationException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
@@ -180,6 +181,8 @@ class ChapterController extends Controller
|
||||
|
||||
try {
|
||||
$newBook = $this->chapterRepo->move($chapter, $entitySelection);
|
||||
} catch (PermissionsException $exception) {
|
||||
$this->showPermissionError();
|
||||
} catch (MoveOperationException $exception) {
|
||||
$this->showErrorNotification(trans('errors.selected_book_not_found'));
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Model;
|
||||
use BookStack\Util\WebSafeMimeSniffer;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
@@ -48,17 +48,14 @@ abstract class Controller extends BaseController
|
||||
/**
|
||||
* On a permission error redirect to home and display.
|
||||
* the error as a notification.
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
protected function showPermissionError()
|
||||
{
|
||||
if (request()->wantsJson()) {
|
||||
$response = response()->json(['error' => trans('errors.permissionJson')], 403);
|
||||
} else {
|
||||
$response = redirect('/');
|
||||
$this->showErrorNotification(trans('errors.permission'));
|
||||
}
|
||||
$message = request()->wantsJson() ? trans('errors.permissionJson') : trans('errors.permission');
|
||||
|
||||
throw new HttpResponseException($response);
|
||||
throw new NotifyException($message, '/', 403);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,8 @@ class FavouriteController extends Controller
|
||||
|
||||
$hasMoreLink = ($favourites->count() > $viewCount) ? url('/favourites?page=' . ($page + 1)) : null;
|
||||
|
||||
$this->setPageTitle(trans('entities.my_favourites'));
|
||||
|
||||
return view('common.detailed-listing-with-more', [
|
||||
'title' => trans('entities.my_favourites'),
|
||||
'entities' => $favourites->slice(0, $viewCount),
|
||||
|
||||
@@ -107,14 +107,6 @@ class HomeController extends Controller
|
||||
return view('home.default', $commonData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom head HTML, Used in ajax calls to show in editor.
|
||||
*/
|
||||
public function customHeadContent()
|
||||
{
|
||||
return view('common.custom-head');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the view for /robots.txt.
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,7 @@ use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
@@ -364,13 +365,22 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRecentlyUpdated()
|
||||
{
|
||||
$pages = Page::visible()->orderBy('updated_at', 'desc')
|
||||
$visibleBelongsScope = function (BelongsTo $query) {
|
||||
$query->scopes('visible');
|
||||
};
|
||||
|
||||
$pages = Page::visible()->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])
|
||||
->orderBy('updated_at', 'desc')
|
||||
->paginate(20)
|
||||
->setPath(url('/pages/recently-updated'));
|
||||
|
||||
$this->setPageTitle(trans('entities.recently_updated_pages'));
|
||||
|
||||
return view('common.detailed-listing-paginated', [
|
||||
'title' => trans('entities.recently_updated_pages'),
|
||||
'entities' => $pages,
|
||||
'title' => trans('entities.recently_updated_pages'),
|
||||
'entities' => $pages,
|
||||
'showUpdatedBy' => true,
|
||||
'showPath' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -410,11 +420,9 @@ class PageController extends Controller
|
||||
|
||||
try {
|
||||
$parent = $this->pageRepo->move($page, $entitySelection);
|
||||
} catch (PermissionsException $exception) {
|
||||
$this->showPermissionError();
|
||||
} catch (Exception $exception) {
|
||||
if ($exception instanceof PermissionsException) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
$this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
|
||||
|
||||
return redirect()->back();
|
||||
|
||||
@@ -124,11 +124,8 @@ class PageRevisionController extends Controller
|
||||
throw new NotFoundException("Revision #{$revId} not found");
|
||||
}
|
||||
|
||||
// Get the current revision for the page
|
||||
$currentRevision = $page->getCurrentRevision();
|
||||
|
||||
// Check if its the latest revision, cannot delete latest revision.
|
||||
if (intval($currentRevision->id) === intval($revId)) {
|
||||
// Check if it's the latest revision, cannot delete the latest revision.
|
||||
if (intval($page->currentRevision->id ?? null) === intval($revId)) {
|
||||
$this->showErrorNotification(trans('entities.revision_cannot_delete_latest'));
|
||||
|
||||
return redirect($page->getUrl('/revisions'));
|
||||
|
||||
@@ -29,6 +29,8 @@ class RoleController extends Controller
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$roles = $this->permissionsRepo->getAllRoles();
|
||||
|
||||
$this->setPageTitle(trans('settings.roles'));
|
||||
|
||||
return view('settings.roles.index', ['roles' => $roles]);
|
||||
}
|
||||
|
||||
@@ -49,6 +51,8 @@ class RoleController extends Controller
|
||||
$role->display_name .= ' (' . trans('common.copy') . ')';
|
||||
}
|
||||
|
||||
$this->setPageTitle(trans('settings.role_create'));
|
||||
|
||||
return view('settings.roles.create', ['role' => $role]);
|
||||
}
|
||||
|
||||
@@ -82,6 +86,8 @@ class RoleController extends Controller
|
||||
throw new PermissionsException(trans('errors.role_cannot_be_edited'));
|
||||
}
|
||||
|
||||
$this->setPageTitle(trans('settings.role_edit'));
|
||||
|
||||
return view('settings.roles.edit', ['role' => $role]);
|
||||
}
|
||||
|
||||
@@ -116,6 +122,8 @@ class RoleController extends Controller
|
||||
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
|
||||
$roles->prepend($blankRole);
|
||||
|
||||
$this->setPageTitle(trans('settings.role_delete'));
|
||||
|
||||
return view('settings.roles.delete', ['role' => $role, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,10 @@ use Illuminate\Http\Request;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
protected $imageRepo;
|
||||
protected ImageRepo $imageRepo;
|
||||
|
||||
protected array $settingCategories = ['features', 'customization', 'registration'];
|
||||
|
||||
/**
|
||||
* SettingController constructor.
|
||||
*/
|
||||
public function __construct(ImageRepo $imageRepo)
|
||||
{
|
||||
$this->imageRepo = $imageRepo;
|
||||
@@ -22,15 +21,17 @@ class SettingController extends Controller
|
||||
/**
|
||||
* Display a listing of the settings.
|
||||
*/
|
||||
public function index()
|
||||
public function index(string $category)
|
||||
{
|
||||
$this->ensureCategoryExists($category);
|
||||
$this->checkPermission('settings-manage');
|
||||
$this->setPageTitle(trans('settings.settings'));
|
||||
|
||||
// Get application version
|
||||
$version = trim(file_get_contents(base_path('version')));
|
||||
|
||||
return view('settings.index', [
|
||||
return view('settings.' . $category, [
|
||||
'category' => $category,
|
||||
'version' => $version,
|
||||
'guestUser' => User::getDefault(),
|
||||
]);
|
||||
@@ -39,8 +40,9 @@ class SettingController extends Controller
|
||||
/**
|
||||
* Update the specified settings in storage.
|
||||
*/
|
||||
public function update(Request $request)
|
||||
public function update(Request $request, string $category)
|
||||
{
|
||||
$this->ensureCategoryExists($category);
|
||||
$this->preventAccessInDemoMode();
|
||||
$this->checkPermission('settings-manage');
|
||||
$this->validate($request, [
|
||||
@@ -57,7 +59,7 @@ class SettingController extends Controller
|
||||
}
|
||||
|
||||
// Update logo image if set
|
||||
if ($request->hasFile('app_logo')) {
|
||||
if ($category === 'customization' && $request->hasFile('app_logo')) {
|
||||
$logoFile = $request->file('app_logo');
|
||||
$this->imageRepo->destroyByType('system');
|
||||
$image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86);
|
||||
@@ -65,16 +67,21 @@ class SettingController extends Controller
|
||||
}
|
||||
|
||||
// Clear logo image if requested
|
||||
if ($request->get('app_logo_reset', null)) {
|
||||
if ($category === 'customization' && $request->get('app_logo_reset', null)) {
|
||||
$this->imageRepo->destroyByType('system');
|
||||
setting()->remove('app-logo');
|
||||
}
|
||||
|
||||
$section = $request->get('section', '');
|
||||
$this->logActivity(ActivityType::SETTINGS_UPDATE, $section);
|
||||
$this->logActivity(ActivityType::SETTINGS_UPDATE, $category);
|
||||
$this->showSuccessNotification(trans('settings.settings_save_success'));
|
||||
$redirectLocation = '/settings#' . $section;
|
||||
|
||||
return redirect(rtrim($redirectLocation, '#'));
|
||||
return redirect("/settings/${category}");
|
||||
}
|
||||
|
||||
protected function ensureCategoryExists(string $category): void
|
||||
{
|
||||
if (!in_array($category, $this->settingCategories)) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ class TagController extends Controller
|
||||
'name' => $nameFilter,
|
||||
]));
|
||||
|
||||
$this->setPageTitle(trans('entities.tags'));
|
||||
|
||||
return view('tags.index', [
|
||||
'tags' => $tags,
|
||||
'search' => $search,
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Auth\Access\UserInviteService;
|
||||
use BookStack\Auth\Queries\AllUsersPaginatedAndSorted;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
@@ -12,25 +12,21 @@ use BookStack\Exceptions\UserUpdateException;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
protected $user;
|
||||
protected $userRepo;
|
||||
protected $inviteService;
|
||||
protected $imageRepo;
|
||||
|
||||
/**
|
||||
* UserController constructor.
|
||||
*/
|
||||
public function __construct(User $user, UserRepo $userRepo, UserInviteService $inviteService, ImageRepo $imageRepo)
|
||||
public function __construct(UserRepo $userRepo, ImageRepo $imageRepo)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->inviteService = $inviteService;
|
||||
$this->imageRepo = $imageRepo;
|
||||
}
|
||||
|
||||
@@ -45,12 +41,16 @@ class UserController extends Controller
|
||||
'search' => $request->get('search', ''),
|
||||
'sort' => $request->get('sort', 'name'),
|
||||
];
|
||||
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
|
||||
|
||||
$users = (new AllUsersPaginatedAndSorted())->run(20, $listDetails);
|
||||
|
||||
$this->setPageTitle(trans('settings.users'));
|
||||
$users->appends($listDetails);
|
||||
|
||||
return view('users.index', ['users' => $users, 'listDetails' => $listDetails]);
|
||||
return view('users.index', [
|
||||
'users' => $users,
|
||||
'listDetails' => $listDetails,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,59 +60,42 @@ class UserController extends Controller
|
||||
{
|
||||
$this->checkPermission('users-manage');
|
||||
$authMethod = config('auth.method');
|
||||
$roles = $this->userRepo->getAllRoles();
|
||||
$roles = Role::query()->orderBy('display_name', 'asc')->get();
|
||||
$this->setPageTitle(trans('settings.users_add_new'));
|
||||
|
||||
return view('users.create', ['authMethod' => $authMethod, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created user in storage.
|
||||
* Store a new user in storage.
|
||||
*
|
||||
* @throws UserUpdateException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('users-manage');
|
||||
$validationRules = [
|
||||
'name' => ['required'],
|
||||
'email' => ['required', 'email', 'unique:users,email'],
|
||||
];
|
||||
|
||||
$authMethod = config('auth.method');
|
||||
$sendInvite = ($request->get('send_invite', 'false') === 'true');
|
||||
$externalAuth = $authMethod === 'ldap' || $authMethod === 'saml2' || $authMethod === 'oidc';
|
||||
$passwordRequired = ($authMethod === 'standard' && !$sendInvite);
|
||||
|
||||
if ($authMethod === 'standard' && !$sendInvite) {
|
||||
$validationRules['password'] = ['required', Password::default()];
|
||||
$validationRules['password-confirm'] = ['required', 'same:password'];
|
||||
} elseif ($authMethod === 'ldap' || $authMethod === 'saml2' || $authMethod === 'openid') {
|
||||
$validationRules['external_auth_id'] = ['required'];
|
||||
}
|
||||
$this->validate($request, $validationRules);
|
||||
$validationRules = [
|
||||
'name' => ['required'],
|
||||
'email' => ['required', 'email', 'unique:users,email'],
|
||||
'language' => ['string'],
|
||||
'roles' => ['array'],
|
||||
'roles.*' => ['integer'],
|
||||
'password' => $passwordRequired ? ['required', Password::default()] : null,
|
||||
'password-confirm' => $passwordRequired ? ['required', 'same:password'] : null,
|
||||
'external_auth_id' => $externalAuth ? ['required'] : null,
|
||||
];
|
||||
|
||||
$user = $this->user->fill($request->all());
|
||||
$validated = $this->validate($request, array_filter($validationRules));
|
||||
|
||||
if ($authMethod === 'standard') {
|
||||
$user->password = bcrypt($request->get('password', Str::random(32)));
|
||||
} elseif ($authMethod === 'ldap' || $authMethod === 'saml2' || $authMethod === 'openid') {
|
||||
$user->external_auth_id = $request->get('external_auth_id');
|
||||
}
|
||||
|
||||
$user->refreshSlug();
|
||||
$user->save();
|
||||
|
||||
if ($sendInvite) {
|
||||
$this->inviteService->sendInvitation($user);
|
||||
}
|
||||
|
||||
if ($request->filled('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
$this->userRepo->setUserRoles($user, $roles);
|
||||
}
|
||||
|
||||
$this->userRepo->downloadAndAssignUserAvatar($user);
|
||||
|
||||
$this->logActivity(ActivityType::USER_CREATE, $user);
|
||||
DB::transaction(function () use ($validated, $sendInvite) {
|
||||
$this->userRepo->create($validated, $sendInvite);
|
||||
});
|
||||
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
@@ -125,14 +108,14 @@ class UserController extends Controller
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->user->newQuery()->with(['apiTokens', 'mfaValues'])->findOrFail($id);
|
||||
$user = User::query()->with(['apiTokens', 'mfaValues'])->findOrFail($id);
|
||||
|
||||
$authMethod = ($user->system_name) ? 'system' : config('auth.method');
|
||||
|
||||
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
||||
$mfaMethods = $user->mfaValues->groupBy('method');
|
||||
$this->setPageTitle(trans('settings.user_profile'));
|
||||
$roles = $this->userRepo->getAllRoles();
|
||||
$roles = Role::query()->orderBy('display_name', 'asc')->get();
|
||||
|
||||
return view('users.edit', [
|
||||
'user' => $user,
|
||||
@@ -155,51 +138,20 @@ class UserController extends Controller
|
||||
$this->preventAccessInDemoMode();
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||
|
||||
$this->validate($request, [
|
||||
$validated = $this->validate($request, [
|
||||
'name' => ['min:2'],
|
||||
'email' => ['min:2', 'email', 'unique:users,email,' . $id],
|
||||
'password' => ['required_with:password_confirm', Password::default()],
|
||||
'password-confirm' => ['same:password', 'required_with:password'],
|
||||
'setting' => ['array'],
|
||||
'language' => ['string'],
|
||||
'roles' => ['array'],
|
||||
'roles.*' => ['integer'],
|
||||
'external_auth_id' => ['string'],
|
||||
'profile_image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
]);
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
$user->fill($request->except(['email']));
|
||||
|
||||
// Email updates
|
||||
if (userCan('users-manage') && $request->filled('email')) {
|
||||
$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');
|
||||
$this->userRepo->setUserRoles($user, $roles);
|
||||
}
|
||||
|
||||
// Password updates
|
||||
if ($request->filled('password')) {
|
||||
$password = $request->get('password');
|
||||
$user->password = bcrypt($password);
|
||||
}
|
||||
|
||||
// External auth id updates
|
||||
if (user()->can('users-manage') && $request->filled('external_auth_id')) {
|
||||
$user->external_auth_id = $request->get('external_auth_id');
|
||||
}
|
||||
|
||||
// Save an user-specific settings
|
||||
if ($request->filled('setting')) {
|
||||
foreach ($request->get('setting') as $key => $value) {
|
||||
setting()->putUser($user, $key, $value);
|
||||
}
|
||||
}
|
||||
$this->userRepo->update($user, $validated, userCan('users-manage'));
|
||||
|
||||
// Save profile image if in request
|
||||
if ($request->hasFile('profile_image')) {
|
||||
@@ -207,6 +159,7 @@ class UserController extends Controller
|
||||
$this->imageRepo->destroyImage($user->avatar);
|
||||
$image = $this->imageRepo->saveNew($imageUpload, 'user', $user->id);
|
||||
$user->image_id = $image->id;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
// Delete the profile image if reset option is in request
|
||||
@@ -214,11 +167,7 @@ class UserController extends Controller
|
||||
$this->imageRepo->destroyImage($user->avatar);
|
||||
}
|
||||
|
||||
$user->save();
|
||||
$this->showSuccessNotification(trans('settings.users_edit_success'));
|
||||
$this->logActivity(ActivityType::USER_UPDATE, $user);
|
||||
|
||||
$redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
|
||||
$redirectUrl = userCan('users-manage') ? '/settings/users' : "/settings/users/{$user->id}";
|
||||
|
||||
return redirect($redirectUrl);
|
||||
}
|
||||
@@ -249,21 +198,7 @@ class UserController extends Controller
|
||||
$user = $this->userRepo->getById($id);
|
||||
$newOwnerId = $request->get('new_owner_id', null);
|
||||
|
||||
if ($this->userRepo->isOnlyAdmin($user)) {
|
||||
$this->showErrorNotification(trans('errors.users_cannot_delete_only_admin'));
|
||||
|
||||
return redirect($user->getEditUrl());
|
||||
}
|
||||
|
||||
if ($user->system_name === 'public') {
|
||||
$this->showErrorNotification(trans('errors.users_cannot_delete_guest'));
|
||||
|
||||
return redirect($user->getEditUrl());
|
||||
}
|
||||
|
||||
$this->userRepo->destroy($user, $newOwnerId);
|
||||
$this->showSuccessNotification(trans('settings.users_delete_success'));
|
||||
$this->logActivity(ActivityType::USER_DELETE, $user);
|
||||
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
@@ -348,7 +283,7 @@ class UserController extends Controller
|
||||
|
||||
$newState = $request->get('expand', 'false');
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
$user = $this->userRepo->getById($id);
|
||||
setting()->putUser($user, 'section_expansion#' . $key, $newState);
|
||||
|
||||
return response('', 204);
|
||||
@@ -371,7 +306,7 @@ class UserController extends Controller
|
||||
$order = 'asc';
|
||||
}
|
||||
|
||||
$user = $this->user->findOrFail($userId);
|
||||
$user = $this->userRepo->getById($userId);
|
||||
$sortKey = $listName . '_sort';
|
||||
$orderKey = $listName . '_sort_order';
|
||||
setting()->putUser($user, $sortKey, $sort);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Actions\ActivityQueries;
|
||||
use BookStack\Auth\Queries\UserContentCounts;
|
||||
use BookStack\Auth\Queries\UserRecentlyCreatedContent;
|
||||
use BookStack\Auth\UserRepo;
|
||||
|
||||
class UserProfileController extends Controller
|
||||
@@ -15,8 +17,10 @@ class UserProfileController extends Controller
|
||||
$user = $repo->getBySlug($slug);
|
||||
|
||||
$userActivity = $activities->userActivity($user);
|
||||
$recentlyCreated = $repo->getRecentlyCreated($user, 5);
|
||||
$assetCounts = $repo->getAssetCounts($user);
|
||||
$recentlyCreated = (new UserRecentlyCreatedContent())->run($user, 5);
|
||||
$assetCounts = (new UserContentCounts())->run($user);
|
||||
|
||||
$this->setPageTitle($user->name);
|
||||
|
||||
return view('users.profile', [
|
||||
'user' => $user,
|
||||
|
||||
@@ -25,6 +25,8 @@ class WebhookController extends Controller
|
||||
->with('trackedEvents')
|
||||
->get();
|
||||
|
||||
$this->setPageTitle(trans('settings.webhooks'));
|
||||
|
||||
return view('settings.webhooks.index', ['webhooks' => $webhooks]);
|
||||
}
|
||||
|
||||
@@ -33,6 +35,8 @@ class WebhookController extends Controller
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$this->setPageTitle(trans('settings.webhooks_create'));
|
||||
|
||||
return view('settings.webhooks.create');
|
||||
}
|
||||
|
||||
@@ -46,6 +50,7 @@ class WebhookController extends Controller
|
||||
'endpoint' => ['required', 'url', 'max:500'],
|
||||
'events' => ['required', 'array'],
|
||||
'active' => ['required'],
|
||||
'timeout' => ['required', 'integer', 'min:1', 'max:600'],
|
||||
]);
|
||||
|
||||
$webhook = new Webhook($validated);
|
||||
@@ -68,6 +73,8 @@ class WebhookController extends Controller
|
||||
->with('trackedEvents')
|
||||
->findOrFail($id);
|
||||
|
||||
$this->setPageTitle(trans('settings.webhooks_edit'));
|
||||
|
||||
return view('settings.webhooks.edit', ['webhook' => $webhook]);
|
||||
}
|
||||
|
||||
@@ -81,6 +88,7 @@ class WebhookController extends Controller
|
||||
'endpoint' => ['required', 'url', 'max:500'],
|
||||
'events' => ['required', 'array'],
|
||||
'active' => ['required'],
|
||||
'timeout' => ['required', 'integer', 'min:1', 'max:600'],
|
||||
]);
|
||||
|
||||
/** @var Webhook $webhook */
|
||||
@@ -103,6 +111,8 @@ class WebhookController extends Controller
|
||||
/** @var Webhook $webhook */
|
||||
$webhook = Webhook::query()->findOrFail($id);
|
||||
|
||||
$this->setPageTitle(trans('settings.webhooks_delete'));
|
||||
|
||||
return view('settings.webhooks.delete', ['webhook' => $webhook]);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,7 @@ use Illuminate\Http\Request;
|
||||
|
||||
class ApplyCspRules
|
||||
{
|
||||
/**
|
||||
* @var CspService
|
||||
*/
|
||||
protected $cspService;
|
||||
protected CspService $cspService;
|
||||
|
||||
public function __construct(CspService $cspService)
|
||||
{
|
||||
@@ -35,10 +32,8 @@ class ApplyCspRules
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
$this->cspService->setFrameAncestors($response);
|
||||
$this->cspService->setScriptSrc($response);
|
||||
$this->cspService->setObjectSrc($response);
|
||||
$this->cspService->setBaseUri($response);
|
||||
$cspHeader = $this->cspService->getCspHeader();
|
||||
$response->headers->set('Content-Security-Policy', $cspHeader, false);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class Localization
|
||||
'es' => 'es_ES',
|
||||
'es_AR' => 'es_AR',
|
||||
'et' => 'et_EE',
|
||||
'eu' => 'eu_ES',
|
||||
'fr' => 'fr_FR',
|
||||
'he' => 'he_IL',
|
||||
'hr' => 'hr_HR',
|
||||
|
||||
@@ -2,35 +2,33 @@
|
||||
|
||||
namespace BookStack\Notifications;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class UserInvite extends MailNotification
|
||||
{
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*
|
||||
* @param string $token
|
||||
*/
|
||||
public function __construct($token)
|
||||
public function __construct(string $token)
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
public function toMail(User $notifiable): MailMessage
|
||||
{
|
||||
$appName = ['appName' => setting('app-name')];
|
||||
$language = setting()->getUser($notifiable, 'language');
|
||||
|
||||
return $this->newMailMessage()
|
||||
->subject(trans('auth.user_invite_email_subject', $appName))
|
||||
->greeting(trans('auth.user_invite_email_greeting', $appName))
|
||||
->line(trans('auth.user_invite_email_text'))
|
||||
->action(trans('auth.user_invite_email_action'), url('/register/invite/' . $this->token));
|
||||
->subject(trans('auth.user_invite_email_subject', $appName, $language))
|
||||
->greeting(trans('auth.user_invite_email_greeting', $appName, $language))
|
||||
->line(trans('auth.user_invite_email_text', [], $language))
|
||||
->action(trans('auth.user_invite_email_action', [], $language), url('/register/invite/' . $this->token));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
public function boot()
|
||||
{
|
||||
// Password Configuration
|
||||
// Changes here must be reflected in ApiDocsGenerate@getValidationAsString.
|
||||
Password::defaults(function () {
|
||||
return Password::min(8);
|
||||
});
|
||||
|
||||
@@ -79,4 +79,22 @@ class ThemeEvents
|
||||
* @returns \League\CommonMark\ConfigurableEnvironmentInterface|null
|
||||
*/
|
||||
const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure';
|
||||
|
||||
/**
|
||||
* Webhook call before event.
|
||||
* Runs before a webhook endpoint is called. Allows for customization
|
||||
* of the data format & content within the webhook POST request.
|
||||
* Provides the original event name as a string (see \BookStack\Actions\ActivityType)
|
||||
* along with the webhook instance along with the event detail which may be a
|
||||
* "Loggable" model type or a string.
|
||||
* If the listener returns a non-null value, that will be used as the POST data instead
|
||||
* of the system default.
|
||||
*
|
||||
* @param string $event
|
||||
* @param \BookStack\Actions\Webhook $webhook
|
||||
* @param string|\BookStack\Interfaces\Loggable $detail
|
||||
* @param \BookStack\Auth\User $initiator
|
||||
* @param int $initiatedTime
|
||||
*/
|
||||
const WEBHOOK_CALL_BEFORE = 'webhook_call_before';
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace BookStack\Uploads;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use ErrorException;
|
||||
use Exception;
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
|
||||
@@ -14,6 +15,7 @@ use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Exception\NotSupportedException;
|
||||
use Intervention\Image\Image as InterventionImage;
|
||||
use Intervention\Image\ImageManager;
|
||||
use League\Flysystem\Util;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
@@ -228,6 +230,21 @@ class ImageService
|
||||
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given image and image data is apng.
|
||||
*/
|
||||
protected function isApngData(Image $image, string &$imageData): bool
|
||||
{
|
||||
$isPng = strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'png';
|
||||
if (!$isPng) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$initialHeader = substr($imageData, 0, strpos($imageData, 'IDAT'));
|
||||
|
||||
return strpos($initialHeader, 'acTL') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the thumbnail for an image.
|
||||
* If $keepRatio is true only the width will be used.
|
||||
@@ -238,6 +255,7 @@ class ImageService
|
||||
*/
|
||||
public function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio = false): string
|
||||
{
|
||||
// Do not resize GIF images where we're not cropping
|
||||
if ($keepRatio && $this->isGif($image)) {
|
||||
return $this->getPublicUrl($image->path);
|
||||
}
|
||||
@@ -246,19 +264,35 @@ class ImageService
|
||||
$imagePath = $image->path;
|
||||
$thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
|
||||
|
||||
if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
$thumbCacheKey = 'images::' . $image->id . '::' . $thumbFilePath;
|
||||
|
||||
// Return path if in cache
|
||||
$cachedThumbPath = $this->cache->get($thumbCacheKey);
|
||||
if ($cachedThumbPath) {
|
||||
return $this->getPublicUrl($cachedThumbPath);
|
||||
}
|
||||
|
||||
// If thumbnail has already been generated, serve that and cache path
|
||||
$storage = $this->getStorageDisk($image->type);
|
||||
if ($storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) {
|
||||
$this->cache->put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
|
||||
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
|
||||
$thumbData = $this->resizeImage($storage->get($this->adjustPathForStorageDisk($imagePath, $image->type)), $width, $height, $keepRatio);
|
||||
$imageData = $storage->get($this->adjustPathForStorageDisk($imagePath, $image->type));
|
||||
|
||||
// Do not resize apng images where we're not cropping
|
||||
if ($keepRatio && $this->isApngData($image, $imageData)) {
|
||||
$this->cache->put($thumbCacheKey, $image->path, 60 * 60 * 72);
|
||||
|
||||
return $this->getPublicUrl($image->path);
|
||||
}
|
||||
|
||||
// If not in cache and thumbnail does not exist, generate thumb and cache path
|
||||
$thumbData = $this->resizeImage($imageData, $width, $height, $keepRatio);
|
||||
$this->saveImageDataInPublicSpace($storage, $this->adjustPathForStorageDisk($thumbFilePath, $image->type), $thumbData);
|
||||
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);
|
||||
$this->cache->put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
|
||||
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
@@ -276,6 +310,8 @@ class ImageService
|
||||
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
|
||||
}
|
||||
|
||||
$this->orientImageToOriginalExif($thumb, $imageData);
|
||||
|
||||
if ($keepRatio) {
|
||||
$thumb->resize($width, $height, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
@@ -296,6 +332,49 @@ class ImageService
|
||||
return $thumbData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orientate the given intervention image based upon the given original image data.
|
||||
* Intervention does have an `orientate` method but the exif data it needs is lost before it
|
||||
* can be used (At least when created using binary string data) so we need to do some
|
||||
* implementation on our side to use the original image data.
|
||||
* Bulk of logic taken from: https://github.com/Intervention/image/blob/b734a4988b2148e7d10364b0609978a88d277536/src/Intervention/Image/Commands/OrientateCommand.php
|
||||
* Copyright (c) Oliver Vogel, MIT License.
|
||||
*/
|
||||
protected function orientImageToOriginalExif(InterventionImage $image, string $originalData): void
|
||||
{
|
||||
if (!extension_loaded('exif')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stream = Utils::streamFor($originalData)->detach();
|
||||
$exif = @exif_read_data($stream);
|
||||
$orientation = $exif ? ($exif['Orientation'] ?? null) : null;
|
||||
|
||||
switch ($orientation) {
|
||||
case 2:
|
||||
$image->flip();
|
||||
break;
|
||||
case 3:
|
||||
$image->rotate(180);
|
||||
break;
|
||||
case 4:
|
||||
$image->rotate(180)->flip();
|
||||
break;
|
||||
case 5:
|
||||
$image->rotate(270)->flip();
|
||||
break;
|
||||
case 6:
|
||||
$image->rotate(270);
|
||||
break;
|
||||
case 7:
|
||||
$image->rotate(90)->flip();
|
||||
break;
|
||||
case 8:
|
||||
$image->rotate(90);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw data content from an image.
|
||||
*
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
namespace BookStack\Util;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CspService
|
||||
{
|
||||
/** @var string */
|
||||
protected $nonce;
|
||||
protected string $nonce;
|
||||
|
||||
public function __construct(string $nonce = '')
|
||||
{
|
||||
@@ -24,37 +22,34 @@ class CspService
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets CSP 'script-src' headers to restrict the forms of script that can
|
||||
* run on the page.
|
||||
* Get the CSP headers for the application.
|
||||
*/
|
||||
public function setScriptSrc(Response $response)
|
||||
public function getCspHeader(): string
|
||||
{
|
||||
if (config('app.allow_content_scripts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parts = [
|
||||
'http:',
|
||||
'https:',
|
||||
'\'nonce-' . $this->nonce . '\'',
|
||||
'\'strict-dynamic\'',
|
||||
$headers = [
|
||||
$this->getFrameAncestors(),
|
||||
$this->getFrameSrc(),
|
||||
$this->getScriptSrc(),
|
||||
$this->getObjectSrc(),
|
||||
$this->getBaseUri(),
|
||||
];
|
||||
|
||||
$value = 'script-src ' . implode(' ', $parts);
|
||||
$response->headers->set('Content-Security-Policy', $value, false);
|
||||
return implode('; ', array_filter($headers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets CSP "frame-ancestors" headers to restrict the hosts that BookStack can be
|
||||
* iframed within. Also adjusts the cookie samesite options so that cookies will
|
||||
* operate in the third-party context.
|
||||
* Get the CSP rules for the application for a HTML meta tag.
|
||||
*/
|
||||
public function setFrameAncestors(Response $response)
|
||||
public function getCspMetaTagValue(): string
|
||||
{
|
||||
$iframeHosts = $this->getAllowedIframeHosts();
|
||||
array_unshift($iframeHosts, "'self'");
|
||||
$cspValue = 'frame-ancestors ' . implode(' ', $iframeHosts);
|
||||
$response->headers->set('Content-Security-Policy', $cspValue, false);
|
||||
$headers = [
|
||||
$this->getFrameSrc(),
|
||||
$this->getScriptSrc(),
|
||||
$this->getObjectSrc(),
|
||||
$this->getBaseUri(),
|
||||
];
|
||||
|
||||
return implode('; ', array_filter($headers));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,25 +61,67 @@ class CspService
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets CSP 'object-src' headers to restrict the types of dynamic content
|
||||
* that can be embedded on the page.
|
||||
* Create CSP 'script-src' rule to restrict the forms of script that can run on the page.
|
||||
*/
|
||||
public function setObjectSrc(Response $response)
|
||||
protected function getScriptSrc(): string
|
||||
{
|
||||
if (config('app.allow_content_scripts')) {
|
||||
return;
|
||||
return '';
|
||||
}
|
||||
|
||||
$response->headers->set('Content-Security-Policy', 'object-src \'self\'', false);
|
||||
$parts = [
|
||||
'http:',
|
||||
'https:',
|
||||
'\'nonce-' . $this->nonce . '\'',
|
||||
'\'strict-dynamic\'',
|
||||
];
|
||||
|
||||
return 'script-src ' . implode(' ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets CSP 'base-uri' headers to restrict what base tags can be set on
|
||||
* Create CSP "frame-ancestors" rule to restrict the hosts that BookStack can be iframed within.
|
||||
*/
|
||||
protected function getFrameAncestors(): string
|
||||
{
|
||||
$iframeHosts = $this->getAllowedIframeHosts();
|
||||
array_unshift($iframeHosts, "'self'");
|
||||
|
||||
return 'frame-ancestors ' . implode(' ', $iframeHosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates CSP "frame-src" rule to restrict what hosts/sources can be loaded
|
||||
* within iframes to provide an allow-list-style approach to iframe content.
|
||||
*/
|
||||
protected function getFrameSrc(): string
|
||||
{
|
||||
$iframeHosts = $this->getAllowedIframeSources();
|
||||
array_unshift($iframeHosts, "'self'");
|
||||
|
||||
return 'frame-src ' . implode(' ', $iframeHosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates CSP 'object-src' rule to restrict the types of dynamic content
|
||||
* that can be embedded on the page.
|
||||
*/
|
||||
protected function getObjectSrc(): string
|
||||
{
|
||||
if (config('app.allow_content_scripts')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return "object-src 'self'";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates CSP 'base-uri' rule to restrict what base tags can be set on
|
||||
* the page to prevent manipulation of relative links.
|
||||
*/
|
||||
public function setBaseUri(Response $response)
|
||||
protected function getBaseUri(): string
|
||||
{
|
||||
$response->headers->set('Content-Security-Policy', 'base-uri \'self\'', false);
|
||||
return "base-uri 'self'";
|
||||
}
|
||||
|
||||
protected function getAllowedIframeHosts(): array
|
||||
@@ -93,4 +130,21 @@ class CspService
|
||||
|
||||
return array_filter(explode(' ', $hosts));
|
||||
}
|
||||
|
||||
protected function getAllowedIframeSources(): array
|
||||
{
|
||||
$sources = config('app.iframe_sources', '');
|
||||
$hosts = array_filter(explode(' ', $sources));
|
||||
|
||||
// Extract drawing service url to allow embedding if active
|
||||
$drawioConfigValue = config('services.drawio');
|
||||
if ($drawioConfigValue) {
|
||||
$drawioSource = is_string($drawioConfigValue) ? $drawioConfigValue : 'https://embed.diagrams.net/';
|
||||
$drawioSourceParsed = parse_url($drawioSource);
|
||||
$drawioHost = $drawioSourceParsed['scheme'] . '://' . $drawioSourceParsed['host'];
|
||||
$hosts[] = $drawioHost;
|
||||
}
|
||||
|
||||
return $hosts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ class WebSafeMimeSniffer
|
||||
'application/json',
|
||||
'application/octet-stream',
|
||||
'application/pdf',
|
||||
'image/apng',
|
||||
'image/bmp',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": "^7.3|^8.0",
|
||||
"php": "^7.4|^8.0",
|
||||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
@@ -17,8 +17,8 @@
|
||||
"ext-mbstring": "*",
|
||||
"ext-xml": "*",
|
||||
"bacon/bacon-qr-code": "^2.0",
|
||||
"barryvdh/laravel-dompdf": "^0.9.0",
|
||||
"barryvdh/laravel-snappy": "^0.4.8",
|
||||
"barryvdh/laravel-dompdf": "^1.0",
|
||||
"barryvdh/laravel-snappy": "^1.0",
|
||||
"doctrine/dbal": "^3.1",
|
||||
"filp/whoops": "^2.14",
|
||||
"guzzlehttp/guzzle": "^7.4",
|
||||
@@ -41,7 +41,7 @@
|
||||
"socialiteproviders/okta": "^4.1",
|
||||
"socialiteproviders/slack": "^4.1",
|
||||
"socialiteproviders/twitch": "^5.3",
|
||||
"ssddanbrown/htmldiff": "^1.0.1"
|
||||
"ssddanbrown/htmldiff": "^1.0.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.16",
|
||||
@@ -95,7 +95,7 @@
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"platform": {
|
||||
"php": "7.3.0"
|
||||
"php": "7.4.0"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
|
||||
1461
composer.lock
generated
1461
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ class WebhookFactory extends Factory
|
||||
'name' => 'My webhook for ' . $this->faker->country(),
|
||||
'endpoint' => $this->faker->url,
|
||||
'active' => true,
|
||||
'timeout' => 3,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddWebhooksTimeoutErrorColumns extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('webhooks', function (Blueprint $table) {
|
||||
$table->unsignedInteger('timeout')->default(3);
|
||||
$table->text('last_error')->default('');
|
||||
$table->timestamp('last_called_at')->nullable();
|
||||
$table->timestamp('last_errored_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('webhooks', function (Blueprint $table) {
|
||||
$table->dropColumn('timeout');
|
||||
$table->dropColumn('last_error');
|
||||
$table->dropColumn('last_called_at');
|
||||
$table->dropColumn('last_errored_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
7
dev/api/requests/users-create.json
Normal file
7
dev/api/requests/users-create.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Dan Brown",
|
||||
"email": "dannyb@example.com",
|
||||
"roles": [1],
|
||||
"language": "fr",
|
||||
"send_invite": true
|
||||
}
|
||||
3
dev/api/requests/users-delete.json
Normal file
3
dev/api/requests/users-delete.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"migrate_ownership_id": 5
|
||||
}
|
||||
7
dev/api/requests/users-update.json
Normal file
7
dev/api/requests/users-update.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Dan Spaggleforth",
|
||||
"email": "dspaggles@example.com",
|
||||
"roles": [2],
|
||||
"language": "de",
|
||||
"password": "hunter2000"
|
||||
}
|
||||
19
dev/api/responses/users-create.json
Normal file
19
dev/api/responses/users-create.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Dan Brown",
|
||||
"email": "dannyb@example.com",
|
||||
"created_at": "2022-02-03T16:27:55.000000Z",
|
||||
"updated_at": "2022-02-03T16:27:55.000000Z",
|
||||
"external_auth_id": "abc123456",
|
||||
"slug": "dan-brown",
|
||||
"last_activity_at": "2022-02-03T16:27:55.000000Z",
|
||||
"profile_url": "https://docs.example.com/user/dan-brown",
|
||||
"edit_url": "https://docs.example.com/settings/users/1",
|
||||
"avatar_url": "https://docs.example.com/uploads/images/user/2021-10/thumbs-50-50/profile-2021.jpg",
|
||||
"roles": [
|
||||
{
|
||||
"id": 1,
|
||||
"display_name": "Admin"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
dev/api/responses/users-list.json
Normal file
33
dev/api/responses/users-list.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Dan Brown",
|
||||
"email": "dannyb@example.com",
|
||||
"created_at": "2022-02-03T16:27:55.000000Z",
|
||||
"updated_at": "2022-02-03T16:27:55.000000Z",
|
||||
"external_auth_id": "abc123456",
|
||||
"slug": "dan-brown",
|
||||
"user_id": 1,
|
||||
"last_activity_at": "2022-02-03T16:27:55.000000Z",
|
||||
"profile_url": "https://docs.example.com/user/dan-brown",
|
||||
"edit_url": "https://docs.example.com/settings/users/1",
|
||||
"avatar_url": "https://docs.example.com/uploads/images/user/2021-10/thumbs-50-50/profile-2021.jpg"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Benny",
|
||||
"email": "benny@example.com",
|
||||
"created_at": "2022-01-31T20:39:24.000000Z",
|
||||
"updated_at": "2021-11-18T17:10:58.000000Z",
|
||||
"external_auth_id": "",
|
||||
"slug": "benny",
|
||||
"user_id": 2,
|
||||
"last_activity_at": "2022-01-31T20:39:24.000000Z",
|
||||
"profile_url": "https://docs.example.com/user/benny",
|
||||
"edit_url": "https://docs.example.com/settings/users/2",
|
||||
"avatar_url": "https://docs.example.com/uploads/images/user/2021-11/thumbs-50-50/guest.jpg"
|
||||
}
|
||||
],
|
||||
"total": 28
|
||||
}
|
||||
19
dev/api/responses/users-read.json
Normal file
19
dev/api/responses/users-read.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Dan Brown",
|
||||
"email": "dannyb@example.com",
|
||||
"created_at": "2022-02-03T16:27:55.000000Z",
|
||||
"updated_at": "2022-02-03T16:27:55.000000Z",
|
||||
"external_auth_id": "abc123456",
|
||||
"slug": "dan-brown",
|
||||
"last_activity_at": "2022-02-03T16:27:55.000000Z",
|
||||
"profile_url": "https://docs.example.com/user/dan-brown",
|
||||
"edit_url": "https://docs.example.com/settings/users/1",
|
||||
"avatar_url": "https://docs.example.com/uploads/images/user/2021-10/thumbs-50-50/profile-2021.jpg",
|
||||
"roles": [
|
||||
{
|
||||
"id": 1,
|
||||
"display_name": "Admin"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
dev/api/responses/users-update.json
Normal file
19
dev/api/responses/users-update.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Dan Spaggleforth",
|
||||
"email": "dspaggles@example.com",
|
||||
"created_at": "2022-02-03T16:27:55.000000Z",
|
||||
"updated_at": "2022-02-03T16:27:55.000000Z",
|
||||
"external_auth_id": "abc123456",
|
||||
"slug": "dan-spaggleforth",
|
||||
"last_activity_at": "2022-02-03T16:27:55.000000Z",
|
||||
"profile_url": "https://docs.example.com/user/dan-spaggleforth",
|
||||
"edit_url": "https://docs.example.com/settings/users/1",
|
||||
"avatar_url": "https://docs.example.com/uploads/images/user/2021-10/thumbs-50-50/profile-2021.jpg",
|
||||
"roles": [
|
||||
{
|
||||
"id": 2,
|
||||
"display_name": "Editors"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM php:7.4-apache
|
||||
FROM php:8.1-apache
|
||||
|
||||
ENV APACHE_DOCUMENT_ROOT /app/public
|
||||
WORKDIR /app
|
||||
@@ -8,6 +8,8 @@ RUN apt-get update -y \
|
||||
&& apt-get install -y git zip unzip libpng-dev libldap2-dev libzip-dev wait-for-it \
|
||||
&& docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \
|
||||
&& docker-php-ext-install pdo_mysql gd ldap zip \
|
||||
&& pecl install xdebug \
|
||||
&& docker-php-ext-enable xdebug \
|
||||
&& a2enmod rewrite \
|
||||
&& sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
|
||||
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
|
||||
|
||||
7
dev/docker/php/conf.d/xdebug.ini
Normal file
7
dev/docker/php/conf.d/xdebug.ini
Normal file
@@ -0,0 +1,7 @@
|
||||
zend_extension=xdebug
|
||||
|
||||
[xdebug]
|
||||
xdebug.mode=debug
|
||||
xdebug.client_host=host.docker.internal
|
||||
xdebug.start_with_request=yes
|
||||
xdebug.client_port=9090
|
||||
@@ -38,7 +38,10 @@ services:
|
||||
- ${DEV_PORT:-8080}:80
|
||||
volumes:
|
||||
- ./:/app
|
||||
- ./dev/docker/php/conf.d/xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||
entrypoint: /app/dev/docker/entrypoint.app.sh
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
node:
|
||||
image: node:alpine
|
||||
working_dir: /app
|
||||
|
||||
556
package-lock.json
generated
556
package-lock.json
generated
@@ -5,20 +5,20 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.8",
|
||||
"codemirror": "^5.63.3",
|
||||
"clipboard": "^2.0.10",
|
||||
"codemirror": "^5.65.2",
|
||||
"dropzone": "^5.9.3",
|
||||
"markdown-it": "^12.2.0",
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"sortablejs": "^1.14.0"
|
||||
"sortablejs": "^1.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar-cli": "^3.0",
|
||||
"esbuild": "0.13.12",
|
||||
"esbuild": "0.14.27",
|
||||
"livereload": "^0.9.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"punycode": "^2.1.1",
|
||||
"sass": "^1.43.4"
|
||||
"sass": "^1.49.9"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
@@ -173,9 +173,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/clipboard": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.8.tgz",
|
||||
"integrity": "sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==",
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.10.tgz",
|
||||
"integrity": "sha512-cz3m2YVwFz95qSEbCDi2fzLN/epEN9zXBvfgAoGkvGOJZATMl9gtTDVOtBYkx2ODUJl2kvmud7n32sV2BpYR4g==",
|
||||
"dependencies": {
|
||||
"good-listener": "^1.2.2",
|
||||
"select": "^1.1.2",
|
||||
@@ -194,9 +194,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "5.63.3",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.63.3.tgz",
|
||||
"integrity": "sha512-1C+LELr+5grgJYqwZKqxrcbPsHFHapVaVAloBsFBASbpLnQqLw1U8yXJ3gT5D+rhxIiSpo+kTqN+hQ+9ialIXw=="
|
||||
"version": "5.65.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.2.tgz",
|
||||
"integrity": "sha512-SZM4Zq7XEC8Fhroqe3LxbEEX1zUPWH1wMr5zxiBuiUF64iYOUH/JI88v4tBag8MiBS8B8gRv8O1pPXGYXQ4ErA=="
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
@@ -341,38 +341,60 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.12.tgz",
|
||||
"integrity": "sha512-vTKKUt+yoz61U/BbrnmlG9XIjwpdIxmHB8DlPR0AAW6OdS+nBQBci6LUHU2q9WbBobMEIQxxDpKbkmOGYvxsow==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.27.tgz",
|
||||
"integrity": "sha512-MZQt5SywZS3hA9fXnMhR22dv0oPGh6QtjJRIYbgL1AeqAoQZE+Qn5ppGYQAoHv/vq827flj4tIJ79Mrdiwk46Q==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"esbuild-android-arm64": "0.13.12",
|
||||
"esbuild-darwin-64": "0.13.12",
|
||||
"esbuild-darwin-arm64": "0.13.12",
|
||||
"esbuild-freebsd-64": "0.13.12",
|
||||
"esbuild-freebsd-arm64": "0.13.12",
|
||||
"esbuild-linux-32": "0.13.12",
|
||||
"esbuild-linux-64": "0.13.12",
|
||||
"esbuild-linux-arm": "0.13.12",
|
||||
"esbuild-linux-arm64": "0.13.12",
|
||||
"esbuild-linux-mips64le": "0.13.12",
|
||||
"esbuild-linux-ppc64le": "0.13.12",
|
||||
"esbuild-netbsd-64": "0.13.12",
|
||||
"esbuild-openbsd-64": "0.13.12",
|
||||
"esbuild-sunos-64": "0.13.12",
|
||||
"esbuild-windows-32": "0.13.12",
|
||||
"esbuild-windows-64": "0.13.12",
|
||||
"esbuild-windows-arm64": "0.13.12"
|
||||
"esbuild-android-64": "0.14.27",
|
||||
"esbuild-android-arm64": "0.14.27",
|
||||
"esbuild-darwin-64": "0.14.27",
|
||||
"esbuild-darwin-arm64": "0.14.27",
|
||||
"esbuild-freebsd-64": "0.14.27",
|
||||
"esbuild-freebsd-arm64": "0.14.27",
|
||||
"esbuild-linux-32": "0.14.27",
|
||||
"esbuild-linux-64": "0.14.27",
|
||||
"esbuild-linux-arm": "0.14.27",
|
||||
"esbuild-linux-arm64": "0.14.27",
|
||||
"esbuild-linux-mips64le": "0.14.27",
|
||||
"esbuild-linux-ppc64le": "0.14.27",
|
||||
"esbuild-linux-riscv64": "0.14.27",
|
||||
"esbuild-linux-s390x": "0.14.27",
|
||||
"esbuild-netbsd-64": "0.14.27",
|
||||
"esbuild-openbsd-64": "0.14.27",
|
||||
"esbuild-sunos-64": "0.14.27",
|
||||
"esbuild-windows-32": "0.14.27",
|
||||
"esbuild-windows-64": "0.14.27",
|
||||
"esbuild-windows-arm64": "0.14.27"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-64": {
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.27.tgz",
|
||||
"integrity": "sha512-LuEd4uPuj/16Y8j6kqy3Z2E9vNY9logfq8Tq+oTE2PZVuNs3M1kj5Qd4O95ee66yDGb3isaOCV7sOLDwtMfGaQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-arm64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.12.tgz",
|
||||
"integrity": "sha512-TSVZVrb4EIXz6KaYjXfTzPyyRpXV5zgYIADXtQsIenjZ78myvDGaPi11o4ZSaHIwFHsuwkB6ne5SZRBwAQ7maw==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.27.tgz",
|
||||
"integrity": "sha512-E8Ktwwa6vX8q7QeJmg8yepBYXaee50OdQS3BFtEHKrzbV45H4foMOeEE7uqdjGQZFBap5VAqo7pvjlyA92wznQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -380,12 +402,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.12.tgz",
|
||||
"integrity": "sha512-c51C+N+UHySoV2lgfWSwwmlnLnL0JWj/LzuZt9Ltk9ub1s2Y8cr6SQV5W3mqVH1egUceew6KZ8GyI4nwu+fhsw==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.27.tgz",
|
||||
"integrity": "sha512-czw/kXl/1ZdenPWfw9jDc5iuIYxqUxgQ/Q+hRd4/3udyGGVI31r29LCViN2bAJgGvQkqyLGVcG03PJPEXQ5i2g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -393,12 +418,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-arm64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.12.tgz",
|
||||
"integrity": "sha512-JvAMtshP45Hd8A8wOzjkY1xAnTKTYuP/QUaKp5eUQGX+76GIie3fCdUUr2ZEKdvpSImNqxiZSIMziEiGB5oUmQ==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.27.tgz",
|
||||
"integrity": "sha512-BEsv2U2U4o672oV8+xpXNxN9bgqRCtddQC6WBh4YhXKDcSZcdNh7+6nS+DM2vu7qWIWNA4JbRG24LUUYXysimQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -406,12 +434,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.12.tgz",
|
||||
"integrity": "sha512-r6On/Skv9f0ZjTu6PW5o7pdXr8aOgtFOEURJZYf1XAJs0IQ+gW+o1DzXjVkIoT+n1cm3N/t1KRJfX71MPg/ZUA==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.27.tgz",
|
||||
"integrity": "sha512-7FeiFPGBo+ga+kOkDxtPmdPZdayrSzsV9pmfHxcyLKxu+3oTcajeZlOO1y9HW+t5aFZPiv7czOHM4KNd0tNwCA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -419,12 +450,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-arm64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.12.tgz",
|
||||
"integrity": "sha512-F6LmI2Q1gii073kmBE3NOTt/6zLL5zvZsxNLF8PMAwdHc+iBhD1vzfI8uQZMJA1IgXa3ocr3L3DJH9fLGXy6Yw==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.27.tgz",
|
||||
"integrity": "sha512-8CK3++foRZJluOWXpllG5zwAVlxtv36NpHfsbWS7TYlD8S+QruXltKlXToc/5ZNzBK++l6rvRKELu/puCLc7jA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -432,12 +466,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-32": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.12.tgz",
|
||||
"integrity": "sha512-U1UZwG3UIwF7/V4tCVAo/nkBV9ag5KJiJTt+gaCmLVWH3bPLX7y+fNlhIWZy8raTMnXhMKfaTvWZ9TtmXzvkuQ==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.27.tgz",
|
||||
"integrity": "sha512-qhNYIcT+EsYSBClZ5QhLzFzV5iVsP1YsITqblSaztr3+ZJUI+GoK8aXHyzKd7/CKKuK93cxEMJPpfi1dfsOfdw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -445,12 +482,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.12.tgz",
|
||||
"integrity": "sha512-YpXSwtu2NxN3N4ifJxEdsgd6Q5d8LYqskrAwjmoCT6yQnEHJSF5uWcxv783HWN7lnGpJi9KUtDvYsnMdyGw71Q==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.27.tgz",
|
||||
"integrity": "sha512-ESjck9+EsHoTaKWlFKJpPZRN26uiav5gkI16RuI8WBxUdLrrAlYuYSndxxKgEn1csd968BX/8yQZATYf/9+/qg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -458,12 +498,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.12.tgz",
|
||||
"integrity": "sha512-SyiT/JKxU6J+DY2qUiSLZJqCAftIt3uoGejZ0HDnUM2MGJqEGSGh7p1ecVL2gna3PxS4P+j6WAehCwgkBPXNIw==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.27.tgz",
|
||||
"integrity": "sha512-JnnmgUBdqLQO9hoNZQqNHFWlNpSX82vzB3rYuCJMhtkuaWQEmQz6Lec1UIxJdC38ifEghNTBsF9bbe8dFilnCw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -471,12 +514,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.12.tgz",
|
||||
"integrity": "sha512-sgDNb8kb3BVodtAlcFGgwk+43KFCYjnFOaOfJibXnnIojNWuJHpL6aQJ4mumzNWw8Rt1xEtDQyuGK9f+Y24jGA==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.27.tgz",
|
||||
"integrity": "sha512-no6Mi17eV2tHlJnqBHRLekpZ2/VYx+NfGxKcBE/2xOMYwctsanCaXxw4zapvNrGE9X38vefVXLz6YCF8b1EHiQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -484,12 +530,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-mips64le": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.12.tgz",
|
||||
"integrity": "sha512-qQJHlZBG+QwVIA8AbTEtbvF084QgDi4DaUsUnA+EolY1bxrG+UyOuGflM2ZritGhfS/k7THFjJbjH2wIeoKA2g==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.27.tgz",
|
||||
"integrity": "sha512-NolWP2uOvIJpbwpsDbwfeExZOY1bZNlWE/kVfkzLMsSgqeVcl5YMen/cedRe9mKnpfLli+i0uSp7N+fkKNU27A==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -497,12 +546,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-ppc64le": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.12.tgz",
|
||||
"integrity": "sha512-2dSnm1ldL7Lppwlo04CGQUpwNn5hGqXI38OzaoPOkRsBRWFBozyGxTFSee/zHFS+Pdh3b28JJbRK3owrrRgWNw==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.27.tgz",
|
||||
"integrity": "sha512-/7dTjDvXMdRKmsSxKXeWyonuGgblnYDn0MI1xDC7J1VQXny8k1qgNp6VmrlsawwnsymSUUiThhkJsI+rx0taNA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -510,12 +562,47 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-riscv64": {
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.27.tgz",
|
||||
"integrity": "sha512-D+aFiUzOJG13RhrSmZgrcFaF4UUHpqj7XSKrIiCXIj1dkIkFqdrmqMSOtSs78dOtObWiOrFCDDzB24UyeEiNGg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-s390x": {
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.27.tgz",
|
||||
"integrity": "sha512-CD/D4tj0U4UQjELkdNlZhQ8nDHU5rBn6NGp47Hiz0Y7/akAY5i0oGadhEIg0WCY/HYVXFb3CsSPPwaKcTOW3bg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-netbsd-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.12.tgz",
|
||||
"integrity": "sha512-D4raxr02dcRiQNbxOLzpqBzcJNFAdsDNxjUbKkDMZBkL54Z0vZh4LRndycdZAMcIdizC/l/Yp/ZsBdAFxc5nbA==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.27.tgz",
|
||||
"integrity": "sha512-h3mAld69SrO1VoaMpYl3a5FNdGRE/Nqc+E8VtHOag4tyBwhCQXxtvDDOAKOUQexBGca0IuR6UayQ4ntSX5ij1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -523,12 +610,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-openbsd-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.12.tgz",
|
||||
"integrity": "sha512-KuLCmYMb2kh05QuPJ+va60bKIH5wHL8ypDkmpy47lzwmdxNsuySeCMHuTv5o2Af1RUn5KLO5ZxaZeq4GEY7DaQ==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.27.tgz",
|
||||
"integrity": "sha512-xwSje6qIZaDHXWoPpIgvL+7fC6WeubHHv18tusLYMwL+Z6bEa4Pbfs5IWDtQdHkArtfxEkIZz77944z8MgDxGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -536,12 +626,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-sunos-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.12.tgz",
|
||||
"integrity": "sha512-jBsF+e0woK3miKI8ufGWKG3o3rY9DpHvCVRn5eburMIIE+2c+y3IZ1srsthKyKI6kkXLvV4Cf/E7w56kLipMXw==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.27.tgz",
|
||||
"integrity": "sha512-/nBVpWIDjYiyMhuqIqbXXsxBc58cBVH9uztAOIfWShStxq9BNBik92oPQPJ57nzWXRNKQUEFWr4Q98utDWz7jg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -549,12 +642,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-32": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.12.tgz",
|
||||
"integrity": "sha512-L9m4lLFQrFeR7F+eLZXG82SbXZfUhyfu6CexZEil6vm+lc7GDCE0Q8DiNutkpzjv1+RAbIGVva9muItQ7HVTkQ==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.27.tgz",
|
||||
"integrity": "sha512-Q9/zEjhZJ4trtWhFWIZvS/7RUzzi8rvkoaS9oiizkHTTKd8UxFwn/Mm2OywsAfYymgUYm8+y2b+BKTNEFxUekw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -562,12 +658,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.12.tgz",
|
||||
"integrity": "sha512-k4tX4uJlSbSkfs78W5d9+I9gpd+7N95W7H2bgOMFPsYREVJs31+Q2gLLHlsnlY95zBoPQMIzHooUIsixQIBjaQ==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.27.tgz",
|
||||
"integrity": "sha512-b3y3vTSl5aEhWHK66ngtiS/c6byLf6y/ZBvODH1YkBM+MGtVL6jN38FdHUsZasCz9gFwYs/lJMVY9u7GL6wfYg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -575,12 +674,15 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-arm64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.12.tgz",
|
||||
"integrity": "sha512-2tTv/BpYRIvuwHpp2M960nG7uvL+d78LFW/ikPItO+2GfK51CswIKSetSpDii+cjz8e9iSPgs+BU4o8nWICBwQ==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.27.tgz",
|
||||
"integrity": "sha512-I/reTxr6TFMcR5qbIkwRGvldMIaiBu2+MP0LlD7sOlNXrfqIl9uNjsuxFPGEG4IRomjfQ5q8WT+xlF/ySVkqKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -588,7 +690,10 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
@@ -771,6 +876,12 @@
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
|
||||
"integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
|
||||
@@ -1088,9 +1199,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "12.2.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.2.0.tgz",
|
||||
"integrity": "sha512-Wjws+uCrVQRqOoJvze4HCqkKl1AsSh95iFAeQDwnyfxM09divCBSXlDR1uTvyUP3Grzpn4Ru8GeCxYPM8vkCQg==",
|
||||
"version": "12.3.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
|
||||
"integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~2.1.0",
|
||||
@@ -1409,18 +1520,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.43.4",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.43.4.tgz",
|
||||
"integrity": "sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==",
|
||||
"version": "1.49.9",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz",
|
||||
"integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0"
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/select": {
|
||||
@@ -1485,9 +1598,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
|
||||
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
|
||||
"integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w=="
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spdx-correct": {
|
||||
"version": "3.1.1",
|
||||
@@ -1880,9 +2002,9 @@
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.8.tgz",
|
||||
"integrity": "sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==",
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.10.tgz",
|
||||
"integrity": "sha512-cz3m2YVwFz95qSEbCDi2fzLN/epEN9zXBvfgAoGkvGOJZATMl9gtTDVOtBYkx2ODUJl2kvmud7n32sV2BpYR4g==",
|
||||
"requires": {
|
||||
"good-listener": "^1.2.2",
|
||||
"select": "^1.1.2",
|
||||
@@ -1901,9 +2023,9 @@
|
||||
}
|
||||
},
|
||||
"codemirror": {
|
||||
"version": "5.63.3",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.63.3.tgz",
|
||||
"integrity": "sha512-1C+LELr+5grgJYqwZKqxrcbPsHFHapVaVAloBsFBASbpLnQqLw1U8yXJ3gT5D+rhxIiSpo+kTqN+hQ+9ialIXw=="
|
||||
"version": "5.65.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.2.tgz",
|
||||
"integrity": "sha512-SZM4Zq7XEC8Fhroqe3LxbEEX1zUPWH1wMr5zxiBuiUF64iYOUH/JI88v4tBag8MiBS8B8gRv8O1pPXGYXQ4ErA=="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
@@ -2024,146 +2146,170 @@
|
||||
}
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.12.tgz",
|
||||
"integrity": "sha512-vTKKUt+yoz61U/BbrnmlG9XIjwpdIxmHB8DlPR0AAW6OdS+nBQBci6LUHU2q9WbBobMEIQxxDpKbkmOGYvxsow==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.27.tgz",
|
||||
"integrity": "sha512-MZQt5SywZS3hA9fXnMhR22dv0oPGh6QtjJRIYbgL1AeqAoQZE+Qn5ppGYQAoHv/vq827flj4tIJ79Mrdiwk46Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild-android-arm64": "0.13.12",
|
||||
"esbuild-darwin-64": "0.13.12",
|
||||
"esbuild-darwin-arm64": "0.13.12",
|
||||
"esbuild-freebsd-64": "0.13.12",
|
||||
"esbuild-freebsd-arm64": "0.13.12",
|
||||
"esbuild-linux-32": "0.13.12",
|
||||
"esbuild-linux-64": "0.13.12",
|
||||
"esbuild-linux-arm": "0.13.12",
|
||||
"esbuild-linux-arm64": "0.13.12",
|
||||
"esbuild-linux-mips64le": "0.13.12",
|
||||
"esbuild-linux-ppc64le": "0.13.12",
|
||||
"esbuild-netbsd-64": "0.13.12",
|
||||
"esbuild-openbsd-64": "0.13.12",
|
||||
"esbuild-sunos-64": "0.13.12",
|
||||
"esbuild-windows-32": "0.13.12",
|
||||
"esbuild-windows-64": "0.13.12",
|
||||
"esbuild-windows-arm64": "0.13.12"
|
||||
"esbuild-android-64": "0.14.27",
|
||||
"esbuild-android-arm64": "0.14.27",
|
||||
"esbuild-darwin-64": "0.14.27",
|
||||
"esbuild-darwin-arm64": "0.14.27",
|
||||
"esbuild-freebsd-64": "0.14.27",
|
||||
"esbuild-freebsd-arm64": "0.14.27",
|
||||
"esbuild-linux-32": "0.14.27",
|
||||
"esbuild-linux-64": "0.14.27",
|
||||
"esbuild-linux-arm": "0.14.27",
|
||||
"esbuild-linux-arm64": "0.14.27",
|
||||
"esbuild-linux-mips64le": "0.14.27",
|
||||
"esbuild-linux-ppc64le": "0.14.27",
|
||||
"esbuild-linux-riscv64": "0.14.27",
|
||||
"esbuild-linux-s390x": "0.14.27",
|
||||
"esbuild-netbsd-64": "0.14.27",
|
||||
"esbuild-openbsd-64": "0.14.27",
|
||||
"esbuild-sunos-64": "0.14.27",
|
||||
"esbuild-windows-32": "0.14.27",
|
||||
"esbuild-windows-64": "0.14.27",
|
||||
"esbuild-windows-arm64": "0.14.27"
|
||||
}
|
||||
},
|
||||
"esbuild-android-64": {
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.27.tgz",
|
||||
"integrity": "sha512-LuEd4uPuj/16Y8j6kqy3Z2E9vNY9logfq8Tq+oTE2PZVuNs3M1kj5Qd4O95ee66yDGb3isaOCV7sOLDwtMfGaQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-android-arm64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.12.tgz",
|
||||
"integrity": "sha512-TSVZVrb4EIXz6KaYjXfTzPyyRpXV5zgYIADXtQsIenjZ78myvDGaPi11o4ZSaHIwFHsuwkB6ne5SZRBwAQ7maw==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.27.tgz",
|
||||
"integrity": "sha512-E8Ktwwa6vX8q7QeJmg8yepBYXaee50OdQS3BFtEHKrzbV45H4foMOeEE7uqdjGQZFBap5VAqo7pvjlyA92wznQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-darwin-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.12.tgz",
|
||||
"integrity": "sha512-c51C+N+UHySoV2lgfWSwwmlnLnL0JWj/LzuZt9Ltk9ub1s2Y8cr6SQV5W3mqVH1egUceew6KZ8GyI4nwu+fhsw==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.27.tgz",
|
||||
"integrity": "sha512-czw/kXl/1ZdenPWfw9jDc5iuIYxqUxgQ/Q+hRd4/3udyGGVI31r29LCViN2bAJgGvQkqyLGVcG03PJPEXQ5i2g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-darwin-arm64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.12.tgz",
|
||||
"integrity": "sha512-JvAMtshP45Hd8A8wOzjkY1xAnTKTYuP/QUaKp5eUQGX+76GIie3fCdUUr2ZEKdvpSImNqxiZSIMziEiGB5oUmQ==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.27.tgz",
|
||||
"integrity": "sha512-BEsv2U2U4o672oV8+xpXNxN9bgqRCtddQC6WBh4YhXKDcSZcdNh7+6nS+DM2vu7qWIWNA4JbRG24LUUYXysimQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-freebsd-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.12.tgz",
|
||||
"integrity": "sha512-r6On/Skv9f0ZjTu6PW5o7pdXr8aOgtFOEURJZYf1XAJs0IQ+gW+o1DzXjVkIoT+n1cm3N/t1KRJfX71MPg/ZUA==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.27.tgz",
|
||||
"integrity": "sha512-7FeiFPGBo+ga+kOkDxtPmdPZdayrSzsV9pmfHxcyLKxu+3oTcajeZlOO1y9HW+t5aFZPiv7czOHM4KNd0tNwCA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-freebsd-arm64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.12.tgz",
|
||||
"integrity": "sha512-F6LmI2Q1gii073kmBE3NOTt/6zLL5zvZsxNLF8PMAwdHc+iBhD1vzfI8uQZMJA1IgXa3ocr3L3DJH9fLGXy6Yw==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.27.tgz",
|
||||
"integrity": "sha512-8CK3++foRZJluOWXpllG5zwAVlxtv36NpHfsbWS7TYlD8S+QruXltKlXToc/5ZNzBK++l6rvRKELu/puCLc7jA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-32": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.12.tgz",
|
||||
"integrity": "sha512-U1UZwG3UIwF7/V4tCVAo/nkBV9ag5KJiJTt+gaCmLVWH3bPLX7y+fNlhIWZy8raTMnXhMKfaTvWZ9TtmXzvkuQ==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.27.tgz",
|
||||
"integrity": "sha512-qhNYIcT+EsYSBClZ5QhLzFzV5iVsP1YsITqblSaztr3+ZJUI+GoK8aXHyzKd7/CKKuK93cxEMJPpfi1dfsOfdw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.12.tgz",
|
||||
"integrity": "sha512-YpXSwtu2NxN3N4ifJxEdsgd6Q5d8LYqskrAwjmoCT6yQnEHJSF5uWcxv783HWN7lnGpJi9KUtDvYsnMdyGw71Q==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.27.tgz",
|
||||
"integrity": "sha512-ESjck9+EsHoTaKWlFKJpPZRN26uiav5gkI16RuI8WBxUdLrrAlYuYSndxxKgEn1csd968BX/8yQZATYf/9+/qg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-arm": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.12.tgz",
|
||||
"integrity": "sha512-SyiT/JKxU6J+DY2qUiSLZJqCAftIt3uoGejZ0HDnUM2MGJqEGSGh7p1ecVL2gna3PxS4P+j6WAehCwgkBPXNIw==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.27.tgz",
|
||||
"integrity": "sha512-JnnmgUBdqLQO9hoNZQqNHFWlNpSX82vzB3rYuCJMhtkuaWQEmQz6Lec1UIxJdC38ifEghNTBsF9bbe8dFilnCw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-arm64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.12.tgz",
|
||||
"integrity": "sha512-sgDNb8kb3BVodtAlcFGgwk+43KFCYjnFOaOfJibXnnIojNWuJHpL6aQJ4mumzNWw8Rt1xEtDQyuGK9f+Y24jGA==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.27.tgz",
|
||||
"integrity": "sha512-no6Mi17eV2tHlJnqBHRLekpZ2/VYx+NfGxKcBE/2xOMYwctsanCaXxw4zapvNrGE9X38vefVXLz6YCF8b1EHiQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-mips64le": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.12.tgz",
|
||||
"integrity": "sha512-qQJHlZBG+QwVIA8AbTEtbvF084QgDi4DaUsUnA+EolY1bxrG+UyOuGflM2ZritGhfS/k7THFjJbjH2wIeoKA2g==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.27.tgz",
|
||||
"integrity": "sha512-NolWP2uOvIJpbwpsDbwfeExZOY1bZNlWE/kVfkzLMsSgqeVcl5YMen/cedRe9mKnpfLli+i0uSp7N+fkKNU27A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-ppc64le": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.12.tgz",
|
||||
"integrity": "sha512-2dSnm1ldL7Lppwlo04CGQUpwNn5hGqXI38OzaoPOkRsBRWFBozyGxTFSee/zHFS+Pdh3b28JJbRK3owrrRgWNw==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.27.tgz",
|
||||
"integrity": "sha512-/7dTjDvXMdRKmsSxKXeWyonuGgblnYDn0MI1xDC7J1VQXny8k1qgNp6VmrlsawwnsymSUUiThhkJsI+rx0taNA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-riscv64": {
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.27.tgz",
|
||||
"integrity": "sha512-D+aFiUzOJG13RhrSmZgrcFaF4UUHpqj7XSKrIiCXIj1dkIkFqdrmqMSOtSs78dOtObWiOrFCDDzB24UyeEiNGg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-s390x": {
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.27.tgz",
|
||||
"integrity": "sha512-CD/D4tj0U4UQjELkdNlZhQ8nDHU5rBn6NGp47Hiz0Y7/akAY5i0oGadhEIg0WCY/HYVXFb3CsSPPwaKcTOW3bg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-netbsd-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.12.tgz",
|
||||
"integrity": "sha512-D4raxr02dcRiQNbxOLzpqBzcJNFAdsDNxjUbKkDMZBkL54Z0vZh4LRndycdZAMcIdizC/l/Yp/ZsBdAFxc5nbA==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.27.tgz",
|
||||
"integrity": "sha512-h3mAld69SrO1VoaMpYl3a5FNdGRE/Nqc+E8VtHOag4tyBwhCQXxtvDDOAKOUQexBGca0IuR6UayQ4ntSX5ij1Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-openbsd-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.12.tgz",
|
||||
"integrity": "sha512-KuLCmYMb2kh05QuPJ+va60bKIH5wHL8ypDkmpy47lzwmdxNsuySeCMHuTv5o2Af1RUn5KLO5ZxaZeq4GEY7DaQ==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.27.tgz",
|
||||
"integrity": "sha512-xwSje6qIZaDHXWoPpIgvL+7fC6WeubHHv18tusLYMwL+Z6bEa4Pbfs5IWDtQdHkArtfxEkIZz77944z8MgDxGw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-sunos-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.12.tgz",
|
||||
"integrity": "sha512-jBsF+e0woK3miKI8ufGWKG3o3rY9DpHvCVRn5eburMIIE+2c+y3IZ1srsthKyKI6kkXLvV4Cf/E7w56kLipMXw==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.27.tgz",
|
||||
"integrity": "sha512-/nBVpWIDjYiyMhuqIqbXXsxBc58cBVH9uztAOIfWShStxq9BNBik92oPQPJ57nzWXRNKQUEFWr4Q98utDWz7jg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-32": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.12.tgz",
|
||||
"integrity": "sha512-L9m4lLFQrFeR7F+eLZXG82SbXZfUhyfu6CexZEil6vm+lc7GDCE0Q8DiNutkpzjv1+RAbIGVva9muItQ7HVTkQ==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.27.tgz",
|
||||
"integrity": "sha512-Q9/zEjhZJ4trtWhFWIZvS/7RUzzi8rvkoaS9oiizkHTTKd8UxFwn/Mm2OywsAfYymgUYm8+y2b+BKTNEFxUekw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.12.tgz",
|
||||
"integrity": "sha512-k4tX4uJlSbSkfs78W5d9+I9gpd+7N95W7H2bgOMFPsYREVJs31+Q2gLLHlsnlY95zBoPQMIzHooUIsixQIBjaQ==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.27.tgz",
|
||||
"integrity": "sha512-b3y3vTSl5aEhWHK66ngtiS/c6byLf6y/ZBvODH1YkBM+MGtVL6jN38FdHUsZasCz9gFwYs/lJMVY9u7GL6wfYg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-arm64": {
|
||||
"version": "0.13.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.12.tgz",
|
||||
"integrity": "sha512-2tTv/BpYRIvuwHpp2M960nG7uvL+d78LFW/ikPItO+2GfK51CswIKSetSpDii+cjz8e9iSPgs+BU4o8nWICBwQ==",
|
||||
"version": "0.14.27",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.27.tgz",
|
||||
"integrity": "sha512-I/reTxr6TFMcR5qbIkwRGvldMIaiBu2+MP0LlD7sOlNXrfqIl9uNjsuxFPGEG4IRomjfQ5q8WT+xlF/ySVkqKg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
@@ -2296,6 +2442,12 @@
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"immutable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
|
||||
"integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
|
||||
"dev": true
|
||||
},
|
||||
"internal-slot": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
|
||||
@@ -2523,9 +2675,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "12.2.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.2.0.tgz",
|
||||
"integrity": "sha512-Wjws+uCrVQRqOoJvze4HCqkKl1AsSh95iFAeQDwnyfxM09divCBSXlDR1uTvyUP3Grzpn4Ru8GeCxYPM8vkCQg==",
|
||||
"version": "12.3.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
|
||||
"integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~2.1.0",
|
||||
@@ -2758,12 +2910,14 @@
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.43.4",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.43.4.tgz",
|
||||
"integrity": "sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==",
|
||||
"version": "1.49.9",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz",
|
||||
"integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0"
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
@@ -2816,9 +2970,15 @@
|
||||
}
|
||||
},
|
||||
"sortablejs": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
|
||||
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
|
||||
"integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w=="
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true
|
||||
},
|
||||
"spdx-correct": {
|
||||
"version": "3.1.1",
|
||||
|
||||
16
package.json
16
package.json
@@ -4,9 +4,9 @@
|
||||
"build:css:dev": "sass ./resources/sass:./public/dist",
|
||||
"build:css:watch": "sass ./resources/sass:./public/dist --watch",
|
||||
"build:css:production": "sass ./resources/sass:./public/dist -s compressed",
|
||||
"build:js:dev": "esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main",
|
||||
"build:js:dev": "esbuild --bundle ./resources/js/*.{js,mjs} --outdir=public/dist/ --sourcemap --target=es2020 --main-fields=module,main --format=esm",
|
||||
"build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
|
||||
"build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main --minify",
|
||||
"build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/*.{js,mjs} --outdir=public/dist/ --sourcemap --target=es2020 --main-fields=module,main --minify --format=esm",
|
||||
"build": "npm-run-all --parallel build:*:dev",
|
||||
"production": "npm-run-all --parallel build:*:production",
|
||||
"dev": "npm-run-all --parallel watch livereload",
|
||||
@@ -16,18 +16,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar-cli": "^3.0",
|
||||
"esbuild": "0.13.12",
|
||||
"esbuild": "0.14.27",
|
||||
"livereload": "^0.9.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"punycode": "^2.1.1",
|
||||
"sass": "^1.43.4"
|
||||
"sass": "^1.49.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.8",
|
||||
"codemirror": "^5.63.3",
|
||||
"clipboard": "^2.0.10",
|
||||
"codemirror": "^5.65.2",
|
||||
"dropzone": "^5.9.3",
|
||||
"markdown-it": "^12.2.0",
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"sortablejs": "^1.14.0"
|
||||
"sortablejs": "^1.15.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ parameters:
|
||||
# The level 8 is the highest level
|
||||
level: 1
|
||||
|
||||
phpVersion: 70300
|
||||
phpVersion: 70400
|
||||
|
||||
bootstrapFiles:
|
||||
- bootstrap/phpstan.php
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
<server name="AVATAR_URL" value=""/>
|
||||
<server name="LDAP_START_TLS" value="false"/>
|
||||
<server name="LDAP_VERSION" value="3"/>
|
||||
<server name="LDAP_DUMP_USER_DETAILS" value="false"/>
|
||||
<server name="LDAP_DUMP_USER_GROUPS" value="false"/>
|
||||
<server name="SESSION_SECURE_COOKIE" value="null"/>
|
||||
<server name="STORAGE_TYPE" value="local"/>
|
||||
<server name="STORAGE_ATTACHMENT_TYPE" value="local"/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user