mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-05 16:49:47 +03:00
Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9078188939 | ||
|
|
ed0aad1a7a | ||
|
|
43749cd94e | ||
|
|
107df6c28f | ||
|
|
c1d1ec5b89 | ||
|
|
12c282597d | ||
|
|
c9d0e22132 | ||
|
|
0801955a26 | ||
|
|
3ed1ffdbeb | ||
|
|
5c59cfb020 | ||
|
|
3ca15ad68a | ||
|
|
36f0a68f1b | ||
|
|
ed981cbab1 | ||
|
|
f69af8933c | ||
|
|
46d71a181e | ||
|
|
8d8da31fdd | ||
|
|
0d9b5a9d90 | ||
|
|
8b211ed461 | ||
|
|
9dd69b04b8 | ||
|
|
0c6f598d91 | ||
|
|
df94b73e29 | ||
|
|
7d4b941abf | ||
|
|
d181106df3 | ||
|
|
75110813e6 | ||
|
|
1e41546e51 | ||
|
|
f39b565a1c | ||
|
|
77cd550fae | ||
|
|
96d9077479 | ||
|
|
be1d691529 | ||
|
|
8cde362f6f | ||
|
|
388343aeb0 | ||
|
|
ba25dda031 | ||
|
|
85f59b5275 | ||
|
|
65d4505079 | ||
|
|
663f81a2b1 | ||
|
|
f145ffc930 | ||
|
|
19d7e26dda | ||
|
|
a13b9d8d14 | ||
|
|
8c67011a1d | ||
|
|
8da856bac3 | ||
|
|
90ec40691a | ||
|
|
d676e1e824 | ||
|
|
0a05119aa5 | ||
|
|
abc283fc64 | ||
|
|
e72ade727d | ||
|
|
c8b123bfac | ||
|
|
88012449f3 | ||
|
|
e00d88f45d | ||
|
|
3fe666f36a | ||
|
|
3f271ebecb | ||
|
|
7c597a05f6 | ||
|
|
16e023985d | ||
|
|
43cbab2822 | ||
|
|
1a3505c899 | ||
|
|
2930025f51 | ||
|
|
39fcf3a68f | ||
|
|
6ce34fe6cc | ||
|
|
3c3aed58aa | ||
|
|
73f36b279e | ||
|
|
2b817e7d24 | ||
|
|
cb10ad804f | ||
|
|
eeccc2ef10 | ||
|
|
b030c1398b | ||
|
|
4759fa1e1f | ||
|
|
cb1c2db282 | ||
|
|
4866a3a198 | ||
|
|
340c9ec7a1 | ||
|
|
49498cfaf9 | ||
|
|
3a4aa81115 | ||
|
|
d20c74babf | ||
|
|
9fda0df798 | ||
|
|
6fa699a835 | ||
|
|
78920d7d65 | ||
|
|
35a47a273b | ||
|
|
89dfa43e73 | ||
|
|
2c74dfd1d4 | ||
|
|
e6864a9cff | ||
|
|
60e319c4b4 | ||
|
|
24b31b624c | ||
|
|
a0fe6147d8 | ||
|
|
221d910ff2 | ||
|
|
bef2045df1 | ||
|
|
f021823287 | ||
|
|
60014989f5 | ||
|
|
57b10f195e | ||
|
|
3a8a476906 | ||
|
|
328bc88f02 | ||
|
|
2a99e23e6d | ||
|
|
b855bbaaea | ||
|
|
96436839f1 | ||
|
|
b4f29a85ab | ||
|
|
4a2a044f3d | ||
|
|
ca09ed916f | ||
|
|
dbefda055f | ||
|
|
b1e95eb39f | ||
|
|
b3da77b8f9 | ||
|
|
93ef8c97b6 | ||
|
|
420b29f32f | ||
|
|
d795af04df | ||
|
|
d2ed98d20d | ||
|
|
ebc69a8f2c | ||
|
|
d5ce6b680c | ||
|
|
1a345b74bb | ||
|
|
8ffc3a4abf | ||
|
|
44013721f0 | ||
|
|
16222de5fa | ||
|
|
ebfe946160 | ||
|
|
5d2aad6a9e | ||
|
|
8fb016d1bf | ||
|
|
c216a6a210 | ||
|
|
26af9acc6c | ||
|
|
c8a7acb6c7 | ||
|
|
d3b39fbe50 | ||
|
|
ac7b2dd1bf | ||
|
|
f1a8ad4980 | ||
|
|
d5b7fff102 | ||
|
|
0930e8519c | ||
|
|
ff8dadefee | ||
|
|
2b0ae23da0 | ||
|
|
63cb6015a8 | ||
|
|
5a7fb20116 | ||
|
|
829f808800 | ||
|
|
0dfe5cb66b | ||
|
|
14bccae6bd | ||
|
|
b97c150ac8 | ||
|
|
0c5723d76e | ||
|
|
bec61a56c0 | ||
|
|
1b46aa8756 | ||
|
|
f14e6e8f2d | ||
|
|
0003ce61cd | ||
|
|
d76bbb2954 | ||
|
|
478067483f | ||
|
|
eff539f89b | ||
|
|
214992650d | ||
|
|
492ffff0a4 | ||
|
|
956eb1308f | ||
|
|
0cc215f8c3 | ||
|
|
e8e38f1f7b | ||
|
|
7dc80a9e14 | ||
|
|
e49afdbd72 | ||
|
|
56254bdb66 | ||
|
|
25654b2322 | ||
|
|
27339079f7 | ||
|
|
55e52e45fb | ||
|
|
c979e6465e | ||
|
|
c30a9d3564 | ||
|
|
59d1fb2d10 | ||
|
|
08a8c0070e | ||
|
|
cb770c534d | ||
|
|
6749faa89a | ||
|
|
82e8b1577e | ||
|
|
4dce03c0d3 | ||
|
|
7233c1c7b2 | ||
|
|
1309a01131 | ||
|
|
affae2e3c4 | ||
|
|
1a90b98b8f | ||
|
|
da4308bb0f | ||
|
|
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 |
@@ -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
|
||||
@@ -136,6 +143,10 @@ STORAGE_URL=false
|
||||
# Can be 'standard', 'ldap', 'saml2' or 'oidc'
|
||||
AUTH_METHOD=standard
|
||||
|
||||
# Automatically initiate login via external auth system if it's the only auth method.
|
||||
# Works with saml2 or oidc auth methods.
|
||||
AUTH_AUTO_INITIATE=false
|
||||
|
||||
# Social authentication configuration
|
||||
# All disabled by default.
|
||||
# Refer to https://www.bookstackapp.com/docs/admin/third-party-auth/
|
||||
@@ -216,6 +227,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/
|
||||
@@ -266,7 +278,7 @@ AVATAR_URL=
|
||||
# Enable diagrams.net integration
|
||||
# Can simply be true/false to enable/disable the integration.
|
||||
# Alternatively, It can be URL to the diagrams.net instance you want to use.
|
||||
# For URLs, The following URL parameters should be included: embed=1&proto=json&spin=1
|
||||
# For URLs, The following URL parameters should be included: embed=1&proto=json&spin=1&configure=1
|
||||
DRAWIO=true
|
||||
|
||||
# Default item listing view
|
||||
@@ -324,6 +336,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/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,3 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [ssddanbrown]
|
||||
ko_fi: ssddanbrown
|
||||
9
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
9
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -35,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:
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,9 +1,13 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discord chat support
|
||||
- name: Discord Chat Support
|
||||
url: https://discord.gg/ztkBqR2
|
||||
about: Realtime support / chat with the community and the team.
|
||||
about: Realtime support & chat with the BookStack community and the team.
|
||||
|
||||
- name: Debugging & Common Issues
|
||||
url: https://www.bookstackapp.com/docs/admin/debugging/
|
||||
about: Find details on how to debug issues and view common issues with thier resolutions.
|
||||
about: Find details on how to debug issues and view common issues with their resolutions.
|
||||
|
||||
- name: Official Support Plans
|
||||
url: https://www.bookstackapp.com/support/
|
||||
about: View our official support plans that offer assured support for business.
|
||||
3
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
3
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -33,8 +33,7 @@ body:
|
||||
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.
|
||||
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
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/language_request.yml
vendored
4
.github/ISSUE_TEMPLATE/language_request.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Language Request
|
||||
description: Request a new language to be added to CrowdIn for you to translate
|
||||
description: Request a new language to be added to Crowdin for you to translate
|
||||
labels: [":earth_africa: Translations"]
|
||||
assignees:
|
||||
- ssddanbrown
|
||||
@@ -23,7 +23,7 @@ body:
|
||||
This issue template is to request a new language be added to our [Crowdin translation management project](https://crowdin.com/project/bookstack).
|
||||
Please don't use this template to request a new language that you are not prepared to provide translations for.
|
||||
options:
|
||||
- label: I confirm I'm offering to help translate for this new language via CrowdIn.
|
||||
- label: I confirm I'm offering to help translate for this new language via Crowdin.
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
30
.github/translators.txt
vendored
30
.github/translators.txt
vendored
@@ -165,7 +165,7 @@ Francesco Franchina (ffranchina) :: Italian
|
||||
Aimrane Kds (aimrane.kds) :: Arabic
|
||||
whenwesober :: Indonesian
|
||||
Rem (remkovdhoef) :: Dutch
|
||||
syn7ax69 :: Bulgarian; Turkish
|
||||
syn7ax69 :: Bulgarian; Turkish; German
|
||||
Blaade :: French
|
||||
Behzad HosseinPoor (behzad.hp) :: Persian
|
||||
Ole Aldric (Swoy) :: Norwegian Bokmal
|
||||
@@ -230,3 +230,31 @@ 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
|
||||
sgenc :: Turkish
|
||||
Shukrullo (vodiylik) :: Uzbek
|
||||
William W. (Nevnt) :: Chinese Traditional
|
||||
eamaro :: Portuguese
|
||||
Ypsilon-dev :: Arabic
|
||||
Hieu Vuong Trung (vuongtrunghieu) :: Vietnamese
|
||||
David Clubb (davidoclubb) :: Welsh
|
||||
welles freire (wellesximenes) :: Portuguese, Brazilian
|
||||
Magnus Jensen (MagnusHJensen) :: Danish
|
||||
Hesley Magno (hesleymagno) :: Portuguese, Brazilian
|
||||
Éric Gaspar (erga) :: French
|
||||
Fr3shlama :: German
|
||||
DSR :: Spanish, Argentina
|
||||
Andrii Bodnar (andrii-bodnar) :: Ukrainian
|
||||
Younes el Anjri (younesea28) :: Dutch
|
||||
Guclu Ozturk (gucluoz) :: Turkish
|
||||
Atmis :: French
|
||||
redjack666 :: Chinese Traditional
|
||||
Ashita007 :: Russian
|
||||
lihaorr :: Chinese Simplified
|
||||
|
||||
@@ -16,11 +16,13 @@ class ActivityType
|
||||
const CHAPTER_MOVE = 'chapter_move';
|
||||
|
||||
const BOOK_CREATE = 'book_create';
|
||||
const BOOK_CREATE_FROM_CHAPTER = 'book_create_from_chapter';
|
||||
const BOOK_UPDATE = 'book_update';
|
||||
const BOOK_DELETE = 'book_delete';
|
||||
const BOOK_SORT = 'book_sort';
|
||||
|
||||
const BOOKSHELF_CREATE = 'bookshelf_create';
|
||||
const BOOKSHELF_CREATE_FROM_BOOK = 'bookshelf_create_from_book';
|
||||
const BOOKSHELF_UPDATE = 'bookshelf_update';
|
||||
const BOOKSHELF_DELETE = 'bookshelf_delete';
|
||||
|
||||
|
||||
@@ -3,17 +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;
|
||||
|
||||
@@ -24,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.
|
||||
*
|
||||
@@ -70,8 +52,8 @@ class DispatchWebhookJob implements ShouldQueue
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$themeResponse = Theme::dispatch(ThemeEvents::WEBHOOK_CALL_BEFORE, $this->event, $this->webhook, $this->detail);
|
||||
$webhookData = $themeResponse ?? $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;
|
||||
|
||||
try {
|
||||
@@ -97,36 +79,4 @@ class DispatchWebhookJob implements ShouldQueue
|
||||
|
||||
$this->webhook->save();
|
||||
}
|
||||
|
||||
protected function buildWebhookData(): array
|
||||
{
|
||||
$textParts = [
|
||||
$this->initiator->name,
|
||||
trans('activities.' . $this->event),
|
||||
];
|
||||
|
||||
if ($this->detail instanceof Entity) {
|
||||
$textParts[] = '"' . $this->detail->name . '"';
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ class TagRepo
|
||||
'name',
|
||||
($searchTerm || $nameFilter) ? 'value' : DB::raw('COUNT(distinct value) as `values`'),
|
||||
DB::raw('COUNT(id) as usages'),
|
||||
DB::raw('SUM(IF(entity_type = \'BookStack\\\\Page\', 1, 0)) as page_count'),
|
||||
DB::raw('SUM(IF(entity_type = \'BookStack\\\\Chapter\', 1, 0)) as chapter_count'),
|
||||
DB::raw('SUM(IF(entity_type = \'BookStack\\\\Book\', 1, 0)) as book_count'),
|
||||
DB::raw('SUM(IF(entity_type = \'BookStack\\\\BookShelf\', 1, 0)) as shelf_count'),
|
||||
DB::raw('SUM(IF(entity_type = \'page\', 1, 0)) as page_count'),
|
||||
DB::raw('SUM(IF(entity_type = \'chapter\', 1, 0)) as chapter_count'),
|
||||
DB::raw('SUM(IF(entity_type = \'book\', 1, 0)) as book_count'),
|
||||
DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'),
|
||||
])
|
||||
->orderBy($nameFilter ? 'value' : 'name');
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,8 @@ class GroupSyncService
|
||||
*/
|
||||
protected function externalIdMatchesGroupNames(string $externalId, array $groupNames): bool
|
||||
{
|
||||
$externalAuthIds = explode(',', strtolower($externalId));
|
||||
|
||||
foreach ($externalAuthIds as $externalAuthId) {
|
||||
if (in_array(trim($externalAuthId), $groupNames)) {
|
||||
foreach ($this->parseRoleExternalAuthId($externalId) as $externalAuthId) {
|
||||
if (in_array($externalAuthId, $groupNames)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +37,18 @@ class GroupSyncService
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function parseRoleExternalAuthId(string $externalId): array
|
||||
{
|
||||
$inputIds = preg_split('/(?<!\\\),/', $externalId);
|
||||
$cleanIds = [];
|
||||
|
||||
foreach ($inputIds as $inputId) {
|
||||
$cleanIds[] = str_replace('\,', ',', trim($inputId));
|
||||
}
|
||||
|
||||
return $cleanIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match an array of group names to BookStack system roles.
|
||||
* Formats group names to be lower-case and hyphenated.
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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', 'cy', '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', 'uz', 'vi', 'zh_CN', 'zh_TW'],
|
||||
|
||||
// Application Fallback Locale
|
||||
'fallback_locale' => 'en',
|
||||
|
||||
@@ -13,6 +13,10 @@ return [
|
||||
// Options: standard, ldap, saml2, oidc
|
||||
'method' => env('AUTH_METHOD', 'standard'),
|
||||
|
||||
// Automatically initiate login via external auth system if it's the sole auth method.
|
||||
// Works with saml2 or oidc auth methods.
|
||||
'auto_initiate' => env('AUTH_AUTO_INITIATE', false),
|
||||
|
||||
// Authentication Defaults
|
||||
// This option controls the default authentication "guard" and password
|
||||
// reset options for your application.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -72,7 +72,7 @@ return [
|
||||
// to the server if the browser has a HTTPS connection. This will keep
|
||||
// the cookie from being sent to you if it can not be done securely.
|
||||
'secure' => env('SESSION_SECURE_COOKIE', null)
|
||||
?? Str::startsWith(env('APP_URL'), 'https:'),
|
||||
?? Str::startsWith(env('APP_URL', ''), 'https:'),
|
||||
|
||||
// HTTP Access Only
|
||||
// Setting this value to true will prevent JavaScript from accessing the
|
||||
|
||||
@@ -10,10 +10,16 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $deleted_by
|
||||
* @property string $deletable_type
|
||||
* @property int $deletable_id
|
||||
* @property Deletable $deletable
|
||||
*/
|
||||
class Deletion extends Model implements Loggable
|
||||
{
|
||||
protected $hidden = [];
|
||||
|
||||
/**
|
||||
* Get the related deletable record.
|
||||
*/
|
||||
|
||||
@@ -10,19 +10,23 @@ 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 string $editor
|
||||
* @property Chapter $chapter
|
||||
* @property Collection $attachments
|
||||
* @property Collection $revisions
|
||||
* @property PageRevision $currentRevision
|
||||
*/
|
||||
class Page extends BookChild
|
||||
{
|
||||
@@ -82,6 +86,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 +134,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,7 +10,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
/**
|
||||
* Class PageRevision.
|
||||
*
|
||||
* @property mixed $id
|
||||
* @property int $page_id
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property string $book_slug
|
||||
* @property int $created_by
|
||||
@@ -20,13 +22,15 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
* @property string $summary
|
||||
* @property string $markdown
|
||||
* @property string $html
|
||||
* @property string $text
|
||||
* @property int $revision_number
|
||||
* @property Page $page
|
||||
* @property-read ?User $createdBy
|
||||
*/
|
||||
class PageRevision extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
|
||||
protected $fillable = ['name', 'text', 'summary'];
|
||||
protected $hidden = ['html', 'markdown', 'restricted', 'text'];
|
||||
|
||||
/**
|
||||
* Get the user that created the page revision.
|
||||
|
||||
@@ -11,8 +11,8 @@ use Illuminate\Http\UploadedFile;
|
||||
|
||||
class BaseRepo
|
||||
{
|
||||
protected $tagRepo;
|
||||
protected $imageRepo;
|
||||
protected TagRepo $tagRepo;
|
||||
protected ImageRepo $imageRepo;
|
||||
|
||||
public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
|
||||
{
|
||||
@@ -58,6 +58,7 @@ class BaseRepo
|
||||
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($entity, $input['tags']);
|
||||
$entity->touch();
|
||||
}
|
||||
|
||||
$entity->rebuildPermissions();
|
||||
|
||||
@@ -91,6 +91,7 @@ class BookRepo
|
||||
{
|
||||
$book = new Book();
|
||||
$this->baseRepo->create($book, $input);
|
||||
$this->baseRepo->updateCoverImage($book, $input['image'] ?? null);
|
||||
Activity::add(ActivityType::BOOK_CREATE, $book);
|
||||
|
||||
return $book;
|
||||
@@ -102,6 +103,11 @@ class BookRepo
|
||||
public function update(Book $book, array $input): Book
|
||||
{
|
||||
$this->baseRepo->update($book, $input);
|
||||
|
||||
if (array_key_exists('image', $input)) {
|
||||
$this->baseRepo->updateCoverImage($book, $input['image'], $input['image'] === null);
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::BOOK_UPDATE, $book);
|
||||
|
||||
return $book;
|
||||
|
||||
@@ -6,12 +6,10 @@ use BookStack\Actions\ActivityType;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Tools\TrashCan;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Facades\Activity;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class BookshelfRepo
|
||||
@@ -89,6 +87,7 @@ class BookshelfRepo
|
||||
{
|
||||
$shelf = new Bookshelf();
|
||||
$this->baseRepo->create($shelf, $input);
|
||||
$this->baseRepo->updateCoverImage($shelf, $input['image'] ?? null);
|
||||
$this->updateBooks($shelf, $bookIds);
|
||||
Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
|
||||
|
||||
@@ -106,14 +105,17 @@ class BookshelfRepo
|
||||
$this->updateBooks($shelf, $bookIds);
|
||||
}
|
||||
|
||||
if (array_key_exists('image', $input)) {
|
||||
$this->baseRepo->updateCoverImage($shelf, $input['image'], $input['image'] === null);
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::BOOKSHELF_UPDATE, $shelf);
|
||||
|
||||
return $shelf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update which books are assigned to this shelf by
|
||||
* syncing the given book ids.
|
||||
* Update which books are assigned to this shelf by syncing the given book ids.
|
||||
* Function ensures the books are visible to the current user and existing.
|
||||
*/
|
||||
protected function updateBooks(Bookshelf $shelf, array $bookIds)
|
||||
@@ -132,17 +134,6 @@ class BookshelfRepo
|
||||
$shelf->books()->sync($syncData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given shelf cover image, or clear it.
|
||||
*
|
||||
* @throws ImageUploadException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function updateCoverImage(Bookshelf $shelf, ?UploadedFile $coverImage, bool $removeImage = false)
|
||||
{
|
||||
$this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy down the permissions of the given shelf to all child books.
|
||||
*/
|
||||
|
||||
36
app/Entities/Repos/DeletionRepo.php
Normal file
36
app/Entities/Repos/DeletionRepo.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Repos;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Entities\Models\Deletion;
|
||||
use BookStack\Entities\Tools\TrashCan;
|
||||
use BookStack\Facades\Activity;
|
||||
|
||||
class DeletionRepo
|
||||
{
|
||||
private TrashCan $trashCan;
|
||||
|
||||
public function __construct(TrashCan $trashCan)
|
||||
{
|
||||
$this->trashCan = $trashCan;
|
||||
}
|
||||
|
||||
public function restore(int $id): int
|
||||
{
|
||||
/** @var Deletion $deletion */
|
||||
$deletion = Deletion::query()->findOrFail($id);
|
||||
Activity::add(ActivityType::RECYCLE_BIN_RESTORE, $deletion);
|
||||
|
||||
return $this->trashCan->restoreFromDeletion($deletion);
|
||||
}
|
||||
|
||||
public function destroy(int $id): int
|
||||
{
|
||||
/** @var Deletion $deletion */
|
||||
$deletion = Deletion::query()->findOrFail($id);
|
||||
Activity::add(ActivityType::RECYCLE_BIN_DESTROY, $deletion);
|
||||
|
||||
return $this->trashCan->destroyFromDeletion($deletion);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Models\PageRevision;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Entities\Tools\PageEditorData;
|
||||
use BookStack\Entities\Tools\TrashCan;
|
||||
use BookStack\Exceptions\MoveOperationException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
@@ -217,11 +218,25 @@ class PageRepo
|
||||
}
|
||||
|
||||
$pageContent = new PageContent($page);
|
||||
if (!empty($input['markdown'] ?? '')) {
|
||||
$currentEditor = $page->editor ?: PageEditorData::getSystemDefaultEditor();
|
||||
$newEditor = $currentEditor;
|
||||
|
||||
$haveInput = isset($input['markdown']) || isset($input['html']);
|
||||
$inputEmpty = empty($input['markdown']) && empty($input['html']);
|
||||
|
||||
if ($haveInput && $inputEmpty) {
|
||||
$pageContent->setNewHTML('');
|
||||
} elseif (!empty($input['markdown']) && is_string($input['markdown'])) {
|
||||
$newEditor = 'markdown';
|
||||
$pageContent->setNewMarkdown($input['markdown']);
|
||||
} elseif (isset($input['html'])) {
|
||||
$newEditor = 'wysiwyg';
|
||||
$pageContent->setNewHTML($input['html']);
|
||||
}
|
||||
|
||||
if ($newEditor !== $currentEditor && userCan('editor-change')) {
|
||||
$page->editor = $newEditor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,8 +244,12 @@ class PageRepo
|
||||
*/
|
||||
protected function savePageRevision(Page $page, string $summary = null): PageRevision
|
||||
{
|
||||
$revision = new PageRevision($page->getAttributes());
|
||||
$revision = new PageRevision();
|
||||
|
||||
$revision->name = $page->name;
|
||||
$revision->html = $page->html;
|
||||
$revision->markdown = $page->markdown;
|
||||
$revision->text = $page->text;
|
||||
$revision->page_id = $page->id;
|
||||
$revision->slug = $page->slug;
|
||||
$revision->book_slug = $page->book->slug;
|
||||
@@ -260,10 +279,15 @@ class PageRepo
|
||||
return $page;
|
||||
}
|
||||
|
||||
// Otherwise save the data to a revision
|
||||
// Otherwise, save the data to a revision
|
||||
$draft = $this->getPageRevisionToUpdate($page);
|
||||
$draft->fill($input);
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
|
||||
if (!empty($input['markdown'])) {
|
||||
$draft->markdown = $input['markdown'];
|
||||
$draft->html = '';
|
||||
} else {
|
||||
$draft->html = $input['html'];
|
||||
$draft->markdown = '';
|
||||
}
|
||||
|
||||
@@ -368,23 +392,6 @@ class PageRepo
|
||||
return $parentClass::visible()->where('id', '=', $entityId)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the page's parent to the given entity.
|
||||
*/
|
||||
protected function changeParent(Page $page, Entity $parent)
|
||||
{
|
||||
$book = ($parent instanceof Chapter) ? $parent->book : $parent;
|
||||
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : 0;
|
||||
$page->save();
|
||||
|
||||
if ($page->book->id !== $book->id) {
|
||||
$page->changeBook($book->id);
|
||||
}
|
||||
|
||||
$page->load('book');
|
||||
$book->rebuildPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page revision to update for the given page.
|
||||
* Checks for an existing revisions before providing a fresh one.
|
||||
|
||||
@@ -16,25 +16,10 @@ use Illuminate\Http\UploadedFile;
|
||||
|
||||
class Cloner
|
||||
{
|
||||
/**
|
||||
* @var PageRepo
|
||||
*/
|
||||
protected $pageRepo;
|
||||
|
||||
/**
|
||||
* @var ChapterRepo
|
||||
*/
|
||||
protected $chapterRepo;
|
||||
|
||||
/**
|
||||
* @var BookRepo
|
||||
*/
|
||||
protected $bookRepo;
|
||||
|
||||
/**
|
||||
* @var ImageService
|
||||
*/
|
||||
protected $imageService;
|
||||
protected PageRepo $pageRepo;
|
||||
protected ChapterRepo $chapterRepo;
|
||||
protected BookRepo $bookRepo;
|
||||
protected ImageService $imageService;
|
||||
|
||||
public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo, BookRepo $bookRepo, ImageService $imageService)
|
||||
{
|
||||
@@ -50,11 +35,8 @@ class Cloner
|
||||
public function clonePage(Page $original, Entity $parent, string $newName): Page
|
||||
{
|
||||
$copyPage = $this->pageRepo->getNewDraftPage($parent);
|
||||
$pageData = $original->getAttributes();
|
||||
|
||||
// Update name & tags
|
||||
$pageData = $this->entityToInputData($original);
|
||||
$pageData['name'] = $newName;
|
||||
$pageData['tags'] = $this->entityTagsToInputArray($original);
|
||||
|
||||
return $this->pageRepo->publishDraft($copyPage, $pageData);
|
||||
}
|
||||
@@ -65,9 +47,8 @@ class Cloner
|
||||
*/
|
||||
public function cloneChapter(Chapter $original, Book $parent, string $newName): Chapter
|
||||
{
|
||||
$chapterDetails = $original->getAttributes();
|
||||
$chapterDetails = $this->entityToInputData($original);
|
||||
$chapterDetails['name'] = $newName;
|
||||
$chapterDetails['tags'] = $this->entityTagsToInputArray($original);
|
||||
|
||||
$copyChapter = $this->chapterRepo->create($chapterDetails, $parent);
|
||||
|
||||
@@ -87,9 +68,8 @@ class Cloner
|
||||
*/
|
||||
public function cloneBook(Book $original, string $newName): Book
|
||||
{
|
||||
$bookDetails = $original->getAttributes();
|
||||
$bookDetails = $this->entityToInputData($original);
|
||||
$bookDetails['name'] = $newName;
|
||||
$bookDetails['tags'] = $this->entityTagsToInputArray($original);
|
||||
|
||||
$copyBook = $this->bookRepo->create($bookDetails);
|
||||
|
||||
@@ -104,26 +84,48 @@ class Cloner
|
||||
}
|
||||
}
|
||||
|
||||
if ($original->cover) {
|
||||
try {
|
||||
$tmpImgFile = tmpfile();
|
||||
$uploadedFile = $this->imageToUploadedFile($original->cover, $tmpImgFile);
|
||||
$this->bookRepo->updateCoverImage($copyBook, $uploadedFile, false);
|
||||
} catch (\Exception $exception) {
|
||||
}
|
||||
return $copyBook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an entity to a raw data array of input data.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function entityToInputData(Entity $entity): array
|
||||
{
|
||||
$inputData = $entity->getAttributes();
|
||||
$inputData['tags'] = $this->entityTagsToInputArray($entity);
|
||||
|
||||
// Add a cover to the data if existing on the original entity
|
||||
if ($entity->cover instanceof Image) {
|
||||
$uploadedFile = $this->imageToUploadedFile($entity->cover);
|
||||
$inputData['image'] = $uploadedFile;
|
||||
}
|
||||
|
||||
return $copyBook;
|
||||
return $inputData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the permission settings from the source entity to the target entity.
|
||||
*/
|
||||
public function copyEntityPermissions(Entity $sourceEntity, Entity $targetEntity): void
|
||||
{
|
||||
$targetEntity->restricted = $sourceEntity->restricted;
|
||||
$permissions = $sourceEntity->permissions()->get(['role_id', 'action'])->toArray();
|
||||
$targetEntity->permissions()->delete();
|
||||
$targetEntity->permissions()->createMany($permissions);
|
||||
$targetEntity->rebuildPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an image instance to an UploadedFile instance to mimic
|
||||
* a file being uploaded.
|
||||
*/
|
||||
protected function imageToUploadedFile(Image $image, &$tmpFile): ?UploadedFile
|
||||
protected function imageToUploadedFile(Image $image): ?UploadedFile
|
||||
{
|
||||
$imgData = $this->imageService->getImageData($image);
|
||||
$tmpImgFilePath = stream_get_meta_data($tmpFile)['uri'];
|
||||
$tmpImgFilePath = tempnam(sys_get_temp_dir(), 'bs_cover_clone_');
|
||||
file_put_contents($tmpImgFilePath, $imgData);
|
||||
|
||||
return new UploadedFile($tmpImgFilePath, basename($image->path));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,9 +39,10 @@ class ExportFormatter
|
||||
public function pageToContainedHtml(Page $page)
|
||||
{
|
||||
$page->html = (new PageContent($page))->render();
|
||||
$pageHtml = view('pages.export', [
|
||||
'page' => $page,
|
||||
'format' => 'html',
|
||||
$pageHtml = view('exports.page', [
|
||||
'page' => $page,
|
||||
'format' => 'html',
|
||||
'cspContent' => $this->cspService->getCspMetaTagValue(),
|
||||
])->render();
|
||||
|
||||
return $this->containHtml($pageHtml);
|
||||
@@ -55,10 +59,11 @@ class ExportFormatter
|
||||
$pages->each(function ($page) {
|
||||
$page->html = (new PageContent($page))->render();
|
||||
});
|
||||
$html = view('chapters.export', [
|
||||
'chapter' => $chapter,
|
||||
'pages' => $pages,
|
||||
'format' => 'html',
|
||||
$html = view('exports.chapter', [
|
||||
'chapter' => $chapter,
|
||||
'pages' => $pages,
|
||||
'format' => 'html',
|
||||
'cspContent' => $this->cspService->getCspMetaTagValue(),
|
||||
])->render();
|
||||
|
||||
return $this->containHtml($html);
|
||||
@@ -72,10 +77,11 @@ class ExportFormatter
|
||||
public function bookToContainedHtml(Book $book)
|
||||
{
|
||||
$bookTree = (new BookContents($book))->getTree(false, true);
|
||||
$html = view('books.export', [
|
||||
$html = view('exports.book', [
|
||||
'book' => $book,
|
||||
'bookChildren' => $bookTree,
|
||||
'format' => 'html',
|
||||
'cspContent' => $this->cspService->getCspMetaTagValue(),
|
||||
])->render();
|
||||
|
||||
return $this->containHtml($html);
|
||||
@@ -89,7 +95,7 @@ class ExportFormatter
|
||||
public function pageToPdf(Page $page)
|
||||
{
|
||||
$page->html = (new PageContent($page))->render();
|
||||
$html = view('pages.export', [
|
||||
$html = view('exports.page', [
|
||||
'page' => $page,
|
||||
'format' => 'pdf',
|
||||
'engine' => $this->pdfGenerator->getActiveEngine(),
|
||||
@@ -110,7 +116,7 @@ class ExportFormatter
|
||||
$page->html = (new PageContent($page))->render();
|
||||
});
|
||||
|
||||
$html = view('chapters.export', [
|
||||
$html = view('exports.chapter', [
|
||||
'chapter' => $chapter,
|
||||
'pages' => $pages,
|
||||
'format' => 'pdf',
|
||||
@@ -128,7 +134,7 @@ class ExportFormatter
|
||||
public function bookToPdf(Book $book)
|
||||
{
|
||||
$bookTree = (new BookContents($book))->getTree(false, true);
|
||||
$html = view('books.export', [
|
||||
$html = view('exports.book', [
|
||||
'book' => $book,
|
||||
'bookChildren' => $bookTree,
|
||||
'format' => 'pdf',
|
||||
@@ -320,7 +326,7 @@ class ExportFormatter
|
||||
$text .= $this->pageToMarkdown($page) . "\n\n";
|
||||
}
|
||||
|
||||
return $text;
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,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);
|
||||
}
|
||||
}
|
||||
|
||||
87
app/Entities/Tools/HierarchyTransformer.php
Normal file
87
app/Entities/Tools/HierarchyTransformer.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\BookRepo;
|
||||
use BookStack\Entities\Repos\BookshelfRepo;
|
||||
use BookStack\Facades\Activity;
|
||||
|
||||
class HierarchyTransformer
|
||||
{
|
||||
protected BookRepo $bookRepo;
|
||||
protected BookshelfRepo $shelfRepo;
|
||||
protected Cloner $cloner;
|
||||
protected TrashCan $trashCan;
|
||||
|
||||
public function __construct(BookRepo $bookRepo, BookshelfRepo $shelfRepo, Cloner $cloner, TrashCan $trashCan)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->shelfRepo = $shelfRepo;
|
||||
$this->cloner = $cloner;
|
||||
$this->trashCan = $trashCan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a chapter into a book.
|
||||
* Does not check permissions, check before calling.
|
||||
*/
|
||||
public function transformChapterToBook(Chapter $chapter): Book
|
||||
{
|
||||
$inputData = $this->cloner->entityToInputData($chapter);
|
||||
$book = $this->bookRepo->create($inputData);
|
||||
$this->cloner->copyEntityPermissions($chapter, $book);
|
||||
|
||||
/** @var Page $page */
|
||||
foreach ($chapter->pages as $page) {
|
||||
$page->chapter_id = 0;
|
||||
$page->changeBook($book->id);
|
||||
}
|
||||
|
||||
$this->trashCan->destroyEntity($chapter);
|
||||
|
||||
Activity::add(ActivityType::BOOK_CREATE_FROM_CHAPTER, $book);
|
||||
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a book into a shelf.
|
||||
* Does not check permissions, check before calling.
|
||||
*/
|
||||
public function transformBookToShelf(Book $book): Bookshelf
|
||||
{
|
||||
$inputData = $this->cloner->entityToInputData($book);
|
||||
$shelf = $this->shelfRepo->create($inputData, []);
|
||||
$this->cloner->copyEntityPermissions($book, $shelf);
|
||||
|
||||
$shelfBookSyncData = [];
|
||||
|
||||
/** @var Chapter $chapter */
|
||||
foreach ($book->chapters as $index => $chapter) {
|
||||
$newBook = $this->transformChapterToBook($chapter);
|
||||
$shelfBookSyncData[$newBook->id] = ['order' => $index];
|
||||
if (!$newBook->restricted) {
|
||||
$this->cloner->copyEntityPermissions($shelf, $newBook);
|
||||
}
|
||||
}
|
||||
|
||||
if ($book->directPages->count() > 0) {
|
||||
$book->name .= ' ' . trans('entities.pages');
|
||||
$shelfBookSyncData[$book->id] = ['order' => count($shelfBookSyncData) + 1];
|
||||
$book->save();
|
||||
} else {
|
||||
$this->trashCan->destroyEntity($book);
|
||||
}
|
||||
|
||||
$shelf->books()->sync($shelfBookSyncData);
|
||||
|
||||
Activity::add(ActivityType::BOOKSHELF_CREATE_FROM_BOOK, $shelf);
|
||||
|
||||
return $shelf;
|
||||
}
|
||||
}
|
||||
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'];
|
||||
}
|
||||
}
|
||||
20
app/Entities/Tools/Markdown/CustomDivConverter.php
Normal file
20
app/Entities/Tools/Markdown/CustomDivConverter.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools\Markdown;
|
||||
|
||||
use League\HTMLToMarkdown\Converter\DivConverter;
|
||||
use League\HTMLToMarkdown\ElementInterface;
|
||||
|
||||
class CustomDivConverter extends DivConverter
|
||||
{
|
||||
public function convert(ElementInterface $element): string
|
||||
{
|
||||
// Clean up draw.io diagrams
|
||||
$drawIoDiagram = $element->getAttribute('drawio-diagram');
|
||||
if ($drawIoDiagram) {
|
||||
return "<div drawio-diagram=\"{$drawIoDiagram}\">{$element->getValue()}</div>\n\n";
|
||||
}
|
||||
|
||||
return parent::convert($element);
|
||||
}
|
||||
}
|
||||
25
app/Entities/Tools/Markdown/CustomImageConverter.php
Normal file
25
app/Entities/Tools/Markdown/CustomImageConverter.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools\Markdown;
|
||||
|
||||
use League\HTMLToMarkdown\Converter\ImageConverter;
|
||||
use League\HTMLToMarkdown\ElementInterface;
|
||||
|
||||
class CustomImageConverter extends ImageConverter
|
||||
{
|
||||
public function convert(ElementInterface $element): string
|
||||
{
|
||||
$parent = $element->getParent();
|
||||
|
||||
// Remain as HTML if within diagram block.
|
||||
$withinDrawing = $parent && !empty($parent->getAttribute('drawio-diagram'));
|
||||
if ($withinDrawing) {
|
||||
$src = e($element->getAttribute('src'));
|
||||
$alt = e($element->getAttribute('alt'));
|
||||
|
||||
return "<img src=\"{$src}\" alt=\"{$alt}\"/>";
|
||||
}
|
||||
|
||||
return parent::convert($element);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ class CustomParagraphConverter extends ParagraphConverter
|
||||
{
|
||||
public function convert(ElementInterface $element): string
|
||||
{
|
||||
$class = $element->getAttribute('class');
|
||||
$class = e($element->getAttribute('class'));
|
||||
if (strpos($class, 'callout') !== false) {
|
||||
return "<{$element->getTagName()} class=\"{$class}\">{$element->getValue()}</{$element->getTagName()}>\n\n";
|
||||
}
|
||||
|
||||
@@ -5,12 +5,10 @@ namespace BookStack\Entities\Tools\Markdown;
|
||||
use League\HTMLToMarkdown\Converter\BlockquoteConverter;
|
||||
use League\HTMLToMarkdown\Converter\CodeConverter;
|
||||
use League\HTMLToMarkdown\Converter\CommentConverter;
|
||||
use League\HTMLToMarkdown\Converter\DivConverter;
|
||||
use League\HTMLToMarkdown\Converter\EmphasisConverter;
|
||||
use League\HTMLToMarkdown\Converter\HardBreakConverter;
|
||||
use League\HTMLToMarkdown\Converter\HeaderConverter;
|
||||
use League\HTMLToMarkdown\Converter\HorizontalRuleConverter;
|
||||
use League\HTMLToMarkdown\Converter\ImageConverter;
|
||||
use League\HTMLToMarkdown\Converter\LinkConverter;
|
||||
use League\HTMLToMarkdown\Converter\ListBlockConverter;
|
||||
use League\HTMLToMarkdown\Converter\ListItemConverter;
|
||||
@@ -21,7 +19,7 @@ use League\HTMLToMarkdown\HtmlConverter;
|
||||
|
||||
class HtmlToMarkdown
|
||||
{
|
||||
protected $html;
|
||||
protected string $html;
|
||||
|
||||
public function __construct(string $html)
|
||||
{
|
||||
@@ -75,18 +73,20 @@ class HtmlToMarkdown
|
||||
$environment->addConverter(new BlockquoteConverter());
|
||||
$environment->addConverter(new CodeConverter());
|
||||
$environment->addConverter(new CommentConverter());
|
||||
$environment->addConverter(new DivConverter());
|
||||
$environment->addConverter(new CustomDivConverter());
|
||||
$environment->addConverter(new EmphasisConverter());
|
||||
$environment->addConverter(new HardBreakConverter());
|
||||
$environment->addConverter(new HeaderConverter());
|
||||
$environment->addConverter(new HorizontalRuleConverter());
|
||||
$environment->addConverter(new ImageConverter());
|
||||
$environment->addConverter(new CustomImageConverter());
|
||||
$environment->addConverter(new LinkConverter());
|
||||
$environment->addConverter(new ListBlockConverter());
|
||||
$environment->addConverter(new ListItemConverter());
|
||||
$environment->addConverter(new CustomParagraphConverter());
|
||||
$environment->addConverter(new PreformattedConverter());
|
||||
$environment->addConverter(new TextConverter());
|
||||
$environment->addConverter(new CheckboxConverter());
|
||||
$environment->addConverter(new SpacedTagFallbackConverter());
|
||||
|
||||
return $environment;
|
||||
}
|
||||
|
||||
35
app/Entities/Tools/Markdown/MarkdownToHtml.php
Normal file
35
app/Entities/Tools/Markdown/MarkdownToHtml.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools\Markdown;
|
||||
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use League\CommonMark\Block\Element\ListItem;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Environment;
|
||||
use League\CommonMark\Extension\Table\TableExtension;
|
||||
use League\CommonMark\Extension\TaskList\TaskListExtension;
|
||||
|
||||
class MarkdownToHtml
|
||||
{
|
||||
protected string $markdown;
|
||||
|
||||
public function __construct(string $markdown)
|
||||
{
|
||||
$this->markdown = $markdown;
|
||||
}
|
||||
|
||||
public function convert(): string
|
||||
{
|
||||
$environment = Environment::createCommonMarkEnvironment();
|
||||
$environment->addExtension(new TableExtension());
|
||||
$environment->addExtension(new TaskListExtension());
|
||||
$environment->addExtension(new CustomStrikeThroughExtension());
|
||||
$environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
|
||||
$converter = new CommonMarkConverter([], $environment);
|
||||
|
||||
$environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10);
|
||||
|
||||
return $converter->convertToHtml($this->markdown);
|
||||
}
|
||||
}
|
||||
23
app/Entities/Tools/Markdown/SpacedTagFallbackConverter.php
Normal file
23
app/Entities/Tools/Markdown/SpacedTagFallbackConverter.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools\Markdown;
|
||||
|
||||
use League\HTMLToMarkdown\Converter\ConverterInterface;
|
||||
use League\HTMLToMarkdown\ElementInterface;
|
||||
|
||||
/**
|
||||
* For certain defined tags, add additional spacing upon the retained HTML content
|
||||
* to separate it out from anything that may be markdown soon afterwards or within.
|
||||
*/
|
||||
class SpacedTagFallbackConverter implements ConverterInterface
|
||||
{
|
||||
public function convert(ElementInterface $element): string
|
||||
{
|
||||
return \html_entity_decode($element->getChildrenAsString()) . "\n\n";
|
||||
}
|
||||
|
||||
public function getSupportedTags(): array
|
||||
{
|
||||
return ['summary', 'iframe'];
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,8 @@
|
||||
namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Tools\Markdown\CustomListItemRenderer;
|
||||
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
|
||||
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use BookStack\Util\HtmlContentFilter;
|
||||
@@ -17,15 +14,10 @@ use DOMNode;
|
||||
use DOMNodeList;
|
||||
use DOMXPath;
|
||||
use Illuminate\Support\Str;
|
||||
use League\CommonMark\Block\Element\ListItem;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Environment;
|
||||
use League\CommonMark\Extension\Table\TableExtension;
|
||||
use League\CommonMark\Extension\TaskList\TaskListExtension;
|
||||
|
||||
class PageContent
|
||||
{
|
||||
protected $page;
|
||||
protected Page $page;
|
||||
|
||||
/**
|
||||
* PageContent constructor.
|
||||
@@ -53,28 +45,11 @@ class PageContent
|
||||
{
|
||||
$markdown = $this->extractBase64ImagesFromMarkdown($markdown);
|
||||
$this->page->markdown = $markdown;
|
||||
$html = $this->markdownToHtml($markdown);
|
||||
$html = (new MarkdownToHtml($markdown))->convert();
|
||||
$this->page->html = $this->formatHtml($html);
|
||||
$this->page->text = $this->toPlainText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given Markdown content to a HTML string.
|
||||
*/
|
||||
protected function markdownToHtml(string $markdown): string
|
||||
{
|
||||
$environment = Environment::createCommonMarkEnvironment();
|
||||
$environment->addExtension(new TableExtension());
|
||||
$environment->addExtension(new TaskListExtension());
|
||||
$environment->addExtension(new CustomStrikeThroughExtension());
|
||||
$environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
|
||||
$converter = new CommonMarkConverter([], $environment);
|
||||
|
||||
$environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10);
|
||||
|
||||
return $converter->convertToHtml($markdown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all base64 image data to saved images.
|
||||
*/
|
||||
@@ -239,6 +214,9 @@ class PageContent
|
||||
$html .= $doc->saveHTML($childNode);
|
||||
}
|
||||
|
||||
// Perform required string-level tweaks
|
||||
$html = str_replace(' ', ' ', $html);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class PageEditActivity
|
||||
{
|
||||
protected $page;
|
||||
protected Page $page;
|
||||
|
||||
/**
|
||||
* PageEditActivity constructor.
|
||||
|
||||
115
app/Entities/Tools/PageEditorData.php
Normal file
115
app/Entities/Tools/PageEditorData.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\PageRepo;
|
||||
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
|
||||
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
|
||||
|
||||
class PageEditorData
|
||||
{
|
||||
protected Page $page;
|
||||
protected PageRepo $pageRepo;
|
||||
protected string $requestedEditor;
|
||||
|
||||
protected array $viewData;
|
||||
protected array $warnings;
|
||||
|
||||
public function __construct(Page $page, PageRepo $pageRepo, string $requestedEditor)
|
||||
{
|
||||
$this->page = $page;
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->requestedEditor = $requestedEditor;
|
||||
|
||||
$this->viewData = $this->build();
|
||||
}
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
return $this->viewData;
|
||||
}
|
||||
|
||||
public function getWarnings(): array
|
||||
{
|
||||
return $this->warnings;
|
||||
}
|
||||
|
||||
protected function build(): array
|
||||
{
|
||||
$page = clone $this->page;
|
||||
$isDraft = boolval($this->page->draft);
|
||||
$templates = $this->pageRepo->getTemplates(10);
|
||||
$draftsEnabled = auth()->check();
|
||||
|
||||
$isDraftRevision = false;
|
||||
$this->warnings = [];
|
||||
$editActivity = new PageEditActivity($page);
|
||||
|
||||
if ($editActivity->hasActiveEditing()) {
|
||||
$this->warnings[] = $editActivity->activeEditingMessage();
|
||||
}
|
||||
|
||||
// Check for a current draft version for this user
|
||||
$userDraft = $this->pageRepo->getUserDraft($page);
|
||||
if ($userDraft !== null) {
|
||||
$page->forceFill($userDraft->only(['name', 'html', 'markdown']));
|
||||
$isDraftRevision = true;
|
||||
$this->warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
|
||||
}
|
||||
|
||||
$editorType = $this->getEditorType($page);
|
||||
$this->updateContentForEditor($page, $editorType);
|
||||
|
||||
return [
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'isDraft' => $isDraft,
|
||||
'isDraftRevision' => $isDraftRevision,
|
||||
'draftsEnabled' => $draftsEnabled,
|
||||
'templates' => $templates,
|
||||
'editor' => $editorType,
|
||||
];
|
||||
}
|
||||
|
||||
protected function updateContentForEditor(Page $page, string $editorType): void
|
||||
{
|
||||
$isHtml = !empty($page->html) && empty($page->markdown);
|
||||
|
||||
// HTML to markdown-clean conversion
|
||||
if ($editorType === 'markdown' && $isHtml && $this->requestedEditor === 'markdown-clean') {
|
||||
$page->markdown = (new HtmlToMarkdown($page->html))->convert();
|
||||
}
|
||||
|
||||
// Markdown to HTML conversion if we don't have HTML
|
||||
if ($editorType === 'wysiwyg' && !$isHtml) {
|
||||
$page->html = (new MarkdownToHtml($page->markdown))->convert();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of editor to show for editing the given page.
|
||||
* Defaults based upon the current content of the page otherwise will fall back
|
||||
* to system default but will take a requested type (if provided) if permissions allow.
|
||||
*/
|
||||
protected function getEditorType(Page $page): string
|
||||
{
|
||||
$editorType = $page->editor ?: self::getSystemDefaultEditor();
|
||||
|
||||
// Use requested editor if valid and if we have permission
|
||||
$requestedType = explode('-', $this->requestedEditor)[0];
|
||||
if (($requestedType === 'markdown' || $requestedType === 'wysiwyg') && userCan('editor-change')) {
|
||||
$editorType = $requestedType;
|
||||
}
|
||||
|
||||
return $editorType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured system default editor.
|
||||
*/
|
||||
public static function getSystemDefaultEditor(): string
|
||||
{
|
||||
return setting('app-editor') === 'markdown' ? 'markdown' : 'wysiwyg';
|
||||
}
|
||||
}
|
||||
@@ -147,6 +147,8 @@ class SearchIndex
|
||||
];
|
||||
|
||||
$html = '<body>' . $html . '</body>';
|
||||
$html = str_ireplace(['<br>', '<br />', '<br/>'], "\n", $html);
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
@@ -360,7 +360,7 @@ class SearchRunner
|
||||
/** @var Connection $connection */
|
||||
$connection = $query->getConnection();
|
||||
$tagValue = (float) trim($connection->getPdo()->quote($tagValue), "'");
|
||||
$query->whereRaw("value ${tagOperator} ${tagValue}");
|
||||
$query->whereRaw("value {$tagOperator} {$tagValue}");
|
||||
} else {
|
||||
$query->where('value', $tagOperator, $tagValue);
|
||||
}
|
||||
|
||||
@@ -344,7 +344,7 @@ class TrashCan
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function destroyEntity(Entity $entity): int
|
||||
public function destroyEntity(Entity $entity): int
|
||||
{
|
||||
if ($entity instanceof Page) {
|
||||
return $this->destroyPage($entity);
|
||||
|
||||
@@ -21,6 +21,7 @@ class Handler extends ExceptionHandler
|
||||
*/
|
||||
protected $dontReport = [
|
||||
NotFoundException::class,
|
||||
StoppedAuthenticationException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,10 +19,13 @@ class JsonDebugException extends Exception
|
||||
}
|
||||
|
||||
/**
|
||||
* Covert this exception into a response.
|
||||
* Convert this exception into a response.
|
||||
* We add a manual data conversion to UTF8 to ensure any binary data is presentable as a JSON string.
|
||||
*/
|
||||
public function render(): JsonResponse
|
||||
{
|
||||
return response()->json($this->data);
|
||||
$cleaned = mb_convert_encoding($this->data, 'UTF-8');
|
||||
|
||||
return response()->json($cleaned);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,14 +87,33 @@ class AttachmentApiController extends ApiController
|
||||
'markdown' => $attachment->markdownLink(),
|
||||
]);
|
||||
|
||||
if (!$attachment->external) {
|
||||
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
|
||||
$attachment->setAttribute('content', base64_encode($attachmentContents));
|
||||
} else {
|
||||
// Simply return a JSON response of the attachment for link-based attachments
|
||||
if ($attachment->external) {
|
||||
$attachment->setAttribute('content', $attachment->path);
|
||||
|
||||
return response()->json($attachment);
|
||||
}
|
||||
|
||||
return response()->json($attachment);
|
||||
// Build and split our core JSON, at point of content.
|
||||
$splitter = 'CONTENT_SPLIT_LOCATION_' . time() . '_' . rand(1, 40000);
|
||||
$attachment->setAttribute('content', $splitter);
|
||||
$json = $attachment->toJson();
|
||||
$jsonParts = explode($splitter, $json);
|
||||
// Get a stream for the file data from storage
|
||||
$stream = $this->attachmentService->streamAttachmentFromStorage($attachment);
|
||||
|
||||
return response()->stream(function () use ($jsonParts, $stream) {
|
||||
// Output the pre-content JSON data
|
||||
echo $jsonParts[0];
|
||||
|
||||
// Stream out our attachment data as base64 content
|
||||
stream_filter_append($stream, 'convert.base64-encode', STREAM_FILTER_READ);
|
||||
fpassthru($stream);
|
||||
fclose($stream);
|
||||
|
||||
// Output our post-content JSON data
|
||||
echo $jsonParts[1];
|
||||
}, 200, ['Content-Type' => 'application/json']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,19 +11,6 @@ class BookApiController extends ApiController
|
||||
{
|
||||
protected $bookRepo;
|
||||
|
||||
protected $rules = [
|
||||
'create' => [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'tags' => ['array'],
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'tags' => ['array'],
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(BookRepo $bookRepo)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
@@ -37,19 +24,21 @@ class BookApiController extends ApiController
|
||||
$books = Book::visible();
|
||||
|
||||
return $this->apiListingResponse($books, [
|
||||
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', 'image_id',
|
||||
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new book in the system.
|
||||
* The cover image of a book can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
|
||||
* If the 'image' property is null then the book cover image will be removed.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$this->checkPermission('book-create-all');
|
||||
$requestData = $this->validate($request, $this->rules['create']);
|
||||
$requestData = $this->validate($request, $this->rules()['create']);
|
||||
|
||||
$book = $this->bookRepo->create($requestData);
|
||||
|
||||
@@ -68,6 +57,8 @@ class BookApiController extends ApiController
|
||||
|
||||
/**
|
||||
* Update the details of a single book.
|
||||
* The cover image of a book can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
|
||||
* If the 'image' property is null then the book cover image will be removed.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
@@ -76,7 +67,7 @@ class BookApiController extends ApiController
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
|
||||
$requestData = $this->validate($request, $this->rules['update']);
|
||||
$requestData = $this->validate($request, $this->rules()['update']);
|
||||
$book = $this->bookRepo->update($book, $requestData);
|
||||
|
||||
return response()->json($book);
|
||||
@@ -97,4 +88,22 @@ class BookApiController extends ApiController
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'create' => [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'tags' => ['array'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'tags' => ['array'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class BookExportApiController extends ApiController
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$pdfContent = $this->exportFormatter->bookToPdf($book);
|
||||
|
||||
return $this->downloadResponse($pdfContent, $book->slug . '.pdf');
|
||||
return $this->download()->directly($pdfContent, $book->slug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +39,7 @@ class BookExportApiController extends ApiController
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
|
||||
|
||||
return $this->downloadResponse($htmlContent, $book->slug . '.html');
|
||||
return $this->download()->directly($htmlContent, $book->slug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +50,7 @@ class BookExportApiController extends ApiController
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$textContent = $this->exportFormatter->bookToPlainText($book);
|
||||
|
||||
return $this->downloadResponse($textContent, $book->slug . '.txt');
|
||||
return $this->download()->directly($textContent, $book->slug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +61,6 @@ class BookExportApiController extends ApiController
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$markdown = $this->exportFormatter->bookToMarkdown($book);
|
||||
|
||||
return $this->downloadResponse($markdown, $book->slug . '.md');
|
||||
return $this->download()->directly($markdown, $book->slug . '.md');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,23 +11,7 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
class BookshelfApiController extends ApiController
|
||||
{
|
||||
/**
|
||||
* @var BookshelfRepo
|
||||
*/
|
||||
protected $bookshelfRepo;
|
||||
|
||||
protected $rules = [
|
||||
'create' => [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'books' => ['array'],
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'books' => ['array'],
|
||||
],
|
||||
];
|
||||
protected BookshelfRepo $bookshelfRepo;
|
||||
|
||||
/**
|
||||
* BookshelfApiController constructor.
|
||||
@@ -45,7 +29,7 @@ class BookshelfApiController extends ApiController
|
||||
$shelves = Bookshelf::visible();
|
||||
|
||||
return $this->apiListingResponse($shelves, [
|
||||
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', 'image_id',
|
||||
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -53,13 +37,15 @@ class BookshelfApiController extends ApiController
|
||||
* Create a new shelf in the system.
|
||||
* An array of books IDs can be provided in the request. These
|
||||
* will be added to the shelf in the same order as provided.
|
||||
* The cover image of a shelf can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
|
||||
* If the 'image' property is null then the shelf cover image will be removed.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$requestData = $this->validate($request, $this->rules['create']);
|
||||
$requestData = $this->validate($request, $this->rules()['create']);
|
||||
|
||||
$bookIds = $request->get('books', []);
|
||||
$shelf = $this->bookshelfRepo->create($requestData, $bookIds);
|
||||
@@ -87,6 +73,8 @@ class BookshelfApiController extends ApiController
|
||||
* An array of books IDs can be provided in the request. These
|
||||
* will be added to the shelf in the same order as provided and overwrite
|
||||
* any existing book assignments.
|
||||
* The cover image of a shelf can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
|
||||
* If the 'image' property is null then the shelf cover image will be removed.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
@@ -95,7 +83,7 @@ class BookshelfApiController extends ApiController
|
||||
$shelf = Bookshelf::visible()->findOrFail($id);
|
||||
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||
|
||||
$requestData = $this->validate($request, $this->rules['update']);
|
||||
$requestData = $this->validate($request, $this->rules()['update']);
|
||||
$bookIds = $request->get('books', null);
|
||||
|
||||
$shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
|
||||
@@ -118,4 +106,24 @@ class BookshelfApiController extends ApiController
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'create' => [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'books' => ['array'],
|
||||
'tags' => ['array'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'books' => ['array'],
|
||||
'tags' => ['array'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class ChapterExportApiController extends ApiController
|
||||
$chapter = Chapter::visible()->findOrFail($id);
|
||||
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
|
||||
|
||||
return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf');
|
||||
return $this->download()->directly($pdfContent, $chapter->slug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +42,7 @@ class ChapterExportApiController extends ApiController
|
||||
$chapter = Chapter::visible()->findOrFail($id);
|
||||
$htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
|
||||
|
||||
return $this->downloadResponse($htmlContent, $chapter->slug . '.html');
|
||||
return $this->download()->directly($htmlContent, $chapter->slug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +53,7 @@ class ChapterExportApiController extends ApiController
|
||||
$chapter = Chapter::visible()->findOrFail($id);
|
||||
$textContent = $this->exportFormatter->chapterToPlainText($chapter);
|
||||
|
||||
return $this->downloadResponse($textContent, $chapter->slug . '.txt');
|
||||
return $this->download()->directly($textContent, $chapter->slug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,6 +64,6 @@ class ChapterExportApiController extends ApiController
|
||||
$chapter = Chapter::visible()->findOrFail($id);
|
||||
$markdown = $this->exportFormatter->chapterToMarkdown($chapter);
|
||||
|
||||
return $this->downloadResponse($markdown, $chapter->slug . '.md');
|
||||
return $this->download()->directly($markdown, $chapter->slug . '.md');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use Illuminate\Http\Request;
|
||||
|
||||
class PageApiController extends ApiController
|
||||
{
|
||||
protected $pageRepo;
|
||||
protected PageRepo $pageRepo;
|
||||
|
||||
protected $rules = [
|
||||
'create' => [
|
||||
@@ -24,8 +24,8 @@ class PageApiController extends ApiController
|
||||
'tags' => ['array'],
|
||||
],
|
||||
'update' => [
|
||||
'book_id' => ['required', 'integer'],
|
||||
'chapter_id' => ['required', 'integer'],
|
||||
'book_id' => ['integer'],
|
||||
'chapter_id' => ['integer'],
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'html' => ['string'],
|
||||
'markdown' => ['string'],
|
||||
@@ -103,6 +103,8 @@ class PageApiController extends ApiController
|
||||
*/
|
||||
public function update(Request $request, string $id)
|
||||
{
|
||||
$requestData = $this->validate($request, $this->rules['update']);
|
||||
|
||||
$page = $this->pageRepo->getById($id, []);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
@@ -127,7 +129,7 @@ class PageApiController extends ApiController
|
||||
}
|
||||
}
|
||||
|
||||
$updatedPage = $this->pageRepo->update($page, $request->all());
|
||||
$updatedPage = $this->pageRepo->update($page, $requestData);
|
||||
|
||||
return response()->json($updatedPage->forJsonDisplay());
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class PageExportApiController extends ApiController
|
||||
$page = Page::visible()->findOrFail($id);
|
||||
$pdfContent = $this->exportFormatter->pageToPdf($page);
|
||||
|
||||
return $this->downloadResponse($pdfContent, $page->slug . '.pdf');
|
||||
return $this->download()->directly($pdfContent, $page->slug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +39,7 @@ class PageExportApiController extends ApiController
|
||||
$page = Page::visible()->findOrFail($id);
|
||||
$htmlContent = $this->exportFormatter->pageToContainedHtml($page);
|
||||
|
||||
return $this->downloadResponse($htmlContent, $page->slug . '.html');
|
||||
return $this->download()->directly($htmlContent, $page->slug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +50,7 @@ class PageExportApiController extends ApiController
|
||||
$page = Page::visible()->findOrFail($id);
|
||||
$textContent = $this->exportFormatter->pageToPlainText($page);
|
||||
|
||||
return $this->downloadResponse($textContent, $page->slug . '.txt');
|
||||
return $this->download()->directly($textContent, $page->slug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +61,6 @@ class PageExportApiController extends ApiController
|
||||
$page = Page::visible()->findOrFail($id);
|
||||
$markdown = $this->exportFormatter->pageToMarkdown($page);
|
||||
|
||||
return $this->downloadResponse($markdown, $page->slug . '.md');
|
||||
return $this->download()->directly($markdown, $page->slug . '.md');
|
||||
}
|
||||
}
|
||||
|
||||
90
app/Http/Controllers/Api/RecycleBinApiController.php
Normal file
90
app/Http/Controllers/Api/RecycleBinApiController.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Api;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Deletion;
|
||||
use BookStack\Entities\Repos\DeletionRepo;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class RecycleBinApiController extends ApiController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware(function ($request, $next) {
|
||||
$this->checkPermission('settings-manage');
|
||||
$this->checkPermission('restrictions-manage-all');
|
||||
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a top-level listing of the items in the recycle bin.
|
||||
* The "deletable" property will reflect the main item deleted.
|
||||
* For books and chapters, counts of child pages/chapters will
|
||||
* be loaded within this "deletable" data.
|
||||
* For chapters & pages, the parent item will be loaded within this "deletable" data.
|
||||
* Requires permission to manage both system settings and permissions.
|
||||
*/
|
||||
public function list()
|
||||
{
|
||||
return $this->apiListingResponse(Deletion::query()->with('deletable'), [
|
||||
'id',
|
||||
'deleted_by',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deletable_type',
|
||||
'deletable_id',
|
||||
], [Closure::fromCallable([$this, 'listFormatter'])]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a single deletion from the recycle bin.
|
||||
* Requires permission to manage both system settings and permissions.
|
||||
*/
|
||||
public function restore(DeletionRepo $deletionRepo, string $deletionId)
|
||||
{
|
||||
$restoreCount = $deletionRepo->restore(intval($deletionId));
|
||||
|
||||
return response()->json(['restore_count' => $restoreCount]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a single deletion from the recycle bin.
|
||||
* Use this endpoint carefully as it will entirely remove the underlying deleted items from the system.
|
||||
* Requires permission to manage both system settings and permissions.
|
||||
*/
|
||||
public function destroy(DeletionRepo $deletionRepo, string $deletionId)
|
||||
{
|
||||
$deleteCount = $deletionRepo->destroy(intval($deletionId));
|
||||
|
||||
return response()->json(['delete_count' => $deleteCount]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load some related details for the deletion listing.
|
||||
*/
|
||||
protected function listFormatter(Deletion $deletion)
|
||||
{
|
||||
$deletable = $deletion->deletable;
|
||||
$withTrashedQuery = fn (Builder $query) => $query->withTrashed();
|
||||
|
||||
if ($deletable instanceof BookChild) {
|
||||
$parent = $deletable->getParent();
|
||||
$parent->setAttribute('type', $parent->getType());
|
||||
$deletable->setRelation('parent', $parent);
|
||||
}
|
||||
|
||||
if ($deletable instanceof Book || $deletable instanceof Chapter) {
|
||||
$countsToLoad = ['pages' => $withTrashedQuery];
|
||||
if ($deletable instanceof Book) {
|
||||
$countsToLoad['chapters'] = $withTrashedQuery;
|
||||
}
|
||||
$deletable->loadCount($countsToLoad);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AttachmentController extends Controller
|
||||
{
|
||||
protected $attachmentService;
|
||||
protected $pageRepo;
|
||||
protected AttachmentService $attachmentService;
|
||||
protected PageRepo $pageRepo;
|
||||
|
||||
/**
|
||||
* AttachmentController constructor.
|
||||
@@ -230,13 +230,13 @@ class AttachmentController extends Controller
|
||||
}
|
||||
|
||||
$fileName = $attachment->getFileName();
|
||||
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
|
||||
$attachmentStream = $this->attachmentService->streamAttachmentFromStorage($attachment);
|
||||
|
||||
if ($request->get('open') === 'true') {
|
||||
return $this->inlineDownloadResponse($attachmentContents, $fileName);
|
||||
return $this->download()->streamedInline($attachmentStream, $fileName);
|
||||
}
|
||||
|
||||
return $this->downloadResponse($attachmentContents, $fileName);
|
||||
return $this->download()->streamedDirectly($attachmentStream, $fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,17 +25,16 @@ class LoginController extends Controller
|
||||
|
|
||||
*/
|
||||
|
||||
use AuthenticatesUsers;
|
||||
use AuthenticatesUsers { logout as traitLogout; }
|
||||
|
||||
/**
|
||||
* Redirection paths.
|
||||
*/
|
||||
protected $redirectTo = '/';
|
||||
protected $redirectPath = '/';
|
||||
protected $redirectAfterLogout = '/login';
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $loginService;
|
||||
protected SocialAuthService $socialAuthService;
|
||||
protected LoginService $loginService;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
@@ -50,7 +49,6 @@ class LoginController extends Controller
|
||||
$this->loginService = $loginService;
|
||||
|
||||
$this->redirectPath = url('/');
|
||||
$this->redirectAfterLogout = url('/login');
|
||||
}
|
||||
|
||||
public function username()
|
||||
@@ -73,6 +71,7 @@ class LoginController extends Controller
|
||||
{
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
$authMethod = config('auth.method');
|
||||
$preventInitiation = $request->get('prevent_auto_init') === 'true';
|
||||
|
||||
if ($request->has('email')) {
|
||||
session()->flashInput([
|
||||
@@ -84,6 +83,12 @@ class LoginController extends Controller
|
||||
// Store the previous location for redirect after login
|
||||
$this->updateIntendedFromPrevious();
|
||||
|
||||
if (!$preventInitiation && $this->shouldAutoInitiate()) {
|
||||
return view('auth.login-initiate', [
|
||||
'authMethod' => $authMethod,
|
||||
]);
|
||||
}
|
||||
|
||||
return view('auth.login', [
|
||||
'socialDrivers' => $socialDrivers,
|
||||
'authMethod' => $authMethod,
|
||||
@@ -251,4 +256,32 @@ class LoginController extends Controller
|
||||
|
||||
redirect()->setIntendedUrl($previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if login auto-initiate should be valid based upon authentication config.
|
||||
*/
|
||||
protected function shouldAutoInitiate(): bool
|
||||
{
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
$authMethod = config('auth.method');
|
||||
$autoRedirect = config('auth.auto_initiate');
|
||||
|
||||
return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user and perform subsequent redirect.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
$this->traitLogout($request);
|
||||
|
||||
$redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
|
||||
|
||||
return redirect($redirectUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Repos\BookRepo;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\Cloner;
|
||||
use BookStack\Entities\Tools\HierarchyTransformer;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Entities\Tools\ShelfContext;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
@@ -87,10 +88,11 @@ class BookController extends Controller
|
||||
public function store(Request $request, string $shelfSlug = null)
|
||||
{
|
||||
$this->checkPermission('book-create-all');
|
||||
$this->validate($request, [
|
||||
$validated = $this->validate($request, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
'tags' => ['array'],
|
||||
]);
|
||||
|
||||
$bookshelf = null;
|
||||
@@ -99,8 +101,7 @@ class BookController extends Controller
|
||||
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
|
||||
}
|
||||
|
||||
$book = $this->bookRepo->create($request->all());
|
||||
$this->bookRepo->updateCoverImage($book, $request->file('image', null));
|
||||
$book = $this->bookRepo->create($validated);
|
||||
|
||||
if ($bookshelf) {
|
||||
$bookshelf->appendBook($book);
|
||||
@@ -158,15 +159,21 @@ class BookController extends Controller
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->validate($request, [
|
||||
|
||||
$validated = $this->validate($request, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
'tags' => ['array'],
|
||||
]);
|
||||
|
||||
$book = $this->bookRepo->update($book, $request->all());
|
||||
$resetCover = $request->has('image_reset');
|
||||
$this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
|
||||
if ($request->has('image_reset')) {
|
||||
$validated['image'] = null;
|
||||
} elseif (array_key_exists('image', $validated) && is_null($validated['image'])) {
|
||||
unset($validated['image']);
|
||||
}
|
||||
|
||||
$book = $this->bookRepo->update($book, $validated);
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
@@ -262,4 +269,20 @@ class BookController extends Controller
|
||||
|
||||
return redirect($bookCopy->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the chapter to a book.
|
||||
*/
|
||||
public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$this->checkPermission('book-create-all');
|
||||
|
||||
$shelf = $transformer->transformBookToShelf($book);
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class BookExportController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$pdfContent = $this->exportFormatter->bookToPdf($book);
|
||||
|
||||
return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
|
||||
return $this->download()->directly($pdfContent, $bookSlug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +44,7 @@ class BookExportController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
|
||||
|
||||
return $this->downloadResponse($htmlContent, $bookSlug . '.html');
|
||||
return $this->download()->directly($htmlContent, $bookSlug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +55,7 @@ class BookExportController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$textContent = $this->exportFormatter->bookToPlainText($book);
|
||||
|
||||
return $this->downloadResponse($textContent, $bookSlug . '.txt');
|
||||
return $this->download()->directly($textContent, $bookSlug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,6 +66,6 @@ class BookExportController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$textContent = $this->exportFormatter->bookToMarkdown($book);
|
||||
|
||||
return $this->downloadResponse($textContent, $bookSlug . '.md');
|
||||
return $this->download()->directly($textContent, $bookSlug . '.md');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,15 +83,15 @@ class BookshelfController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$this->validate($request, [
|
||||
$validated = $this->validate($request, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
'tags' => ['array'],
|
||||
]);
|
||||
|
||||
$bookIds = explode(',', $request->get('books', ''));
|
||||
$shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
|
||||
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null));
|
||||
$shelf = $this->bookshelfRepo->create($validated, $bookIds);
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
@@ -160,16 +160,21 @@ class BookshelfController extends Controller
|
||||
{
|
||||
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||
$this->validate($request, [
|
||||
$validated = $this->validate($request, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
'tags' => ['array'],
|
||||
]);
|
||||
|
||||
if ($request->has('image_reset')) {
|
||||
$validated['image'] = null;
|
||||
} elseif (array_key_exists('image', $validated) && is_null($validated['image'])) {
|
||||
unset($validated['image']);
|
||||
}
|
||||
|
||||
$bookIds = explode(',', $request->get('books', ''));
|
||||
$shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
|
||||
$resetCover = $request->has('image_reset');
|
||||
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
|
||||
$shelf = $this->bookshelfRepo->update($shelf, $validated, $bookIds);
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Repos\ChapterRepo;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\Cloner;
|
||||
use BookStack\Entities\Tools\HierarchyTransformer;
|
||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\MoveOperationException;
|
||||
@@ -272,4 +273,19 @@ class ChapterController extends Controller
|
||||
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the chapter to a book.
|
||||
*/
|
||||
public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug)
|
||||
{
|
||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||
$this->checkPermission('book-create-all');
|
||||
|
||||
$book = $transformer->transformChapterToBook($chapter);
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class ChapterExportController extends Controller
|
||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
|
||||
|
||||
return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
|
||||
return $this->download()->directly($pdfContent, $chapterSlug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +47,7 @@ class ChapterExportController extends Controller
|
||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||
$containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter);
|
||||
|
||||
return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
|
||||
return $this->download()->directly($containedHtml, $chapterSlug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +60,7 @@ class ChapterExportController extends Controller
|
||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||
$chapterText = $this->exportFormatter->chapterToPlainText($chapter);
|
||||
|
||||
return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
|
||||
return $this->download()->directly($chapterText, $chapterSlug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,10 +70,9 @@ class ChapterExportController extends Controller
|
||||
*/
|
||||
public function markdown(string $bookSlug, string $chapterSlug)
|
||||
{
|
||||
// TODO: This should probably export to a zip file.
|
||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||
$chapterText = $this->exportFormatter->chapterToMarkdown($chapter);
|
||||
|
||||
return $this->downloadResponse($chapterText, $chapterSlug . '.md');
|
||||
return $this->download()->directly($chapterText, $chapterSlug . '.md');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Http\Responses\DownloadResponseFactory;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Model;
|
||||
use BookStack\Util\WebSafeMimeSniffer;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
@@ -109,30 +108,11 @@ abstract class Controller extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a response that forces a download in the browser.
|
||||
* Create and return a new download response factory using the current request.
|
||||
*/
|
||||
protected function downloadResponse(string $content, string $fileName): Response
|
||||
protected function download(): DownloadResponseFactory
|
||||
{
|
||||
return response()->make($content, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file download response that provides the file with a content-type
|
||||
* correct for the file, in a way so the browser can show the content in browser.
|
||||
*/
|
||||
protected function inlineDownloadResponse(string $content, string $fileName): Response
|
||||
{
|
||||
$mime = (new WebSafeMimeSniffer())->sniff($content);
|
||||
|
||||
return response()->make($content, 200, [
|
||||
'Content-Type' => $mime,
|
||||
'Content-Disposition' => 'inline; filename="' . $fileName . '"',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]);
|
||||
return new DownloadResponseFactory(request());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ use BookStack\Entities\Tools\Cloner;
|
||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Entities\Tools\PageEditActivity;
|
||||
use BookStack\Entities\Tools\PageEditorData;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
@@ -21,7 +22,7 @@ use Throwable;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
protected $pageRepo;
|
||||
protected PageRepo $pageRepo;
|
||||
|
||||
/**
|
||||
* PageController constructor.
|
||||
@@ -82,22 +83,15 @@ class PageController extends Controller
|
||||
*
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function editDraft(string $bookSlug, int $pageId)
|
||||
public function editDraft(Request $request, string $bookSlug, int $pageId)
|
||||
{
|
||||
$draft = $this->pageRepo->getById($pageId);
|
||||
$this->checkOwnablePermission('page-create', $draft->getParent());
|
||||
|
||||
$editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', ''));
|
||||
$this->setPageTitle(trans('entities.pages_edit_draft'));
|
||||
|
||||
$draftsEnabled = $this->isSignedIn();
|
||||
$templates = $this->pageRepo->getTemplates(10);
|
||||
|
||||
return view('pages.edit', [
|
||||
'page' => $draft,
|
||||
'book' => $draft->book,
|
||||
'isDraft' => true,
|
||||
'draftsEnabled' => $draftsEnabled,
|
||||
'templates' => $templates,
|
||||
]);
|
||||
return view('pages.edit', $editorData->getViewData());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,43 +182,19 @@ class PageController extends Controller
|
||||
*
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function edit(string $bookSlug, string $pageSlug)
|
||||
public function edit(Request $request, string $bookSlug, string $pageSlug)
|
||||
{
|
||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$page->isDraft = false;
|
||||
$editActivity = new PageEditActivity($page);
|
||||
|
||||
// Check for active editing
|
||||
$warnings = [];
|
||||
if ($editActivity->hasActiveEditing()) {
|
||||
$warnings[] = $editActivity->activeEditingMessage();
|
||||
$editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', ''));
|
||||
if ($editorData->getWarnings()) {
|
||||
$this->showWarningNotification(implode("\n", $editorData->getWarnings()));
|
||||
}
|
||||
|
||||
// Check for a current draft version for this user
|
||||
$userDraft = $this->pageRepo->getUserDraft($page);
|
||||
if ($userDraft !== null) {
|
||||
$page->forceFill($userDraft->only(['name', 'html', 'markdown']));
|
||||
$page->isDraft = true;
|
||||
$warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
|
||||
}
|
||||
|
||||
if (count($warnings) > 0) {
|
||||
$this->showWarningNotification(implode("\n", $warnings));
|
||||
}
|
||||
|
||||
$templates = $this->pageRepo->getTemplates(10);
|
||||
$draftsEnabled = $this->isSignedIn();
|
||||
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
|
||||
|
||||
return view('pages.edit', [
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'current' => $page,
|
||||
'draftsEnabled' => $draftsEnabled,
|
||||
'templates' => $templates,
|
||||
]);
|
||||
return view('pages.edit', $editorData->getViewData());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,7 +36,7 @@ class PageExportController extends Controller
|
||||
$page->html = (new PageContent($page))->render();
|
||||
$pdfContent = $this->exportFormatter->pageToPdf($page);
|
||||
|
||||
return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
|
||||
return $this->download()->directly($pdfContent, $pageSlug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +51,7 @@ class PageExportController extends Controller
|
||||
$page->html = (new PageContent($page))->render();
|
||||
$containedHtml = $this->exportFormatter->pageToContainedHtml($page);
|
||||
|
||||
return $this->downloadResponse($containedHtml, $pageSlug . '.html');
|
||||
return $this->download()->directly($containedHtml, $pageSlug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +64,7 @@ class PageExportController extends Controller
|
||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
||||
$pageText = $this->exportFormatter->pageToPlainText($page);
|
||||
|
||||
return $this->downloadResponse($pageText, $pageSlug . '.txt');
|
||||
return $this->download()->directly($pageText, $pageSlug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,6 +77,6 @@ class PageExportController extends Controller
|
||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
||||
$pageText = $this->exportFormatter->pageToMarkdown($page);
|
||||
|
||||
return $this->downloadResponse($pageText, $pageSlug . '.md');
|
||||
return $this->download()->directly($pageText, $pageSlug . '.md');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers;
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Entities\Models\Deletion;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Repos\DeletionRepo;
|
||||
use BookStack\Entities\Tools\TrashCan;
|
||||
|
||||
class RecycleBinController extends Controller
|
||||
@@ -73,12 +74,9 @@ class RecycleBinController extends Controller
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function restore(string $id)
|
||||
public function restore(DeletionRepo $deletionRepo, string $id)
|
||||
{
|
||||
/** @var Deletion $deletion */
|
||||
$deletion = Deletion::query()->findOrFail($id);
|
||||
$this->logActivity(ActivityType::RECYCLE_BIN_RESTORE, $deletion);
|
||||
$restoreCount = (new TrashCan())->restoreFromDeletion($deletion);
|
||||
$restoreCount = $deletionRepo->restore((int) $id);
|
||||
|
||||
$this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount]));
|
||||
|
||||
@@ -103,12 +101,9 @@ class RecycleBinController extends Controller
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function destroy(string $id)
|
||||
public function destroy(DeletionRepo $deletionRepo, string $id)
|
||||
{
|
||||
/** @var Deletion $deletion */
|
||||
$deletion = Deletion::query()->findOrFail($id);
|
||||
$this->logActivity(ActivityType::RECYCLE_BIN_DESTROY, $deletion);
|
||||
$deleteCount = (new TrashCan())->destroyFromDeletion($deletion);
|
||||
$deleteCount = $deletionRepo->destroy((int) $id);
|
||||
|
||||
$this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
|
||||
|
||||
|
||||
@@ -9,28 +9,37 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the settings.
|
||||
* Handle requests to the settings index path.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return redirect('/settings/features');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the settings for the given category.
|
||||
*/
|
||||
public function category(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 +48,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 +67,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 +75,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class Localization
|
||||
/**
|
||||
* Array of right-to-left locales.
|
||||
*/
|
||||
protected $rtlLocales = ['ar', 'he'];
|
||||
protected $rtlLocales = ['ar', 'fa', 'he'];
|
||||
|
||||
/**
|
||||
* Map of BookStack locale names to best-estimate system locale names.
|
||||
@@ -29,6 +29,8 @@ class Localization
|
||||
'es' => 'es_ES',
|
||||
'es_AR' => 'es_AR',
|
||||
'et' => 'et_EE',
|
||||
'eu' => 'eu_ES',
|
||||
'fa' => 'fa_IR',
|
||||
'fr' => 'fr_FR',
|
||||
'he' => 'he_IL',
|
||||
'hr' => 'hr_HR',
|
||||
|
||||
@@ -8,20 +8,38 @@ class Request extends LaravelRequest
|
||||
{
|
||||
/**
|
||||
* Override the default request methods to get the scheme and host
|
||||
* to set the custom APP_URL, if set.
|
||||
* to directly use the custom APP_URL, if set.
|
||||
*
|
||||
* @return \Illuminate\Config\Repository|mixed|string
|
||||
* @return string
|
||||
*/
|
||||
public function getSchemeAndHttpHost()
|
||||
{
|
||||
$base = config('app.url', null);
|
||||
$appUrl = config('app.url', null);
|
||||
|
||||
if ($base) {
|
||||
$base = trim($base, '/');
|
||||
} else {
|
||||
$base = $this->getScheme() . '://' . $this->getHttpHost();
|
||||
if ($appUrl) {
|
||||
return implode('/', array_slice(explode('/', $appUrl), 0, 3));
|
||||
}
|
||||
|
||||
return $base;
|
||||
return parent::getSchemeAndHttpHost();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the default request methods to get the base URL
|
||||
* to directly use the custom APP_URL, if set.
|
||||
* The base URL never ends with a / but should start with one if not empty.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBaseUrl()
|
||||
{
|
||||
$appUrl = config('app.url', null);
|
||||
|
||||
if ($appUrl) {
|
||||
$parsedBaseUrl = rtrim(implode('/', array_slice(explode('/', $appUrl), 3)), '/');
|
||||
|
||||
return empty($parsedBaseUrl) ? '' : ('/' . $parsedBaseUrl);
|
||||
}
|
||||
|
||||
return parent::getBaseUrl();
|
||||
}
|
||||
}
|
||||
|
||||
77
app/Http/Responses/DownloadResponseFactory.php
Normal file
77
app/Http/Responses/DownloadResponseFactory.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Responses;
|
||||
|
||||
use BookStack\Util\WebSafeMimeSniffer;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class DownloadResponseFactory
|
||||
{
|
||||
protected Request $request;
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a response that directly forces a download in the browser.
|
||||
*/
|
||||
public function directly(string $content, string $fileName): Response
|
||||
{
|
||||
return response()->make($content, 200, $this->getHeaders($fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a response that forces a download, from a given stream of content.
|
||||
*/
|
||||
public function streamedDirectly($stream, string $fileName): StreamedResponse
|
||||
{
|
||||
return response()->stream(function () use ($stream) {
|
||||
|
||||
// End & flush the output buffer, if we're in one, otherwise we still use memory.
|
||||
// Output buffer may or may not exist depending on PHP `output_buffering` setting.
|
||||
// Ignore in testing since output buffers are used to gather a response.
|
||||
if (!empty(ob_get_status()) && !app()->runningUnitTests()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
fpassthru($stream);
|
||||
fclose($stream);
|
||||
}, 200, $this->getHeaders($fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file download response that provides the file with a content-type
|
||||
* correct for the file, in a way so the browser can show the content in browser,
|
||||
* for a given content stream.
|
||||
*/
|
||||
public function streamedInline($stream, string $fileName): StreamedResponse
|
||||
{
|
||||
$sniffContent = fread($stream, 2000);
|
||||
$mime = (new WebSafeMimeSniffer())->sniff($sniffContent);
|
||||
|
||||
return response()->stream(function () use ($sniffContent, $stream) {
|
||||
echo $sniffContent;
|
||||
fpassthru($stream);
|
||||
fclose($stream);
|
||||
}, 200, $this->getHeaders($fileName, $mime));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the common headers to provide for a download response.
|
||||
*/
|
||||
protected function getHeaders(string $fileName, string $mime = 'application/octet-stream'): array
|
||||
{
|
||||
$disposition = ($mime === 'application/octet-stream') ? 'attachment' : 'inline';
|
||||
$downloadName = str_replace('"', '', $fileName);
|
||||
|
||||
return [
|
||||
'Content-Type' => $mime,
|
||||
'Content-Disposition' => "{$disposition}; filename=\"{$downloadName}\"",
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -51,12 +51,12 @@ class AppServiceProvider extends ServiceProvider
|
||||
// Allow longer string lengths after upgrade to utf8mb4
|
||||
Schema::defaultStringLength(191);
|
||||
|
||||
// Set morph-map due to namespace changes
|
||||
Relation::morphMap([
|
||||
'BookStack\\Bookshelf' => Bookshelf::class,
|
||||
'BookStack\\Book' => Book::class,
|
||||
'BookStack\\Chapter' => Chapter::class,
|
||||
'BookStack\\Page' => Page::class,
|
||||
// Set morph-map for our relations to friendlier aliases
|
||||
Relation::enforceMorphMap([
|
||||
'bookshelf' => Bookshelf::class,
|
||||
'book' => Book::class,
|
||||
'chapter' => Chapter::class,
|
||||
'page' => Page::class,
|
||||
]);
|
||||
|
||||
// View Composers
|
||||
|
||||
@@ -93,6 +93,8 @@ class ThemeEvents
|
||||
* @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';
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class AttachmentService
|
||||
{
|
||||
protected $fileSystem;
|
||||
protected FilesystemManager $fileSystem;
|
||||
|
||||
/**
|
||||
* AttachmentService constructor.
|
||||
@@ -64,13 +64,15 @@ class AttachmentService
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an attachment from storage.
|
||||
* Stream an attachment from storage.
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getAttachmentFromStorage(Attachment $attachment): string
|
||||
public function streamAttachmentFromStorage(Attachment $attachment)
|
||||
{
|
||||
return $this->getStorageDisk()->get($this->adjustPathForStorageDisk($attachment->path));
|
||||
return $this->getStorageDisk()->readStream($this->adjustPathForStorageDisk($attachment->path));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,8 +213,6 @@ class AttachmentService
|
||||
*/
|
||||
protected function putFileInStorage(UploadedFile $uploadedFile): string
|
||||
{
|
||||
$attachmentData = file_get_contents($uploadedFile->getRealPath());
|
||||
|
||||
$storage = $this->getStorageDisk();
|
||||
$basePath = 'uploads/files/' . date('Y-m-M') . '/';
|
||||
|
||||
@@ -221,10 +221,11 @@ class AttachmentService
|
||||
$uploadFileName = Str::random(3) . $uploadFileName;
|
||||
}
|
||||
|
||||
$attachmentStream = fopen($uploadedFile->getRealPath(), 'r');
|
||||
$attachmentPath = $basePath . $uploadFileName;
|
||||
|
||||
try {
|
||||
$storage->put($this->adjustPathForStorageDisk($attachmentPath), $attachmentData);
|
||||
$storage->writeStream($this->adjustPathForStorageDisk($attachmentPath), $attachmentStream);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error when attempting file upload:' . $e->getMessage());
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -308,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();
|
||||
@@ -328,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,14 @@ class WebSafeMimeSniffer
|
||||
'application/json',
|
||||
'application/octet-stream',
|
||||
'application/pdf',
|
||||
'audio/aac',
|
||||
'audio/midi',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/opus',
|
||||
'audio/wav',
|
||||
'audio/webm',
|
||||
'audio/x-m4a',
|
||||
'image/apng',
|
||||
'image/bmp',
|
||||
'image/jpeg',
|
||||
|
||||
1140
composer.lock
generated
1140
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -21,8 +21,9 @@ class RoleFactory extends Factory
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'display_name' => $this->faker->sentence(3),
|
||||
'description' => $this->faker->sentence(10),
|
||||
'display_name' => $this->faker->sentence(3),
|
||||
'description' => $this->faker->sentence(10),
|
||||
'external_auth_id' => '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddEditorChangeFieldAndPermission extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// Add the new 'editor' column to the pages table
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->string('editor', 50)->default('');
|
||||
});
|
||||
|
||||
// Populate the new 'editor' column
|
||||
// We set it to 'markdown' for pages currently with markdown content
|
||||
DB::table('pages')->where('markdown', '!=', '')->update(['editor' => 'markdown']);
|
||||
// We set it to 'wysiwyg' where we have HTML but no markdown
|
||||
DB::table('pages')->where('markdown', '=', '')
|
||||
->where('html', '!=', '')
|
||||
->update(['editor' => 'wysiwyg']);
|
||||
|
||||
// Give the admin user permission to change the editor
|
||||
$adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
|
||||
|
||||
$permissionId = DB::table('role_permissions')->insertGetId([
|
||||
'name' => 'editor-change',
|
||||
'display_name' => 'Change page editor',
|
||||
'created_at' => Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => Carbon::now()->toDateTimeString(),
|
||||
]);
|
||||
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $adminRoleId,
|
||||
'permission_id' => $permissionId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// Drop the new column from the pages table
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropColumn('editor');
|
||||
});
|
||||
|
||||
// Remove traces of the role permission
|
||||
DB::table('role_permissions')->where('name', '=', 'editor-change')->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdatePolymorphicTypes extends Migration
|
||||
{
|
||||
/**
|
||||
* Mapping of old polymorphic types to new simpler values.
|
||||
*/
|
||||
protected $changeMap = [
|
||||
'BookStack\\Bookshelf' => 'bookshelf',
|
||||
'BookStack\\Book' => 'book',
|
||||
'BookStack\\Chapter' => 'chapter',
|
||||
'BookStack\\Page' => 'page',
|
||||
];
|
||||
|
||||
/**
|
||||
* Mapping of tables and columns that contain polymorphic types.
|
||||
*/
|
||||
protected $columnsByTable = [
|
||||
'activities' => 'entity_type',
|
||||
'comments' => 'entity_type',
|
||||
'deletions' => 'deletable_type',
|
||||
'entity_permissions' => 'restrictable_type',
|
||||
'favourites' => 'favouritable_type',
|
||||
'joint_permissions' => 'entity_type',
|
||||
'search_terms' => 'entity_type',
|
||||
'tags' => 'entity_type',
|
||||
'views' => 'viewable_type',
|
||||
];
|
||||
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
foreach ($this->columnsByTable as $table => $column) {
|
||||
foreach ($this->changeMap as $oldVal => $newVal) {
|
||||
DB::table($table)
|
||||
->where([$column => $oldVal])
|
||||
->update([$column => $newVal]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
foreach ($this->columnsByTable as $table => $column) {
|
||||
foreach ($this->changeMap as $oldVal => $newVal) {
|
||||
DB::table($table)
|
||||
->where([$column => $newVal])
|
||||
->update([$column => $oldVal]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,7 @@
|
||||
"updated_at": "2019-12-11T20:57:31.000000Z",
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"owned_by": 1,
|
||||
"image_id": 3
|
||||
"owned_by": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
@@ -21,8 +20,7 @@
|
||||
"updated_at": "2019-12-11T20:57:23.000000Z",
|
||||
"created_by": 4,
|
||||
"updated_by": 3,
|
||||
"owned_by": 3,
|
||||
"image_id": 34
|
||||
"owned_by": 3
|
||||
}
|
||||
],
|
||||
"total": 14
|
||||
|
||||
@@ -7,6 +7,5 @@
|
||||
"updated_at": "2020-01-12T14:16:10.000000Z",
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"owned_by": 1,
|
||||
"image_id": 452
|
||||
"owned_by": 1
|
||||
}
|
||||
3
dev/api/responses/recycle-bin-destroy.json
Normal file
3
dev/api/responses/recycle-bin-destroy.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"delete_count": 2
|
||||
}
|
||||
64
dev/api/responses/recycle-bin-list.json
Normal file
64
dev/api/responses/recycle-bin-list.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 18,
|
||||
"deleted_by": 1,
|
||||
"created_at": "2022-04-20T12:57:46.000000Z",
|
||||
"updated_at": "2022-04-20T12:57:46.000000Z",
|
||||
"deletable_type": "page",
|
||||
"deletable_id": 2582,
|
||||
"deletable": {
|
||||
"id": 2582,
|
||||
"book_id": 25,
|
||||
"chapter_id": 0,
|
||||
"name": "A Wonderful Page",
|
||||
"slug": "a-wonderful-page",
|
||||
"priority": 9,
|
||||
"created_at": "2022-02-08T00:44:45.000000Z",
|
||||
"updated_at": "2022-04-20T12:57:46.000000Z",
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"draft": false,
|
||||
"revision_count": 1,
|
||||
"template": false,
|
||||
"owned_by": 1,
|
||||
"editor": "wysiwyg",
|
||||
"book_slug": "a-great-book",
|
||||
"parent": {
|
||||
"id": 25,
|
||||
"name": "A Great Book",
|
||||
"slug": "a-great-book",
|
||||
"description": "",
|
||||
"created_at": "2022-01-24T16:14:28.000000Z",
|
||||
"updated_at": "2022-03-06T15:14:50.000000Z",
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"owned_by": 1,
|
||||
"type": "book"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"deleted_by": 1,
|
||||
"created_at": "2022-04-25T16:07:46.000000Z",
|
||||
"updated_at": "2022-04-25T16:07:46.000000Z",
|
||||
"deletable_type": "book",
|
||||
"deletable_id": 13,
|
||||
"deletable": {
|
||||
"id": 13,
|
||||
"name": "A Big Book!",
|
||||
"slug": "a-big-book",
|
||||
"description": "This is a very large book with loads of cool stuff in it!",
|
||||
"created_at": "2021-11-08T11:26:43.000000Z",
|
||||
"updated_at": "2022-04-25T16:07:47.000000Z",
|
||||
"created_by": 27,
|
||||
"updated_by": 1,
|
||||
"owned_by": 1,
|
||||
"pages_count": 208,
|
||||
"chapters_count": 50
|
||||
}
|
||||
}
|
||||
],
|
||||
"total": 2
|
||||
}
|
||||
3
dev/api/responses/recycle-bin-restore.json
Normal file
3
dev/api/responses/recycle-bin-restore.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"restore_count": 2
|
||||
}
|
||||
@@ -9,8 +9,7 @@
|
||||
"updated_at": "2020-04-10T13:00:45.000000Z",
|
||||
"created_by": 4,
|
||||
"updated_by": 1,
|
||||
"owned_by": 1,
|
||||
"image_id": 31
|
||||
"owned_by": 1
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
@@ -21,8 +20,7 @@
|
||||
"updated_at": "2020-04-10T13:00:58.000000Z",
|
||||
"created_by": 4,
|
||||
"updated_by": 1,
|
||||
"owned_by": 1,
|
||||
"image_id": 28
|
||||
"owned_by": 1
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
@@ -33,8 +31,7 @@
|
||||
"updated_at": "2020-04-10T13:00:53.000000Z",
|
||||
"created_by": 4,
|
||||
"updated_by": 1,
|
||||
"owned_by": 4,
|
||||
"image_id": 30
|
||||
"owned_by": 4
|
||||
}
|
||||
],
|
||||
"total": 3
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"owned_by": 1,
|
||||
"image_id": 501,
|
||||
"created_at": "2020-04-10T13:24:09.000000Z",
|
||||
"updated_at": "2020-04-10T13:48:22.000000Z"
|
||||
}
|
||||
32
dev/build/esbuild.js
Normal file
32
dev/build/esbuild.js
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const esbuild = require('esbuild');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Check if we're building for production
|
||||
// (Set via passing `production` as first argument)
|
||||
const isProd = process.argv[2] === 'production';
|
||||
|
||||
// Gather our input files
|
||||
const jsInDir = path.join(__dirname, '../../resources/js');
|
||||
const jsInDirFiles = fs.readdirSync(jsInDir, 'utf8');
|
||||
const entryFiles = jsInDirFiles
|
||||
.filter(f => f.endsWith('.js') || f.endsWith('.mjs'))
|
||||
.map(f => path.join(jsInDir, f));
|
||||
|
||||
// Locate our output directory
|
||||
const outDir = path.join(__dirname, '../../public/dist');
|
||||
|
||||
// Build via esbuild
|
||||
esbuild.build({
|
||||
bundle: true,
|
||||
entryPoints: entryFiles,
|
||||
outdir: outDir,
|
||||
sourcemap: true,
|
||||
target: 'es2020',
|
||||
mainFields: ['module', 'main'],
|
||||
format: 'esm',
|
||||
minify: isProd,
|
||||
logLevel: "info",
|
||||
}).catch(() => process.exit(1));
|
||||
475
package-lock.json
generated
475
package-lock.json
generated
@@ -4,28 +4,28 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bookstack",
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.10",
|
||||
"codemirror": "^5.65.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"codemirror": "^5.65.5",
|
||||
"dropzone": "^5.9.3",
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"sortablejs": "^1.14.0"
|
||||
"snabbdom": "^3.5.0",
|
||||
"sortablejs": "^1.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar-cli": "^3.0",
|
||||
"esbuild": "0.14.23",
|
||||
"esbuild": "0.14.42",
|
||||
"livereload": "^0.9.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"punycode": "^2.1.1",
|
||||
"sass": "^1.49.8"
|
||||
"sass": "^1.52.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
|
||||
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -174,9 +174,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/clipboard": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.10.tgz",
|
||||
"integrity": "sha512-cz3m2YVwFz95qSEbCDi2fzLN/epEN9zXBvfgAoGkvGOJZATMl9gtTDVOtBYkx2ODUJl2kvmud7n32sV2BpYR4g==",
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
|
||||
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
|
||||
"dependencies": {
|
||||
"good-listener": "^1.2.2",
|
||||
"select": "^1.1.2",
|
||||
@@ -195,9 +195,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "5.65.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.2.tgz",
|
||||
"integrity": "sha512-SZM4Zq7XEC8Fhroqe3LxbEEX1zUPWH1wMr5zxiBuiUF64iYOUH/JI88v4tBag8MiBS8B8gRv8O1pPXGYXQ4ErA=="
|
||||
"version": "5.65.5",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.5.tgz",
|
||||
"integrity": "sha512-HNyhvGLnYz5c+kIsB9QKVitiZUevha3ovbIYaQiGzKo7ECSL/elWD9RXt3JgNr0NdnyqE9/Rc/7uLfkJQL638w=="
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
@@ -274,9 +274,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
|
||||
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
@@ -342,9 +345,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.23.tgz",
|
||||
"integrity": "sha512-XjnIcZ9KB6lfonCa+jRguXyRYcldmkyZ99ieDksqW/C8bnyEX299yA4QH2XcgijCgaddEZePPTgvx/2imsq7Ig==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.42.tgz",
|
||||
"integrity": "sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
@@ -354,31 +357,48 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"esbuild-android-arm64": "0.14.23",
|
||||
"esbuild-darwin-64": "0.14.23",
|
||||
"esbuild-darwin-arm64": "0.14.23",
|
||||
"esbuild-freebsd-64": "0.14.23",
|
||||
"esbuild-freebsd-arm64": "0.14.23",
|
||||
"esbuild-linux-32": "0.14.23",
|
||||
"esbuild-linux-64": "0.14.23",
|
||||
"esbuild-linux-arm": "0.14.23",
|
||||
"esbuild-linux-arm64": "0.14.23",
|
||||
"esbuild-linux-mips64le": "0.14.23",
|
||||
"esbuild-linux-ppc64le": "0.14.23",
|
||||
"esbuild-linux-riscv64": "0.14.23",
|
||||
"esbuild-linux-s390x": "0.14.23",
|
||||
"esbuild-netbsd-64": "0.14.23",
|
||||
"esbuild-openbsd-64": "0.14.23",
|
||||
"esbuild-sunos-64": "0.14.23",
|
||||
"esbuild-windows-32": "0.14.23",
|
||||
"esbuild-windows-64": "0.14.23",
|
||||
"esbuild-windows-arm64": "0.14.23"
|
||||
"esbuild-android-64": "0.14.42",
|
||||
"esbuild-android-arm64": "0.14.42",
|
||||
"esbuild-darwin-64": "0.14.42",
|
||||
"esbuild-darwin-arm64": "0.14.42",
|
||||
"esbuild-freebsd-64": "0.14.42",
|
||||
"esbuild-freebsd-arm64": "0.14.42",
|
||||
"esbuild-linux-32": "0.14.42",
|
||||
"esbuild-linux-64": "0.14.42",
|
||||
"esbuild-linux-arm": "0.14.42",
|
||||
"esbuild-linux-arm64": "0.14.42",
|
||||
"esbuild-linux-mips64le": "0.14.42",
|
||||
"esbuild-linux-ppc64le": "0.14.42",
|
||||
"esbuild-linux-riscv64": "0.14.42",
|
||||
"esbuild-linux-s390x": "0.14.42",
|
||||
"esbuild-netbsd-64": "0.14.42",
|
||||
"esbuild-openbsd-64": "0.14.42",
|
||||
"esbuild-sunos-64": "0.14.42",
|
||||
"esbuild-windows-32": "0.14.42",
|
||||
"esbuild-windows-64": "0.14.42",
|
||||
"esbuild-windows-arm64": "0.14.42"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.42.tgz",
|
||||
"integrity": "sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-arm64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.23.tgz",
|
||||
"integrity": "sha512-k9sXem++mINrZty1v4FVt6nC5BQCFG4K2geCIUUqHNlTdFnuvcqsY7prcKZLFhqVC1rbcJAr9VSUGFL/vD4vsw==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -392,9 +412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.23.tgz",
|
||||
"integrity": "sha512-lB0XRbtOYYL1tLcYw8BoBaYsFYiR48RPrA0KfA/7RFTr4MV7Bwy/J4+7nLsVnv9FGuQummM3uJ93J3ptaTqFug==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.42.tgz",
|
||||
"integrity": "sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -408,9 +428,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-arm64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.23.tgz",
|
||||
"integrity": "sha512-yat73Z/uJ5tRcfRiI4CCTv0FSnwErm3BJQeZAh+1tIP0TUNh6o+mXg338Zl5EKChD+YGp6PN+Dbhs7qa34RxSw==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -424,9 +444,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.23.tgz",
|
||||
"integrity": "sha512-/1xiTjoLuQ+LlbfjJdKkX45qK/M7ARrbLmyf7x3JhyQGMjcxRYVR6Dw81uH3qlMHwT4cfLW4aEVBhP1aNV7VsA==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.42.tgz",
|
||||
"integrity": "sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -440,9 +460,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-arm64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.23.tgz",
|
||||
"integrity": "sha512-uyPqBU/Zcp6yEAZS4LKj5jEE0q2s4HmlMBIPzbW6cTunZ8cyvjG6YWpIZXb1KK3KTJDe62ltCrk3VzmWHp+iLg==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -456,9 +476,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-32": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.23.tgz",
|
||||
"integrity": "sha512-37R/WMkQyUfNhbH7aJrr1uCjDVdnPeTHGeDhZPUNhfoHV0lQuZNCKuNnDvlH/u/nwIYZNdVvz1Igv5rY/zfrzQ==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.42.tgz",
|
||||
"integrity": "sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -472,9 +492,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.23.tgz",
|
||||
"integrity": "sha512-H0gztDP60qqr8zoFhAO64waoN5yBXkmYCElFklpd6LPoobtNGNnDe99xOQm28+fuD75YJ7GKHzp/MLCLhw2+vQ==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.42.tgz",
|
||||
"integrity": "sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -488,9 +508,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.23.tgz",
|
||||
"integrity": "sha512-x64CEUxi8+EzOAIpCUeuni0bZfzPw/65r8tC5cy5zOq9dY7ysOi5EVQHnzaxS+1NmV+/RVRpmrzGw1QgY2Xpmw==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.42.tgz",
|
||||
"integrity": "sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -504,9 +524,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.23.tgz",
|
||||
"integrity": "sha512-c4MLOIByNHR55n3KoYf9hYDfBRghMjOiHLaoYLhkQkIabb452RWi+HsNgB41sUpSlOAqfpqKPFNg7VrxL3UX9g==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -520,9 +540,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-mips64le": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.23.tgz",
|
||||
"integrity": "sha512-kHKyKRIAedYhKug2EJpyJxOUj3VYuamOVA1pY7EimoFPzaF3NeY7e4cFBAISC/Av0/tiV0xlFCt9q0HJ68IBIw==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.42.tgz",
|
||||
"integrity": "sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -536,9 +556,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-ppc64le": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.23.tgz",
|
||||
"integrity": "sha512-7ilAiJEPuJJnJp/LiDO0oJm5ygbBPzhchJJh9HsHZzeqO+3PUzItXi+8PuicY08r0AaaOe25LA7sGJ0MzbfBag==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.42.tgz",
|
||||
"integrity": "sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -552,9 +572,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-riscv64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.23.tgz",
|
||||
"integrity": "sha512-fbL3ggK2wY0D8I5raPIMPhpCvODFE+Bhb5QGtNP3r5aUsRR6TQV+ZBXIaw84iyvKC8vlXiA4fWLGhghAd/h/Zg==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.42.tgz",
|
||||
"integrity": "sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -568,9 +588,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-s390x": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.23.tgz",
|
||||
"integrity": "sha512-GHMDCyfy7+FaNSO8RJ8KCFsnax8fLUsOrj9q5Gi2JmZMY0Zhp75keb5abTFCq2/Oy6KVcT0Dcbyo/bFb4rIFJA==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.42.tgz",
|
||||
"integrity": "sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -584,9 +604,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-netbsd-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.23.tgz",
|
||||
"integrity": "sha512-ovk2EX+3rrO1M2lowJfgMb/JPN1VwVYrx0QPUyudxkxLYrWeBxDKQvc6ffO+kB4QlDyTfdtAURrVzu3JeNdA2g==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.42.tgz",
|
||||
"integrity": "sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -600,9 +620,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-openbsd-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.23.tgz",
|
||||
"integrity": "sha512-uYYNqbVR+i7k8ojP/oIROAHO9lATLN7H2QeXKt2H310Fc8FJj4y3Wce6hx0VgnJ4k1JDrgbbiXM8rbEgQyg8KA==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.42.tgz",
|
||||
"integrity": "sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -616,9 +636,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-sunos-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.23.tgz",
|
||||
"integrity": "sha512-hAzeBeET0+SbScknPzS2LBY6FVDpgE+CsHSpe6CEoR51PApdn2IB0SyJX7vGelXzlyrnorM4CAsRyb9Qev4h9g==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.42.tgz",
|
||||
"integrity": "sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -632,9 +652,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-32": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.23.tgz",
|
||||
"integrity": "sha512-Kttmi3JnohdaREbk6o9e25kieJR379TsEWF0l39PQVHXq3FR6sFKtVPgY8wk055o6IB+rllrzLnbqOw/UV60EA==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.42.tgz",
|
||||
"integrity": "sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -648,9 +668,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.23.tgz",
|
||||
"integrity": "sha512-JtIT0t8ymkpl6YlmOl6zoSWL5cnCgyLaBdf/SiU/Eg3C13r0NbHZWNT/RDEMKK91Y6t79kTs3vyRcNZbfu5a8g==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.42.tgz",
|
||||
"integrity": "sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -664,9 +684,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-arm64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.23.tgz",
|
||||
"integrity": "sha512-cTFaQqT2+ik9e4hePvYtRZQ3pqOvKDVNarzql0VFIzhc0tru/ZgdLoXd6epLiKT+SzoSce6V9YJ+nn6RCn6SHw==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1111,9 +1131,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
|
||||
"integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
||||
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
||||
"dependencies": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
@@ -1183,13 +1203,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "12.3.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
|
||||
"integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
|
||||
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~2.1.0",
|
||||
"linkify-it": "^3.0.1",
|
||||
"entities": "~3.0.1",
|
||||
"linkify-it": "^4.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
},
|
||||
@@ -1504,9 +1524,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.49.8",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.8.tgz",
|
||||
"integrity": "sha512-NoGOjvDDOU9og9oAxhRnap71QaTjjlzrvLnKecUJ3GxhaQBrV6e7gPuSPF28u1OcVAArVojPAe4ZhOXwwC4tGw==",
|
||||
"version": "1.52.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.52.1.tgz",
|
||||
"integrity": "sha512-fSzYTbr7z8oQnVJ3Acp9hV80dM1fkMN7mSD/25mpcct9F7FPBMOI8krEYALgU1aZoqGhQNhTPsuSmxjnIvAm4Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
@@ -1581,10 +1601,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/snabbdom": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.0.tgz",
|
||||
"integrity": "sha512-Ff5BKG18KrrPuskHJlA9aujPHqEabItaDl96l7ZZndF4zt5AYSczz7ZjjgQAX5IBd5cd25lw9NfgX21yVUJ+9g==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -1870,9 +1898,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
|
||||
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
@@ -1986,9 +2014,9 @@
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.10.tgz",
|
||||
"integrity": "sha512-cz3m2YVwFz95qSEbCDi2fzLN/epEN9zXBvfgAoGkvGOJZATMl9gtTDVOtBYkx2ODUJl2kvmud7n32sV2BpYR4g==",
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
|
||||
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
|
||||
"requires": {
|
||||
"good-listener": "^1.2.2",
|
||||
"select": "^1.1.2",
|
||||
@@ -2007,9 +2035,9 @@
|
||||
}
|
||||
},
|
||||
"codemirror": {
|
||||
"version": "5.65.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.2.tgz",
|
||||
"integrity": "sha512-SZM4Zq7XEC8Fhroqe3LxbEEX1zUPWH1wMr5zxiBuiUF64iYOUH/JI88v4tBag8MiBS8B8gRv8O1pPXGYXQ4ErA=="
|
||||
"version": "5.65.5",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.5.tgz",
|
||||
"integrity": "sha512-HNyhvGLnYz5c+kIsB9QKVitiZUevha3ovbIYaQiGzKo7ECSL/elWD9RXt3JgNr0NdnyqE9/Rc/7uLfkJQL638w=="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
@@ -2077,9 +2105,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
|
||||
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
@@ -2130,162 +2158,170 @@
|
||||
}
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.23.tgz",
|
||||
"integrity": "sha512-XjnIcZ9KB6lfonCa+jRguXyRYcldmkyZ99ieDksqW/C8bnyEX299yA4QH2XcgijCgaddEZePPTgvx/2imsq7Ig==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.42.tgz",
|
||||
"integrity": "sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild-android-arm64": "0.14.23",
|
||||
"esbuild-darwin-64": "0.14.23",
|
||||
"esbuild-darwin-arm64": "0.14.23",
|
||||
"esbuild-freebsd-64": "0.14.23",
|
||||
"esbuild-freebsd-arm64": "0.14.23",
|
||||
"esbuild-linux-32": "0.14.23",
|
||||
"esbuild-linux-64": "0.14.23",
|
||||
"esbuild-linux-arm": "0.14.23",
|
||||
"esbuild-linux-arm64": "0.14.23",
|
||||
"esbuild-linux-mips64le": "0.14.23",
|
||||
"esbuild-linux-ppc64le": "0.14.23",
|
||||
"esbuild-linux-riscv64": "0.14.23",
|
||||
"esbuild-linux-s390x": "0.14.23",
|
||||
"esbuild-netbsd-64": "0.14.23",
|
||||
"esbuild-openbsd-64": "0.14.23",
|
||||
"esbuild-sunos-64": "0.14.23",
|
||||
"esbuild-windows-32": "0.14.23",
|
||||
"esbuild-windows-64": "0.14.23",
|
||||
"esbuild-windows-arm64": "0.14.23"
|
||||
"esbuild-android-64": "0.14.42",
|
||||
"esbuild-android-arm64": "0.14.42",
|
||||
"esbuild-darwin-64": "0.14.42",
|
||||
"esbuild-darwin-arm64": "0.14.42",
|
||||
"esbuild-freebsd-64": "0.14.42",
|
||||
"esbuild-freebsd-arm64": "0.14.42",
|
||||
"esbuild-linux-32": "0.14.42",
|
||||
"esbuild-linux-64": "0.14.42",
|
||||
"esbuild-linux-arm": "0.14.42",
|
||||
"esbuild-linux-arm64": "0.14.42",
|
||||
"esbuild-linux-mips64le": "0.14.42",
|
||||
"esbuild-linux-ppc64le": "0.14.42",
|
||||
"esbuild-linux-riscv64": "0.14.42",
|
||||
"esbuild-linux-s390x": "0.14.42",
|
||||
"esbuild-netbsd-64": "0.14.42",
|
||||
"esbuild-openbsd-64": "0.14.42",
|
||||
"esbuild-sunos-64": "0.14.42",
|
||||
"esbuild-windows-32": "0.14.42",
|
||||
"esbuild-windows-64": "0.14.42",
|
||||
"esbuild-windows-arm64": "0.14.42"
|
||||
}
|
||||
},
|
||||
"esbuild-android-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.42.tgz",
|
||||
"integrity": "sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-android-arm64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.23.tgz",
|
||||
"integrity": "sha512-k9sXem++mINrZty1v4FVt6nC5BQCFG4K2geCIUUqHNlTdFnuvcqsY7prcKZLFhqVC1rbcJAr9VSUGFL/vD4vsw==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-darwin-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.23.tgz",
|
||||
"integrity": "sha512-lB0XRbtOYYL1tLcYw8BoBaYsFYiR48RPrA0KfA/7RFTr4MV7Bwy/J4+7nLsVnv9FGuQummM3uJ93J3ptaTqFug==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.42.tgz",
|
||||
"integrity": "sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-darwin-arm64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.23.tgz",
|
||||
"integrity": "sha512-yat73Z/uJ5tRcfRiI4CCTv0FSnwErm3BJQeZAh+1tIP0TUNh6o+mXg338Zl5EKChD+YGp6PN+Dbhs7qa34RxSw==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-freebsd-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.23.tgz",
|
||||
"integrity": "sha512-/1xiTjoLuQ+LlbfjJdKkX45qK/M7ARrbLmyf7x3JhyQGMjcxRYVR6Dw81uH3qlMHwT4cfLW4aEVBhP1aNV7VsA==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.42.tgz",
|
||||
"integrity": "sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-freebsd-arm64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.23.tgz",
|
||||
"integrity": "sha512-uyPqBU/Zcp6yEAZS4LKj5jEE0q2s4HmlMBIPzbW6cTunZ8cyvjG6YWpIZXb1KK3KTJDe62ltCrk3VzmWHp+iLg==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-32": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.23.tgz",
|
||||
"integrity": "sha512-37R/WMkQyUfNhbH7aJrr1uCjDVdnPeTHGeDhZPUNhfoHV0lQuZNCKuNnDvlH/u/nwIYZNdVvz1Igv5rY/zfrzQ==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.42.tgz",
|
||||
"integrity": "sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.23.tgz",
|
||||
"integrity": "sha512-H0gztDP60qqr8zoFhAO64waoN5yBXkmYCElFklpd6LPoobtNGNnDe99xOQm28+fuD75YJ7GKHzp/MLCLhw2+vQ==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.42.tgz",
|
||||
"integrity": "sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-arm": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.23.tgz",
|
||||
"integrity": "sha512-x64CEUxi8+EzOAIpCUeuni0bZfzPw/65r8tC5cy5zOq9dY7ysOi5EVQHnzaxS+1NmV+/RVRpmrzGw1QgY2Xpmw==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.42.tgz",
|
||||
"integrity": "sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-arm64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.23.tgz",
|
||||
"integrity": "sha512-c4MLOIByNHR55n3KoYf9hYDfBRghMjOiHLaoYLhkQkIabb452RWi+HsNgB41sUpSlOAqfpqKPFNg7VrxL3UX9g==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-mips64le": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.23.tgz",
|
||||
"integrity": "sha512-kHKyKRIAedYhKug2EJpyJxOUj3VYuamOVA1pY7EimoFPzaF3NeY7e4cFBAISC/Av0/tiV0xlFCt9q0HJ68IBIw==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.42.tgz",
|
||||
"integrity": "sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-ppc64le": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.23.tgz",
|
||||
"integrity": "sha512-7ilAiJEPuJJnJp/LiDO0oJm5ygbBPzhchJJh9HsHZzeqO+3PUzItXi+8PuicY08r0AaaOe25LA7sGJ0MzbfBag==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.42.tgz",
|
||||
"integrity": "sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-riscv64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.23.tgz",
|
||||
"integrity": "sha512-fbL3ggK2wY0D8I5raPIMPhpCvODFE+Bhb5QGtNP3r5aUsRR6TQV+ZBXIaw84iyvKC8vlXiA4fWLGhghAd/h/Zg==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.42.tgz",
|
||||
"integrity": "sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-s390x": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.23.tgz",
|
||||
"integrity": "sha512-GHMDCyfy7+FaNSO8RJ8KCFsnax8fLUsOrj9q5Gi2JmZMY0Zhp75keb5abTFCq2/Oy6KVcT0Dcbyo/bFb4rIFJA==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.42.tgz",
|
||||
"integrity": "sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-netbsd-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.23.tgz",
|
||||
"integrity": "sha512-ovk2EX+3rrO1M2lowJfgMb/JPN1VwVYrx0QPUyudxkxLYrWeBxDKQvc6ffO+kB4QlDyTfdtAURrVzu3JeNdA2g==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.42.tgz",
|
||||
"integrity": "sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-openbsd-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.23.tgz",
|
||||
"integrity": "sha512-uYYNqbVR+i7k8ojP/oIROAHO9lATLN7H2QeXKt2H310Fc8FJj4y3Wce6hx0VgnJ4k1JDrgbbiXM8rbEgQyg8KA==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.42.tgz",
|
||||
"integrity": "sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-sunos-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.23.tgz",
|
||||
"integrity": "sha512-hAzeBeET0+SbScknPzS2LBY6FVDpgE+CsHSpe6CEoR51PApdn2IB0SyJX7vGelXzlyrnorM4CAsRyb9Qev4h9g==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.42.tgz",
|
||||
"integrity": "sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-32": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.23.tgz",
|
||||
"integrity": "sha512-Kttmi3JnohdaREbk6o9e25kieJR379TsEWF0l39PQVHXq3FR6sFKtVPgY8wk055o6IB+rllrzLnbqOw/UV60EA==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.42.tgz",
|
||||
"integrity": "sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.23.tgz",
|
||||
"integrity": "sha512-JtIT0t8ymkpl6YlmOl6zoSWL5cnCgyLaBdf/SiU/Eg3C13r0NbHZWNT/RDEMKK91Y6t79kTs3vyRcNZbfu5a8g==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.42.tgz",
|
||||
"integrity": "sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-arm64": {
|
||||
"version": "0.14.23",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.23.tgz",
|
||||
"integrity": "sha512-cTFaQqT2+ik9e4hePvYtRZQ3pqOvKDVNarzql0VFIzhc0tru/ZgdLoXd6epLiKT+SzoSce6V9YJ+nn6RCn6SHw==",
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
@@ -2591,9 +2627,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
|
||||
"integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
||||
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
||||
"requires": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
@@ -2651,13 +2687,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "12.3.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
|
||||
"integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
|
||||
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~2.1.0",
|
||||
"linkify-it": "^3.0.1",
|
||||
"entities": "~3.0.1",
|
||||
"linkify-it": "^4.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
}
|
||||
@@ -2886,9 +2922,9 @@
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.49.8",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.8.tgz",
|
||||
"integrity": "sha512-NoGOjvDDOU9og9oAxhRnap71QaTjjlzrvLnKecUJ3GxhaQBrV6e7gPuSPF28u1OcVAArVojPAe4ZhOXwwC4tGw==",
|
||||
"version": "1.52.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.52.1.tgz",
|
||||
"integrity": "sha512-fSzYTbr7z8oQnVJ3Acp9hV80dM1fkMN7mSD/25mpcct9F7FPBMOI8krEYALgU1aZoqGhQNhTPsuSmxjnIvAm4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
@@ -2945,10 +2981,15 @@
|
||||
"object-inspect": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"snabbdom": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.0.tgz",
|
||||
"integrity": "sha512-Ff5BKG18KrrPuskHJlA9aujPHqEabItaDl96l7ZZndF4zt5AYSczz7ZjjgQAX5IBd5cd25lw9NfgX21yVUJ+9g=="
|
||||
},
|
||||
"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",
|
||||
|
||||
21
package.json
21
package.json
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:css:dev": "sass ./resources/sass:./public/dist",
|
||||
"build:css:watch": "sass ./resources/sass:./public/dist --watch",
|
||||
"build:css:dev": "sass ./resources/sass:./public/dist --embed-sources",
|
||||
"build:css:watch": "sass ./resources/sass:./public/dist --watch --embed-sources",
|
||||
"build:css:production": "sass ./resources/sass:./public/dist -s compressed",
|
||||
"build:js:dev": "esbuild --bundle ./resources/js/*.{js,mjs} --outdir=public/dist/ --sourcemap --target=es2020 --main-fields=module,main --format=esm",
|
||||
"build:js:dev": "node dev/build/esbuild.js",
|
||||
"build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
|
||||
"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:js:production": "node dev/build/esbuild.js production",
|
||||
"build": "npm-run-all --parallel build:*:dev",
|
||||
"production": "npm-run-all --parallel build:*:production",
|
||||
"dev": "npm-run-all --parallel watch livereload",
|
||||
@@ -16,18 +16,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar-cli": "^3.0",
|
||||
"esbuild": "0.14.23",
|
||||
"esbuild": "0.14.42",
|
||||
"livereload": "^0.9.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"punycode": "^2.1.1",
|
||||
"sass": "^1.49.8"
|
||||
"sass": "^1.52.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.10",
|
||||
"codemirror": "^5.65.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"codemirror": "^5.65.5",
|
||||
"dropzone": "^5.9.3",
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"sortablejs": "^1.14.0"
|
||||
"snabbdom": "^3.5.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
|
||||
|
||||
@@ -29,11 +29,14 @@
|
||||
<server name="MAIL_DRIVER" value="array"/>
|
||||
<server name="LOG_CHANNEL" value="single"/>
|
||||
<server name="AUTH_METHOD" value="standard"/>
|
||||
<server name="AUTH_AUTO_INITIATE" value="false"/>
|
||||
<server name="DISABLE_EXTERNAL_SERVICES" value="true"/>
|
||||
<server name="ALLOW_UNTRUSTED_SERVER_FETCHING" value="false"/>
|
||||
<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"/>
|
||||
|
||||
54
public/dist/app.js
vendored
54
public/dist/app.js
vendored
File diff suppressed because one or more lines are too long
47
public/dist/code.js
vendored
47
public/dist/code.js
vendored
File diff suppressed because one or more lines are too long
2
public/dist/export-styles.css
vendored
2
public/dist/export-styles.css
vendored
File diff suppressed because one or more lines are too long
2
public/dist/styles.css
vendored
2
public/dist/styles.css
vendored
File diff suppressed because one or more lines are too long
@@ -63,7 +63,7 @@ Below is a high-level road map view for BookStack to provide a sense of directio
|
||||
|
||||
- **Platform REST API** - *(Most actions implemented, maturing)*
|
||||
- *A REST API covering, at minimum, control of core content models (Books, Chapters, Pages) for automation and platform extension.*
|
||||
- **Editor Alignment & Review** - *(Started)*
|
||||
- **Editor Alignment & Review** - *(In Progress)*
|
||||
- *Review the page editors with goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.*
|
||||
- **Permission System Review**
|
||||
- *Improvement in how permissions are applied and a review of the efficiency of the permission & roles system.*
|
||||
|
||||
1
resources/icons/download.svg
Normal file
1
resources/icons/download.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16.59 9H15V4c0-.55-.45-1-1-1h-4c-.55 0-1 .45-1 1v5H7.41c-.89 0-1.34 1.08-.71 1.71l4.59 4.59c.39.39 1.02.39 1.41 0l4.59-4.59c.63-.63.19-1.71-.7-1.71zM5 19c0 .55.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1z"/></svg>
|
||||
|
After Width: | Height: | Size: 297 B |
1
resources/icons/palette.svg
Normal file
1
resources/icons/palette.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,2C6.49,2,2,6.49,2,12s4.49,10,10,10c1.38,0,2.5-1.12,2.5-2.5c0-0.61-0.23-1.2-0.64-1.67c-0.08-0.1-0.13-0.21-0.13-0.33 c0-0.28,0.22-0.5,0.5-0.5H16c3.31,0,6-2.69,6-6C22,6.04,17.51,2,12,2z M17.5,13c-0.83,0-1.5-0.67-1.5-1.5c0-0.83,0.67-1.5,1.5-1.5 s1.5,0.67,1.5,1.5C19,12.33,18.33,13,17.5,13z M14.5,9C13.67,9,13,8.33,13,7.5C13,6.67,13.67,6,14.5,6S16,6.67,16,7.5 C16,8.33,15.33,9,14.5,9z M5,11.5C5,10.67,5.67,10,6.5,10S8,10.67,8,11.5C8,12.33,7.33,13,6.5,13S5,12.33,5,11.5z M11,7.5 C11,8.33,10.33,9,9.5,9S8,8.33,8,7.5C8,6.67,8.67,6,9.5,6S11,6.67,11,7.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 626 B |
1
resources/icons/swap-horizontal.svg
Normal file
1
resources/icons/swap-horizontal.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.99 16H14v-2H6.99v-3L3 15l3.99 4ZM21 9l-3.99-4v3H10v2h7.01v3z"/></svg>
|
||||
|
After Width: | Height: | Size: 141 B |
@@ -25,6 +25,7 @@ import 'codemirror/mode/ruby/ruby';
|
||||
import 'codemirror/mode/rust/rust';
|
||||
import 'codemirror/mode/shell/shell';
|
||||
import 'codemirror/mode/sql/sql';
|
||||
import 'codemirror/mode/stex/stex';
|
||||
import 'codemirror/mode/toml/toml';
|
||||
import 'codemirror/mode/vb/vb';
|
||||
import 'codemirror/mode/vbscript/vbscript';
|
||||
@@ -49,16 +50,19 @@ const modeMap = {
|
||||
diff: 'diff',
|
||||
for: 'fortran',
|
||||
fortran: 'fortran',
|
||||
'f#': 'text/x-fsharp',
|
||||
fsharp: 'text/x-fsharp',
|
||||
go: 'go',
|
||||
haskell: 'haskell',
|
||||
hs: 'haskell',
|
||||
html: 'htmlmixed',
|
||||
ini: 'properties',
|
||||
javascript: 'javascript',
|
||||
json: {name: 'javascript', json: true},
|
||||
js: 'javascript',
|
||||
jl: 'julia',
|
||||
julia: 'julia',
|
||||
javascript: 'text/javascript',
|
||||
json: 'application/json',
|
||||
js: 'text/javascript',
|
||||
jl: 'text/x-julia',
|
||||
julia: 'text/x-julia',
|
||||
latex: 'text/x-stex',
|
||||
lua: 'lua',
|
||||
md: 'markdown',
|
||||
mdown: 'markdown',
|
||||
@@ -69,7 +73,7 @@ const modeMap = {
|
||||
pl: 'perl',
|
||||
powershell: 'powershell',
|
||||
properties: 'properties',
|
||||
ocaml: 'mllike',
|
||||
ocaml: 'text/x-ocaml',
|
||||
pascal: 'text/x-pascal',
|
||||
pas: 'text/x-pascal',
|
||||
php: (content) => {
|
||||
@@ -83,8 +87,11 @@ const modeMap = {
|
||||
rs: 'rust',
|
||||
shell: 'shell',
|
||||
sh: 'shell',
|
||||
stext: 'text/x-stex',
|
||||
bash: 'shell',
|
||||
toml: 'toml',
|
||||
ts: 'text/typescript',
|
||||
typescript: 'text/typescript',
|
||||
sql: 'text/x-sql',
|
||||
vbs: 'vbscript',
|
||||
vbscript: 'vbscript',
|
||||
@@ -242,6 +249,21 @@ export function popupEditor(elem, modeSuggestion) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an inline editor to replace the given textarea.
|
||||
* @param {HTMLTextAreaElement} textArea
|
||||
* @param {String} mode
|
||||
* @returns {CodeMirror3}
|
||||
*/
|
||||
export function inlineEditor(textArea, mode) {
|
||||
return CodeMirror.fromTextArea(textArea, {
|
||||
mode: getMode(mode, textArea.value),
|
||||
lineNumbers: true,
|
||||
lineWrapping: false,
|
||||
theme: getTheme(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mode of a codemirror instance.
|
||||
* @param cmInstance
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user