Compare commits

..

2 Commits

Author SHA1 Message Date
milnerTill
844c79ae72 improved error handling and tests added 2026-04-20 13:38:17 -03:00
milnerTill
f3c1fad50a add support to HS256 algorithm 2026-04-20 11:48:04 -03:00
431 changed files with 1723 additions and 6050 deletions

View File

@@ -395,19 +395,6 @@ ALLOWED_IFRAME_HOSTS=null
# Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured. # 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" ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com"
# A list of sources/hostnames that can be loaded as CSS styles within BookStack.
# Space separated if multiple. BookStack host domain is auto-inferred.
# Defaults to a permissive set if not provided.
# Example: ALLOWED_STYLE_SOURCES="https://fonts.googleapis.com"
ALLOWED_STYLE_SOURCES=null
# A list of sources/hostnames that can be loaded as image content within BookStack.
# Space separated if multiple. BookStack host domain is auto-inferred, in addition to
# data and blob images, due to their use for various functionality.
# Defaults to a permissive set if not provided.
# Example: ALLOWED_IMAGE_SOURCES="https://images.example.com"
ALLOWED_IMAGE_SOURCES=null
# A list of the sources/hostnames that can be reached by application SSR calls. # A list of the sources/hostnames that can be reached by application SSR calls.
# This is used wherever users can provide URLs/hosts in-platform, like for webhooks. # This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
# Host-specific functionality (usually controlled via other options) like auth # Host-specific functionality (usually controlled via other options) like auth

View File

@@ -1,2 +0,0 @@
Please find our community rules on our website here:
https://www.bookstackapp.com/about/community-rules/

View File

@@ -1,4 +0,0 @@
# These are supported funding model platforms
github: [ssddanbrown]
ko_fi: ssddanbrown

View File

@@ -1,13 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Community Forum Support
url: https://community.bookstackapp.com
about: Get support by talking with the BookStack team & community.
- 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 their resolutions.
- name: Official Support Plans
url: https://www.bookstackapp.com/support/
about: View our official support plans that offer assured support for business.

View File

@@ -1,11 +0,0 @@
## Details
<!-- Write details of your pull request in here -->
<!-- Include references to any relevant issues/discussions -->
## Checklist
<!-- Put an 'x' in between the brackets below to confirm these elements -->
- [ ] I have read the [BookStack community rules](https://www.bookstackapp.com/about/community-rules/).
- [ ] This PR does not feature significant use of LLM/AI generation as per the community rules above.

View File

@@ -1,34 +0,0 @@
name: Crowdin Action
on:
push:
branches: [ development ]
paths:
- 'lang/**.php'
schedule:
- cron: '30 4 * * *'
workflow_dispatch:
jobs:
synchronize-with-crowdin:
runs-on: docker
container:
image: docker.io/library/node:24-trixie
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v6
- name: crowdin action
uses: https://github.com/crowdin/github-action@v2
with:
crowdin_branch_name: development
upload_sources: true
upload_translations: false
download_translations: true
localization_branch_name: l10n_development
create_pull_request: false
github_base_url: codeberg.org
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -1,6 +1,6 @@
name: New API Endpoint or API Ability name: New API Endpoint or API Ability
description: Request a new endpoint or API feature be added description: Request a new endpoint or API feature be added
labels: ["Type/API Request"] labels: [":nut_and_bolt: API Request"]
body: body:
- type: textarea - type: textarea
id: feature id: feature

View File

@@ -1,6 +1,6 @@
name: Bug Report name: Bug Report
description: Create a report to help us fix bugs & issues in existing supported functionality description: Create a report to help us fix bugs & issues in existing supported functionality
labels: ["Type/Bug Report"] labels: [":bug: Bug"]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View File

@@ -1,8 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Open Issues Here Instead - name: Discord Chat Support
url: https://codeberg.org/bookstack/bookstack/issues url: https://discord.gg/ztkBqR2
about: This project has migrated to Codeberg, please open issues there instead. about: Realtime support & chat with the BookStack community and the team.
- name: Debugging & Common Issues - name: Debugging & Common Issues
url: https://www.bookstackapp.com/docs/admin/debugging/ url: https://www.bookstackapp.com/docs/admin/debugging/

View File

@@ -1,6 +1,6 @@
name: Feature Request name: Feature Request
description: Request a new feature or idea to be added to BookStack description: Request a new feature or idea to be added to BookStack
labels: ["Type/Feature Request"] labels: [":hammer: Feature Request"]
body: body:
- type: textarea - type: textarea
id: description id: description
@@ -33,7 +33,7 @@ body:
attributes: attributes:
label: Have you searched for an existing open/closed issue? label: Have you searched for an existing open/closed issue?
description: | description: |
To help us keep these issues under control, please ensure you have first [searched our issue list](https://codeberg.org/bookstack/bookstack/issues) for any existing issues that cover the fundamental 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 fundamental benefit/goal of your request.
options: options:
- label: I have searched for existing issues and none cover my fundamental request - label: I have searched for existing issues and none cover my fundamental request
required: true required: true

View File

@@ -1,6 +1,6 @@
name: Language Request 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: ["Focus: Translations"] labels: [":earth_africa: Translations"]
assignees: assignees:
- ssddanbrown - ssddanbrown
body: body:

View File

@@ -1,6 +1,6 @@
name: Support Request name: Support Request
description: Request support for a specific problem you have not been able to solve yourself description: Request support for a specific problem you have not been able to solve yourself
labels: ["Type/Support"] labels: [":dog2: Support"]
body: body:
- type: checkboxes - type: checkboxes
id: useddocs id: useddocs
@@ -15,11 +15,11 @@ body:
- type: checkboxes - type: checkboxes
id: searchissue id: searchissue
attributes: attributes:
label: Searched Existing Issues label: Searched GitHub Issues
description: | description: |
I have searched for the issue and potential resolutions within the [project's issue list](https://codeberg.org/bookstack/bookstack/issues) I have searched for the issue and potential resolutions within the [project's GitHub issue list](https://github.com/BookStackApp/BookStack/issues)
options: options:
- label: I have searched for the issue. - label: I have searched GitHub for the issue.
required: true required: true
- type: textarea - type: textarea
id: scenario id: scenario

View File

@@ -2,7 +2,7 @@
## Supported Versions ## Supported Versions
Only the [latest version](https://codeberg.org/bookstack/bookstack/releases) of BookStack is supported. Only the [latest version](https://github.com/BookStackApp/BookStack/releases) of BookStack is supported.
We generally don't support older versions of BookStack due to maintenance effort and We generally don't support older versions of BookStack due to maintenance effort and
since we aim to provide a fairly stable upgrade path for new versions. since we aim to provide a fairly stable upgrade path for new versions.
@@ -12,14 +12,16 @@ If you'd like to be notified of new potential security concerns you can [sign-up
## Reporting a Vulnerability ## Reporting a Vulnerability
If you've found an issue that likely has no impact to existing users (For example, an issue only in the development branch) If you've found an issue that likely has no impact to existing users (For example, in a development-only branch)
feel free to raise it via a standard Codeberg bug report issue. feel free to raise it via a standard GitHub bug report issue.
If the issue could have a security impact to BookStack instances, If the issue could have a security impact to BookStack instances,
please directly contact the lead maintainer via email Dan Brown using the [details found here](https://www.bookstackapp.com/links/contact/). please directly contact the lead maintainer [@ssddanbrown](https://github.com/ssddanbrown).
You will need to log in to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown).
Alternatively you can send a DM via Mastodon to [@danb@fosstodon.org](https://fosstodon.org/@danb).
Please be patient while the vulnerability is being reviewed. Deploying the fix to address the vulnerability Please be patient while the vulnerability is being reviewed. Deploying the fix to address the vulnerability
can often take a little time due to the amount of preparation required, to ensure the vulnerability has can often take a little time due to the amount of preparation required, to ensure the vulnerability has
been covered, and to create the content required to adequately notify the user-base. been covered, and to create the content required to adequately notify the user-base.
Thank you for keeping BookStack instances safe! Thank you for keeping BookStack instances safe!

View File

@@ -1,10 +1,11 @@
**Warning:** ## Details
This project has migrated to Codeberg: <!-- Write details of your pull request in here -->
https://codeberg.org/bookstack/bookstack <!-- Include references to any relevant issues/discussions -->
Please open pull requests here instead. ## Checklist
ANY PULL REQUESTS OPENED HERE WILL BE CLOSED WITHOUT COMMENT OR MERGE. <!-- Put an 'x' in between the brackets below to confirm these elements -->
--- - [ ] I have read the [BookStack community rules](https://www.bookstackapp.com/about/community-rules/).
- [ ] This PR does not feature significant use of LLM/AI generation as per the community rules above.

View File

@@ -533,14 +533,7 @@ JanDziaslo :: Polish
Charllys Fernandes (CharllysFernandes) :: Portuguese, Brazilian Charllys Fernandes (CharllysFernandes) :: Portuguese, Brazilian
Ilgiz Zigangirov (inov8) :: Russian Ilgiz Zigangirov (inov8) :: Russian
Max Israelsson (Blezie) :: Swedish Max Israelsson (Blezie) :: Swedish
Skiddybison5924 (chris-devel0per) :: German Informal; German Skiddybison5924 (chris-devel0per) :: German
Veyilla Nightwhisper (Veyilla) :: German Veyilla Nightwhisper (Veyilla) :: German
João Barbosa (hypeedd) :: Portuguese João Barbosa (hypeedd) :: Portuguese
Abcdefg Hijklmn (collatek) :: Korean Abcdefg Hijklmn (collatek) :: Korean
Suthep Yonphimai (tomztt) :: Thai
MrClock (MrClock8163) :: Hungarian
Elena0875 :: Russian
FelixFrizzy :: German
Pedro de Mattia (pdmtt) :: Portuguese, Brazilian
lonestan :: Russian
Paul Kernstock (kernstock) :: German

View File

@@ -1,7 +1,6 @@
name: analyse-php name: analyse-php
on: on:
workflow_dispatch:
push: push:
paths: paths:
- '**.php' - '**.php'
@@ -12,16 +11,14 @@ on:
jobs: jobs:
build: build:
if: ${{ github.ref != 'refs/heads/l10n_development' }} if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: docker runs-on: ubuntu-24.04
container:
image: docker.io/library/node:24-trixie
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v6 - uses: actions/checkout@v4
- name: Setup PHP - name: Setup PHP
uses: https://github.com/shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: 8.5 php-version: 8.3
extensions: gd, mbstring, json, curl, xml, mysql, ldap extensions: gd, mbstring, json, curl, xml, mysql, ldap
- name: Get Composer Cache Directory - name: Get Composer Cache Directory
@@ -30,16 +27,14 @@ jobs:
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer packages - name: Cache composer packages
uses: https://code.forgejo.org/actions/cache@v5 uses: actions/cache@v4
with: with:
path: ${{ steps.composer-cache.outputs.dir }} path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-8.5 key: ${{ runner.os }}-composer-8.3
restore-keys: ${{ runner.os }}-composer- restore-keys: ${{ runner.os }}-composer-
- name: Install composer dependencies - name: Install composer dependencies
run: composer install --prefer-dist --no-interaction --ansi run: composer install --prefer-dist --no-interaction --ansi
env:
COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GH_TOKEN }}"}}'
- name: Run static analysis check - name: Run static analysis check
run: composer check-static run: composer check-static

View File

@@ -1,7 +1,6 @@
name: lint-js name: lint-js
on: on:
workflow_dispatch:
push: push:
paths: paths:
- '**.js' - '**.js'
@@ -14,11 +13,9 @@ on:
jobs: jobs:
build: build:
if: ${{ github.ref != 'refs/heads/l10n_development' }} if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: docker runs-on: ubuntu-24.04
container:
image: docker.io/library/node:24-trixie
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v6 - uses: actions/checkout@v4
- name: Install NPM deps - name: Install NPM deps
run: npm ci run: npm ci

View File

@@ -1,7 +1,6 @@
name: lint-php name: lint-php
on: on:
workflow_dispatch:
push: push:
paths: paths:
- '**.php' - '**.php'
@@ -12,16 +11,14 @@ on:
jobs: jobs:
build: build:
if: ${{ github.ref != 'refs/heads/l10n_development' }} if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: docker runs-on: ubuntu-24.04
container:
image: docker.io/library/node:24-trixie
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v6 - uses: actions/checkout@v4
- name: Setup PHP - name: Setup PHP
uses: https://github.com/shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: 8.5 php-version: 8.3
tools: phpcs tools: phpcs
- name: Run formatting check - name: Run formatting check

View File

@@ -1,7 +1,6 @@
name: test-js name: test-js
on: on:
workflow_dispatch:
push: push:
paths: paths:
- '**.js' - '**.js'
@@ -16,11 +15,9 @@ on:
jobs: jobs:
build: build:
if: ${{ github.ref != 'refs/heads/l10n_development' }} if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: docker runs-on: ubuntu-24.04
container:
image: docker.io/library/node:24-trixie
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v6 - uses: actions/checkout@v6
- name: Install NPM deps - name: Install NPM deps
run: npm ci run: npm ci
@@ -29,4 +26,4 @@ jobs:
run: npm run ts:lint run: npm run ts:lint
- name: Run JavaScript tests - name: Run JavaScript tests
run: npm run test:ci run: npm run test

View File

@@ -1,7 +1,6 @@
name: test-migrations name: test-migrations
on: on:
workflow_dispatch:
push: push:
paths: paths:
- '**.php' - '**.php'
@@ -14,25 +13,15 @@ on:
jobs: jobs:
build: build:
if: ${{ github.ref != 'refs/heads/l10n_development' }} if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: docker runs-on: ubuntu-24.04
container:
image: docker.io/library/node:24-trixie
strategy: strategy:
matrix: matrix:
php: ['8.2', '8.3', '8.4', '8.5'] php: ['8.2', '8.3', '8.4', '8.5']
services:
mysql:
image: docker.io/library/mariadb:12.2.2-noble
env:
MARIADB_USER: bookstack-test
MARIADB_PASSWORD: bookstack-test
MARIADB_DATABASE: bookstack-test
MARIADB_ROOT_PASSWORD: password
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v6 - uses: actions/checkout@v4
- name: Setup PHP - name: Setup PHP
uses: https://github.com/shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
extensions: gd, mbstring, json, curl, xml, mysql, ldap extensions: gd, mbstring, json, curl, xml, mysql, ldap
@@ -43,31 +32,34 @@ jobs:
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer packages - name: Cache composer packages
uses: https://code.forgejo.org/actions/cache@v5 uses: actions/cache@v4
with: with:
path: ${{ steps.composer-cache.outputs.dir }} path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }} key: ${{ runner.os }}-composer-${{ matrix.php }}
restore-keys: ${{ runner.os }}-composer- restore-keys: ${{ runner.os }}-composer-
- name: Start MySQL
run: |
sudo systemctl start mysql
- name: Create database & user
run: |
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';"
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
- name: Install composer dependencies - name: Install composer dependencies
run: composer install --prefer-dist --no-interaction --ansi run: composer install --prefer-dist --no-interaction --ansi
env:
COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GH_TOKEN }}"}}'
- name: Start migration test - name: Start migration test
env:
TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test'
run: | run: |
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
- name: Start migration:rollback test - name: Start migration:rollback test
env:
TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test'
run: | run: |
php${{ matrix.php }} artisan migrate:rollback --force -n --database=mysql_testing php${{ matrix.php }} artisan migrate:rollback --force -n --database=mysql_testing
- name: Start migration rerun test - name: Start migration rerun test
env:
TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test'
run: | run: |
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing

View File

@@ -1,7 +1,6 @@
name: test-php name: test-php
on: on:
workflow_dispatch:
push: push:
paths: paths:
- '**.php' - '**.php'
@@ -14,38 +13,18 @@ on:
jobs: jobs:
build: build:
if: ${{ github.ref != 'refs/heads/l10n_development' }} if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: docker runs-on: ubuntu-24.04
container:
image: docker.io/setupphp/node:noble
strategy: strategy:
matrix: matrix:
php: ['8.2', '8.3', '8.4', '8.5'] php: ['8.2', '8.3', '8.4', '8.5']
env:
phpextensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp
phpextensioncachekey: cache-v1
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v6 - uses: actions/checkout@v4
- name: Setup cache environment
id: extcache
uses: https://github.com/shivammathur/cache-extensions@v1
with:
php-version: ${{ matrix.php }}
extensions: ${{ env.phpextensions }}
key: ${{ env.phpextensioncachekey }}
- name: Cache extensions
uses: https://code.forgejo.org/actions/cache@v5
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Setup PHP - name: Setup PHP
uses: https://github.com/shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
extensions: ${{ env.phpextensions }} extensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp
- name: Get Composer Cache Directory - name: Get Composer Cache Directory
id: composer-cache id: composer-cache
@@ -53,36 +32,30 @@ jobs:
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer packages - name: Cache composer packages
uses: https://code.forgejo.org/actions/cache@v5 uses: actions/cache@v4
with: with:
path: ${{ steps.composer-cache.outputs.dir }} path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }} key: ${{ runner.os }}-composer-${{ matrix.php }}
restore-keys: ${{ runner.os }}-composer- restore-keys: ${{ runner.os }}-composer-
- name: Install composer dependencies - name: Start Database
run: composer install --prefer-dist --no-interaction --ansi
env:
COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GH_TOKEN }}"}}'
- name: Start MySQL
run: | run: |
sudo systemctl start mysql sudo systemctl start mysql
- name: Create database & user - name: Setup Database
run: | run: |
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;' mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';" mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';"
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';" mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
mysql -uroot -proot -e 'FLUSH PRIVILEGES;' mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
- name: Install composer dependencies
run: composer install --prefer-dist --no-interaction --ansi
- name: Migrate and seed the database - name: Migrate and seed the database
env:
TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@localhost/bookstack-test'
run: | run: |
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
php${{ matrix.php }} artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing php${{ matrix.php }} artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
- name: Run PHP tests - name: Run PHP tests
env:
TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@localhost/bookstack-test'
run: php${{ matrix.php }} ./vendor/bin/phpunit run: php${{ matrix.php }} ./vendor/bin/phpunit

View File

@@ -22,10 +22,4 @@ trait HandlesPartialLogins
return $user; return $user;
} }
protected function clearLastAttemptedUser(): void
{
$loginService = app()->make(LoginService::class);
$loginService->clearLastLoginAttempted();
}
} }

View File

@@ -6,7 +6,6 @@ use BookStack\Access\LoginService;
use BookStack\Access\Mfa\BackupCodeService; use BookStack\Access\Mfa\BackupCodeService;
use BookStack\Access\Mfa\MfaSession; use BookStack\Access\Mfa\MfaSession;
use BookStack\Access\Mfa\MfaValue; use BookStack\Access\Mfa\MfaValue;
use BookStack\Access\Mfa\MfaVerificationLimiter;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controller; use BookStack\Http\Controller;
@@ -20,11 +19,6 @@ class MfaBackupCodesController extends Controller
protected const SETUP_SECRET_SESSION_KEY = 'mfa-setup-backup-codes'; protected const SETUP_SECRET_SESSION_KEY = 'mfa-setup-backup-codes';
public function __construct(
protected MfaVerificationLimiter $limiter,
) {
}
/** /**
* Show a view that generates and displays backup codes. * Show a view that generates and displays backup codes.
*/ */
@@ -77,12 +71,6 @@ class MfaBackupCodesController extends Controller
public function verify(Request $request, BackupCodeService $codeService, MfaSession $mfaSession, LoginService $loginService) public function verify(Request $request, BackupCodeService $codeService, MfaSession $mfaSession, LoginService $loginService)
{ {
$user = $this->currentOrLastAttemptedUser(); $user = $this->currentOrLastAttemptedUser();
$this->limiter->incrementAttempts($user, $request);
if ($this->limiter->hasHitLimit($user, $request)) {
$this->clearLastAttemptedUser();
$this->limiter->throwException();
}
$codes = MfaValue::getValueForUser($user, MfaValue::METHOD_BACKUP_CODES) ?? '[]'; $codes = MfaValue::getValueForUser($user, MfaValue::METHOD_BACKUP_CODES) ?? '[]';
$this->validate($request, [ $this->validate($request, [
@@ -101,7 +89,6 @@ class MfaBackupCodesController extends Controller
$mfaSession->markVerifiedForUser($user); $mfaSession->markVerifiedForUser($user);
$loginService->reattemptLoginFor($user); $loginService->reattemptLoginFor($user);
$this->limiter->decrementAttempts($user, $request);
if ($codeService->countCodesInSet($updatedCodes) < 5) { if ($codeService->countCodesInSet($updatedCodes) < 5) {
$this->showWarningNotification(trans('auth.mfa_backup_codes_usage_limit_warning')); $this->showWarningNotification(trans('auth.mfa_backup_codes_usage_limit_warning'));

View File

@@ -5,7 +5,6 @@ namespace BookStack\Access\Controllers;
use BookStack\Access\LoginService; use BookStack\Access\LoginService;
use BookStack\Access\Mfa\MfaSession; use BookStack\Access\Mfa\MfaSession;
use BookStack\Access\Mfa\MfaValue; use BookStack\Access\Mfa\MfaValue;
use BookStack\Access\Mfa\MfaVerificationLimiter;
use BookStack\Access\Mfa\TotpService; use BookStack\Access\Mfa\TotpService;
use BookStack\Access\Mfa\TotpValidationRule; use BookStack\Access\Mfa\TotpValidationRule;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
@@ -21,8 +20,7 @@ class MfaTotpController extends Controller
protected const SETUP_SECRET_SESSION_KEY = 'mfa-setup-totp-secret'; protected const SETUP_SECRET_SESSION_KEY = 'mfa-setup-totp-secret';
public function __construct( public function __construct(
protected TotpService $totp, protected TotpService $totp
protected MfaVerificationLimiter $limiter,
) { ) {
} }
@@ -88,12 +86,6 @@ class MfaTotpController extends Controller
public function verify(Request $request, LoginService $loginService, MfaSession $mfaSession) public function verify(Request $request, LoginService $loginService, MfaSession $mfaSession)
{ {
$user = $this->currentOrLastAttemptedUser(); $user = $this->currentOrLastAttemptedUser();
$this->limiter->incrementAttempts($user, $request);
if ($this->limiter->hasHitLimit($user, $request)) {
$this->clearLastAttemptedUser();
$this->limiter->throwException();
}
$totpSecret = MfaValue::getValueForUser($user, MfaValue::METHOD_TOTP); $totpSecret = MfaValue::getValueForUser($user, MfaValue::METHOD_TOTP);
$this->validate($request, [ $this->validate($request, [
@@ -106,7 +98,6 @@ class MfaTotpController extends Controller
$mfaSession->markVerifiedForUser($user); $mfaSession->markVerifiedForUser($user);
$loginService->reattemptLoginFor($user); $loginService->reattemptLoginFor($user);
$this->limiter->decrementAttempts($user, $request);
return redirect()->intended(); return redirect()->intended();
} }

View File

@@ -55,6 +55,7 @@ class OidcController extends Controller
} }
try { try {
$this->throwIfAuthorizationError($request);
$this->oidcService->processAuthorizeResponse($request->query('code')); $this->oidcService->processAuthorizeResponse($request->query('code'));
} catch (OidcException $oidcException) { } catch (OidcException $oidcException) {
$this->showErrorNotification($oidcException->getMessage()); $this->showErrorNotification($oidcException->getMessage());
@@ -72,4 +73,23 @@ class OidcController extends Controller
{ {
return redirect($this->oidcService->logout()); return redirect($this->oidcService->logout());
} }
/**
*
* @throws OidcException
*/
private function throwIfAuthorizationError(Request $request): void
{
$errorCode = $request->query('error');
if (!$errorCode) {
return;
}
$errorDescription = $request->query('error_description');
if ($errorDescription) {
throw new OidcException($errorDescription);
}
throw new OidcException(trans('errors.oidc_fail_authed', ['system' => config('oidc.name')]));
}
} }

View File

@@ -42,7 +42,7 @@ class ExternalBaseUserProvider implements UserProvider
/** /**
* Retrieve a user by the given credentials. * Retrieve a user by the given credentials.
*/ */
public function retrieveByCredentials(array $credentials): ?User public function retrieveByCredentials(array $credentials): ?Authenticatable
{ {
return User::query() return User::query()
->where('external_auth_id', $credentials['external_auth_id']) ->where('external_auth_id', $credentials['external_auth_id'])

View File

@@ -50,12 +50,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
/** /**
* Create a new authentication guard. * Create a new authentication guard.
*/ */
public function __construct( public function __construct(string $name, UserProvider $provider, Session $session, RegistrationService $registrationService)
string $name, {
UserProvider $provider,
Session $session,
RegistrationService $registrationService
) {
$this->name = $name; $this->name = $name;
$this->session = $session; $this->session = $session;
$this->provider = $provider; $this->provider = $provider;

View File

@@ -83,10 +83,6 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
} }
} }
if (!($user instanceof User)) {
throw new LoginAttemptException('Could not find or create a user for LDAP login.');
}
// Sync LDAP groups if required // Sync LDAP groups if required
if ($this->ldapService->shouldSyncGroups()) { if ($this->ldapService->shouldSyncGroups()) {
$this->ldapService->syncGroups($user, $username); $this->ldapService->syncGroups($user, $username);

View File

@@ -126,7 +126,7 @@ class LoginService
/** /**
* Clear the last login attempted session value. * Clear the last login attempted session value.
*/ */
public function clearLastLoginAttempted(): void protected function clearLastLoginAttempted(): void
{ {
session()->remove(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY); session()->remove(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
} }

View File

@@ -1,62 +0,0 @@
<?php
namespace BookStack\Access\Mfa;
use BookStack\Exceptions\NotifyException;
use BookStack\Users\Models\User;
use Illuminate\Cache\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
/**
* A rate limit specifically for MFA verification.
* Limits across both the attempted user (on a tight limit) and the
* request IP (on a less strict limit).
*/
class MfaVerificationLimiter
{
protected int $maxUserAttemptsPerMinute = 5;
protected int $maxIpAttemptsPerMinute = 60;
public function __construct(
protected RateLimiter $rateLimiter
) {
}
public function throwException(): never
{
throw new NotifyException(
trans('auth.mfa_throttle', ['seconds' => 60]),
'/login',
Response::HTTP_TOO_MANY_REQUESTS
);
}
public function incrementAttempts(User $user, Request $request): void
{
$this->rateLimiter->hit($this->getUserKey($user));
$this->rateLimiter->hit($this->getRequestKey($request));
}
public function decrementAttempts(User $user, Request $request): void
{
$this->rateLimiter->decrement($this->getUserKey($user));
$this->rateLimiter->decrement($this->getRequestKey($request));
}
public function hasHitLimit(User $user, Request $request): bool
{
return $this->rateLimiter->tooManyAttempts($this->getUserKey($user), $this->maxUserAttemptsPerMinute + 1)
|| $this->rateLimiter->tooManyAttempts($this->getRequestKey($request), $this->maxIpAttemptsPerMinute + 1);
}
protected function getUserKey(User $user): string
{
return "mfa-attempt::user::{$user->id}";
}
protected function getRequestKey(Request $request): string
{
return "mfa-attempt::request::{$request->ip()}";
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace BookStack\Access\Oidc;
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
/**
* Option provider that sends credentials via HTTP Basic Auth header
* and also includes client_id in the request body.
*/
class OidcHttpBasicWithClientIdOptionProvider extends HttpBasicAuthOptionProvider
{
public function getAccessTokenOptions($method, array $params)
{
$clientId = $params['client_id'] ?? null;
$options = parent::getAccessTokenOptions($method, $params);
if ($clientId) {
parse_str($options['body'] ?? '', $body);
$body['client_id'] = $clientId;
$options['body'] = http_build_query($body);
}
return $options;
}
}

View File

@@ -9,10 +9,10 @@ class OidcIdToken extends OidcJwtWithClaims implements ProvidesClaims
* *
* @throws OidcInvalidTokenException * @throws OidcInvalidTokenException
*/ */
public function validate(string $clientId): bool public function validate(OidcProviderSettings $settings): bool
{ {
parent::validateCommonTokenDetails($clientId); parent::validateCommonTokenDetails($settings);
$this->validateTokenClaims($clientId); $this->validateTokenClaims($settings->clientId);
return true; return true;
} }

View File

@@ -9,7 +9,9 @@ class OidcJwtWithClaims implements ProvidesClaims
protected string $signature; protected string $signature;
protected string $issuer; protected string $issuer;
protected array $tokenParts = []; protected array $tokenParts = [];
protected array $acceptedSignatures = [self::hs256Signature, self::rs256Signature];
private const hs256Signature = 'HS256'
, rs256Signature = 'RS256';
/** /**
* @var array[]|string[] * @var array[]|string[]
*/ */
@@ -59,11 +61,11 @@ class OidcJwtWithClaims implements ProvidesClaims
* *
* @throws OidcInvalidTokenException * @throws OidcInvalidTokenException
*/ */
public function validateCommonTokenDetails(string $clientId): bool public function validateCommonTokenDetails(OidcProviderSettings $settings): bool
{ {
$this->validateTokenStructure(); $this->validateTokenStructure();
$this->validateTokenSignature(); $this->validateTokenSignature($settings);
$this->validateCommonClaims($clientId); $this->validateCommonClaims($settings->clientId);
return true; return true;
} }
@@ -102,12 +104,12 @@ class OidcJwtWithClaims implements ProvidesClaims
protected function validateTokenStructure(): void protected function validateTokenStructure(): void
{ {
foreach (['header', 'payload'] as $prop) { foreach (['header', 'payload'] as $prop) {
if (empty($this->$prop)) { if (empty($this->$prop) || !is_array($this->$prop)) {
throw new OidcInvalidTokenException("Could not parse out a valid {$prop} within the provided token"); throw new OidcInvalidTokenException("Could not parse out a valid {$prop} within the provided token");
} }
} }
if (empty($this->signature)) { if (empty($this->signature) || !is_string($this->signature)) {
throw new OidcInvalidTokenException('Could not parse out a valid signature within the provided token'); throw new OidcInvalidTokenException('Could not parse out a valid signature within the provided token');
} }
} }
@@ -117,29 +119,42 @@ class OidcJwtWithClaims implements ProvidesClaims
* *
* @throws OidcInvalidTokenException * @throws OidcInvalidTokenException
*/ */
protected function validateTokenSignature(): void protected function validateTokenSignature(OidcProviderSettings $settings): void {
{ $validSignatures = implode(', ',$this->acceptedSignatures);
if ($this->header['alg'] !== 'RS256') { switch ($this->header['alg']) {
throw new OidcInvalidTokenException("Only RS256 signature validation is supported. Token reports using {$this->header['alg']}"); case self::rs256Signature:
$parsedKeys = array_map(function ($key) {
try {
return new OidcJwtSigningKey($key);
} catch (OidcInvalidKeyException $e) {
throw new OidcInvalidTokenException('Failed to read signing key with error: ' . $e->getMessage());
}
}, $this->keys);
$parsedKeys = array_filter($parsedKeys);
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
/** @var OidcJwtSigningKey $parsedKey */
foreach ($parsedKeys as $parsedKey) {
if ($parsedKey->verify($contentToSign, $this->signature)) {
return;
}
}
throw new OidcInvalidTokenException('Token signature could not be validated using the provided keys');
case self::hs256Signature:
$secret = $settings->clientSecret;
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
$expectedSignature = hash_hmac('sha256', $contentToSign, $secret, true);
if (hash_equals($expectedSignature, $this->signature)) {
return;
}
throw new OidcInvalidTokenException('Token signature could not be validated using the provided secret');
default:
throw new OidcInvalidTokenException("Only $validSignatures signatures validation are supported. Token reports using {$this->header['alg']}");
} }
$parsedKeys = array_map(function ($key) {
try {
return new OidcJwtSigningKey($key);
} catch (OidcInvalidKeyException $e) {
throw new OidcInvalidTokenException('Failed to read signing key with error: ' . $e->getMessage());
}
}, $this->keys);
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
/** @var OidcJwtSigningKey $parsedKey */
foreach ($parsedKeys as $parsedKey) {
if ($parsedKey->verify($contentToSign, $this->signature)) {
return;
}
}
throw new OidcInvalidTokenException('Token signature could not be validated using the provided keys');
} }
/** /**

View File

@@ -74,7 +74,7 @@ class OidcProviderSettings
{ {
$this->validateInitial(); $this->validateInitial();
$required = ['keys', 'tokenEndpoint', 'authorizationEndpoint']; $required = ['tokenEndpoint', 'authorizationEndpoint'];
foreach ($required as $prop) { foreach ($required as $prop) {
if (empty($this->$prop)) { if (empty($this->$prop)) {
throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value"); throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value");

View File

@@ -14,7 +14,6 @@ use BookStack\Theming\ThemeEvents;
use BookStack\Uploads\UserAvatars; use BookStack\Uploads\UserAvatars;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
/** /**
@@ -80,7 +79,6 @@ class OidcService
$provider->setPkceCode($pkceCode); $provider->setPkceCode($pkceCode);
// Try to exchange authorization code for access token // Try to exchange authorization code for access token
/** @var OidcAccessToken $accessToken */
$accessToken = $provider->getAccessToken('authorization_code', [ $accessToken = $provider->getAccessToken('authorization_code', [
'code' => $authorizationCode, 'code' => $authorizationCode,
]); ]);
@@ -141,7 +139,7 @@ class OidcService
'redirectUri' => url('/oidc/callback'), 'redirectUri' => url('/oidc/callback'),
], [ ], [
'httpClient' => $this->http->buildClient(5), 'httpClient' => $this->http->buildClient(5),
'optionProvider' => new HttpBasicAuthOptionProvider(), 'optionProvider' => new OidcHttpBasicWithClientIdOptionProvider(),
]); ]);
foreach ($this->getAdditionalScopes() as $scope) { foreach ($this->getAdditionalScopes() as $scope) {
@@ -200,7 +198,7 @@ class OidcService
} }
try { try {
$idToken->validate($settings->clientId); $idToken->validate($settings);
} catch (OidcInvalidTokenException $exception) { } catch (OidcInvalidTokenException $exception) {
throw new OidcException("ID token validation failed with error: {$exception->getMessage()}"); throw new OidcException("ID token validation failed with error: {$exception->getMessage()}");
} }

View File

@@ -11,7 +11,6 @@ use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;
@@ -28,7 +27,6 @@ class ActivityQueries
*/ */
public function latest(int $count = 20, int $page = 0): array public function latest(int $count = 20, int $page = 0): array
{ {
/** @var Collection<int, Activity> $activityList */
$activityList = $this->permissions $activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type') ->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
@@ -85,7 +83,6 @@ class ActivityQueries
*/ */
public function userActivity(User $user, int $count = 20, int $page = 0): array public function userActivity(User $user, int $count = 20, int $page = 0): array
{ {
/** @var Collection<int, Activity> $activityList */
$activityList = $this->permissions $activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type') ->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')

View File

@@ -46,7 +46,6 @@ class ActivityType
const USER_CREATE = 'user_create'; const USER_CREATE = 'user_create';
const USER_UPDATE = 'user_update'; const USER_UPDATE = 'user_update';
const USER_DELETE = 'user_delete'; const USER_DELETE = 'user_delete';
const USER_MFA_RESET = 'user_mfa_reset';
const API_TOKEN_CREATE = 'api_token_create'; const API_TOKEN_CREATE = 'api_token_create';
const API_TOKEN_UPDATE = 'api_token_update'; const API_TOKEN_UPDATE = 'api_token_update';

View File

@@ -3,7 +3,6 @@
namespace BookStack\Activity\Models; namespace BookStack\Activity\Models;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Entities\Models\Page;
use BookStack\Permissions\Models\JointPermission; use BookStack\Permissions\Models\JointPermission;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\HasCreatorAndUpdater; use BookStack\Users\Models\HasCreatorAndUpdater;
@@ -41,9 +40,6 @@ class Comment extends Model implements Loggable, OwnableInterface
/** /**
* Get the entity that this comment belongs to. * Get the entity that this comment belongs to.
* It's only pages right now hence the typing below.
* Would need a deeper audit if that changes as many areas assume this is always a page.
* @return MorphTo<Page, $this>
*/ */
public function entity(): MorphTo public function entity(): MorphTo
{ {
@@ -59,9 +55,7 @@ class Comment extends Model implements Loggable, OwnableInterface
// Ultimately, we could just align the method name to 'commentable' but that would be a potential // Ultimately, we could just align the method name to 'commentable' but that would be a potential
// breaking change and not really worthwhile in a patch due to the risk of creating extra problems. // breaking change and not really worthwhile in a patch due to the risk of creating extra problems.
/** @var MorphTo<Page, $this> $relation */ return $this->morphTo(null, 'commentable_type', 'commentable_id');
$relation = $this->morphTo(null, 'commentable_type', 'commentable_id');
return $relation;
} }
/** /**

View File

@@ -73,17 +73,13 @@ class WebhookFormatter
// Load entity owner, creator, updater details // Load entity owner, creator, updater details
$this->addModelFormatter( $this->addModelFormatter(
fn ($event, $model) => ($model instanceof Entity), fn ($event, $model) => ($model instanceof Entity),
function ($model) { fn ($model) => $model->load(['ownedBy', 'createdBy', 'updatedBy'])
$model->load(['ownedBy', 'createdBy', 'updatedBy']);
}
); );
// Load revision detail for page update and create events // Load revision detail for page update and create events
$this->addModelFormatter( $this->addModelFormatter(
fn ($event, $model) => ($model instanceof Page && ($event === ActivityType::PAGE_CREATE || $event === ActivityType::PAGE_UPDATE)), fn ($event, $model) => ($model instanceof Page && ($event === ActivityType::PAGE_CREATE || $event === ActivityType::PAGE_UPDATE)),
function ($model) { fn ($model) => $model->load('currentRevision')
$model->load('currentRevision');
}
); );
} }

View File

@@ -138,8 +138,8 @@ class ApiDocsGenerator
return $validation; return $validation;
} }
if (is_object($validation) && $validation instanceof \Stringable) { if (is_object($validation) && method_exists($validation, '__toString')) {
return $validation->__toString(); return strval($validation);
} }
if ($validation instanceof Password) { if ($validation instanceof Password) {

View File

@@ -7,7 +7,6 @@ use BookStack\Theming\ThemeService;
use BookStack\Theming\ThemeViews; use BookStack\Theming\ThemeViews;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\View\FileViewFinder;
class ThemeServiceProvider extends ServiceProvider class ThemeServiceProvider extends ServiceProvider
{ {
@@ -28,11 +27,7 @@ class ThemeServiceProvider extends ServiceProvider
// Boot up the theme system // Boot up the theme system
$themeService = $this->app->make(ThemeService::class); $themeService = $this->app->make(ThemeService::class);
$viewFactory = $this->app->make('view'); $viewFactory = $this->app->make('view');
$viewFinder = $viewFactory->getFinder(); $themeViews = new ThemeViews($viewFactory->getFinder());
if (!($viewFinder instanceof FileViewFinder)) {
throw new \Exception('Only the file view finder is supported for the theme system');
}
$themeViews = new ThemeViews($viewFinder);
// Use a custom include so that we can insert theme views before/after includes. // Use a custom include so that we can insert theme views before/after includes.
// This is done, even if no theme is active, so that view caching does not create problems // This is done, even if no theme is active, so that view caching does not create problems

View File

@@ -10,7 +10,7 @@ class PwaManifestBuilder
// does not start a session, so we won't have current user context. // does not start a session, so we won't have current user context.
// This was attempted but removed since manifest calls could affect user session // This was attempted but removed since manifest calls could affect user session
// history tracking and back redirection. // history tracking and back redirection.
// Context: https://codeberg.org/bookstack/bookstack/issues/4649 // Context: https://github.com/BookStackApp/BookStack/issues/4649
$darkMode = (bool) setting()->getForCurrentUser('dark-mode-enabled'); $darkMode = (bool) setting()->getForCurrentUser('dark-mode-enabled');
$appName = setting('app-name'); $appName = setting('app-name');

View File

@@ -6,7 +6,6 @@ use BookStack\Facades\Theme;
use BookStack\Permissions\Permission; use BookStack\Permissions\Permission;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Settings\SettingService; use BookStack\Settings\SettingService;
use BookStack\Users\Models\OwnableInterface;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
/** /**
@@ -41,7 +40,7 @@ function user(): User
* Check if the current user has a permission. If an ownable element * Check if the current user has a permission. If an ownable element
* is passed in the jointPermissions are checked against that particular item. * is passed in the jointPermissions are checked against that particular item.
*/ */
function userCan(string|Permission $permission, (Model&OwnableInterface)|null $ownable = null): bool function userCan(string|Permission $permission, ?Model $ownable = null): bool
{ {
if (is_null($ownable)) { if (is_null($ownable)) {
return user()->can($permission); return user()->can($permission);

View File

@@ -72,17 +72,6 @@ return [
// Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured. // 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'), 'iframe_sources' => env('ALLOWED_IFRAME_SOURCES', 'https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com'),
// A list of style sources/hostnames that can be loaded styles within BookStack.
// Space separated if multiple. BookStack host domain is auto-inferred.
// If not set, a permissive default set is used to reduce potential breakage.
'style_sources' => env('ALLOWED_STYLE_SOURCES', null),
// A list of sources/hostnames that can be loaded as image content within BookStack.
// Space separated if multiple. BookStack host domain is auto-inferred, in addition to
// data and blob images, due to their use for various functionality.
// If not set, a permissive default set is used to reduce potential breakage.
'image_sources' => env('ALLOWED_IMAGE_SOURCES', null),
// A list of the sources/hostnames that can be reached by application SSR calls. // A list of the sources/hostnames that can be reached by application SSR calls.
// This is used wherever users can provide URLs/hosts in-platform, like for webhooks. // This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
// Host-specific functionality (usually controlled via other options) like auth // Host-specific functionality (usually controlled via other options) like auth

View File

@@ -81,6 +81,7 @@ return [
'strict' => false, 'strict' => false,
'engine' => null, 'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([ 'options' => extension_loaded('pdo_mysql') ? array_filter([
// @phpstan-ignore class.notFound
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [], ]) : [],
], ],

View File

@@ -68,7 +68,7 @@ return [
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic, * Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
* Symbol, ZapfDingbats. * Symbol, ZapfDingbats.
*/ */
'font_dir' => storage_path('fonts/dompdf'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782) 'font_dir' => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
/** /**
* The location of the DOMPDF font cache directory. * The location of the DOMPDF font cache directory.
@@ -78,7 +78,7 @@ return [
* *
* Note: This directory must exist and be writable by the webserver process. * Note: This directory must exist and be writable by the webserver process.
*/ */
'font_cache' => storage_path('fonts/dompdf/cache'), 'font_cache' => storage_path('fonts/'),
/** /**
* The location of a temporary directory. * The location of a temporary directory.

View File

@@ -1,7 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Activity\Tools\CommentTree; use BookStack\Activity\Tools\CommentTree;
@@ -12,9 +10,7 @@ use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController; use BookStack\Http\ApiController;
use BookStack\Permissions\Permission; use BookStack\Permissions\Permission;
use Exception; use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
class PageApiController extends ApiController class PageApiController extends ApiController
{ {
@@ -49,7 +45,7 @@ class PageApiController extends ApiController
/** /**
* Get a listing of pages visible to the user. * Get a listing of pages visible to the user.
*/ */
public function list(): JsonResponse public function list()
{ {
$pages = $this->queries->visibleForList() $pages = $this->queries->visibleForList()
->addSelect(['created_by', 'updated_by', 'revision_count', 'editor']); ->addSelect(['created_by', 'updated_by', 'revision_count', 'editor']);
@@ -73,7 +69,7 @@ class PageApiController extends ApiController
* Any images included via base64 data URIs will be extracted and saved as gallery * Any images included via base64 data URIs will be extracted and saved as gallery
* images against the page during upload. * images against the page during upload.
*/ */
public function create(Request $request): JsonResponse public function create(Request $request)
{ {
$this->validate($request, $this->rules['create']); $this->validate($request, $this->rules['create']);
@@ -106,9 +102,9 @@ class PageApiController extends ApiController
* Comments for the page are provided in a tree-structure representing the hierarchy of top-level * Comments for the page are provided in a tree-structure representing the hierarchy of top-level
* comments and replies, for both archived and active comments. * comments and replies, for both archived and active comments.
*/ */
public function read(string $id): JsonResponse public function read(string $id)
{ {
$page = $this->queries->findVisibleByIdOrFail(intval($id)); $page = $this->queries->findVisibleByIdOrFail($id);
$page = $page->forJsonDisplay(); $page = $page->forJsonDisplay();
$commentTree = (new CommentTree($page)); $commentTree = (new CommentTree($page));
@@ -128,11 +124,11 @@ class PageApiController extends ApiController
* Providing a 'book_id' or 'chapter_id' property will essentially move * Providing a 'book_id' or 'chapter_id' property will essentially move
* the page into that parent element if you have permissions to do so. * the page into that parent element if you have permissions to do so.
*/ */
public function update(Request $request, string $id): JsonResponse public function update(Request $request, string $id)
{ {
$requestData = $this->validate($request, $this->rules['update']); $requestData = $this->validate($request, $this->rules['update']);
$page = $this->queries->findVisibleByIdOrFail(intval($id)); $page = $this->queries->findVisibleByIdOrFail($id);
$this->checkOwnablePermission(Permission::PageUpdate, $page); $this->checkOwnablePermission(Permission::PageUpdate, $page);
$parent = null; $parent = null;
@@ -165,9 +161,9 @@ class PageApiController extends ApiController
* Delete a page. * Delete a page.
* This will typically send the page to the recycle bin. * This will typically send the page to the recycle bin.
*/ */
public function delete(string $id): Response public function delete(string $id)
{ {
$page = $this->queries->findVisibleByIdOrFail(intval($id)); $page = $this->queries->findVisibleByIdOrFail($id);
$this->checkOwnablePermission(Permission::PageDelete, $page); $this->checkOwnablePermission(Permission::PageDelete, $page);
$this->pageRepo->destroy($page); $this->pageRepo->destroy($page);

View File

@@ -25,7 +25,6 @@ use BookStack\Util\HtmlContentFilter;
use BookStack\Util\HtmlContentFilterConfig; use BookStack\Util\HtmlContentFilterConfig;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Throwable; use Throwable;
@@ -359,8 +358,8 @@ class PageController extends Controller
*/ */
public function showRecentlyUpdated() public function showRecentlyUpdated()
{ {
$visibleBelongsScope = function (Relation $relation): void { $visibleBelongsScope = function (BelongsTo $query) {
$relation->scopes('visible'); $query->scopes('visible');
}; };
$pages = $this->queries->visibleForList() $pages = $this->queries->visibleForList()

View File

@@ -177,7 +177,7 @@ class PageRevisionController extends Controller
*/ */
public function destroyUserDraft(string $pageId) public function destroyUserDraft(string $pageId)
{ {
$page = $this->pageQueries->findVisibleByIdOrFail(intval($pageId)); $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->revisionRepo->deleteDraftsForCurrentUser($page); $this->revisionRepo->deleteDraftsForCurrentUser($page);
return response('', 200); return response('', 200);

View File

@@ -20,10 +20,10 @@ use Illuminate\Support\Collection;
* @property ?int $image_id * @property ?int $image_id
* @property ?int $default_template_id * @property ?int $default_template_id
* @property ?int $sort_rule_id * @property ?int $sort_rule_id
* @property \Illuminate\Database\Eloquent\Collection<int, Chapter> $chapters * @property \Illuminate\Database\Eloquent\Collection $chapters
* @property \Illuminate\Database\Eloquent\Collection<int, Page> $pages * @property \Illuminate\Database\Eloquent\Collection $pages
* @property \Illuminate\Database\Eloquent\Collection<int, Page> $directPages * @property \Illuminate\Database\Eloquent\Collection $directPages
* @property \Illuminate\Database\Eloquent\Collection<int, Bookshelf> $shelves * @property \Illuminate\Database\Eloquent\Collection $shelves
* @property ?SortRule $sortRule * @property ?SortRule $sortRule
*/ */
class Book extends Entity implements HasDescriptionInterface, HasCoverInterface, HasDefaultTemplateInterface class Book extends Entity implements HasDescriptionInterface, HasCoverInterface, HasDefaultTemplateInterface

View File

@@ -46,7 +46,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int|null $created_by * @property int|null $created_by
* @property int|null $updated_by * @property int|null $updated_by
* @property int|null $owned_by * @property int|null $owned_by
* @property Collection<int, Tag> $tags * @property Collection $tags
* *
* @method static Entity|Builder visible() * @method static Entity|Builder visible()
* @method static Builder withLastView() * @method static Builder withLastView()

View File

@@ -24,8 +24,8 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
* @property int $revision_count * @property int $revision_count
* @property string $editor * @property string $editor
* @property Chapter|null $chapter * @property Chapter|null $chapter
* @property Collection<int, Attachment> $attachments * @property Collection $attachments
* @property Collection<int, PageRevision> $revisions * @property Collection $revisions
* @property PageRevision $currentRevision * @property PageRevision $currentRevision
*/ */
class Page extends BookChild class Page extends BookChild

View File

@@ -6,7 +6,6 @@ use BookStack\Activity\Models\View;
use BookStack\Entities\EntityProvider; use BookStack\Entities\EntityProvider;
use BookStack\Entities\Tools\MixedEntityListLoader; use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -21,7 +20,6 @@ class QueryPopular
public function run(int $count, int $page, array $filterModels): Collection public function run(int $count, int $page, array $filterModels): Collection
{ {
/** @var Builder<View> $query */
$query = $this->permissions $query = $this->permissions
->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type') ->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type')
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count')) ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))

View File

@@ -5,7 +5,6 @@ namespace BookStack\Entities\Queries;
use BookStack\Activity\Models\View; use BookStack\Activity\Models\View;
use BookStack\Entities\Tools\MixedEntityListLoader; use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class QueryRecentlyViewed class QueryRecentlyViewed
@@ -23,7 +22,6 @@ class QueryRecentlyViewed
return collect(); return collect();
} }
/** @var Builder<View> $query */
$query = $this->permissions->restrictEntityRelationQuery( $query = $this->permissions->restrictEntityRelationQuery(
View::query(), View::query(),
'views', 'views',

View File

@@ -5,7 +5,6 @@ namespace BookStack\Entities\Queries;
use BookStack\Activity\Models\Favourite; use BookStack\Activity\Models\Favourite;
use BookStack\Entities\Tools\MixedEntityListLoader; use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
class QueryTopFavourites class QueryTopFavourites
@@ -23,7 +22,6 @@ class QueryTopFavourites
return collect(); return collect();
} }
/** @var Builder<Favourite> $query */
$query = $this->permissions $query = $this->permissions
->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type') ->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type')
->select('favourites.*') ->select('favourites.*')

View File

@@ -25,13 +25,9 @@ class CustomListItemRenderer implements NodeRendererInterface
*/ */
public function render(Node $node, ChildNodeRendererInterface $childRenderer) public function render(Node $node, ChildNodeRendererInterface $childRenderer)
{ {
if (!($node instanceof ListItem)) {
return null;
}
$listItem = $this->baseRenderer->render($node, $childRenderer); $listItem = $this->baseRenderer->render($node, $childRenderer);
if ($this->startsTaskListItem($node) && $listItem instanceof HtmlElement) { if ($node instanceof ListItem && $this->startsTaskListItem($node) && $listItem instanceof HtmlElement) {
$listItem->setAttribute('class', 'task-list-item'); $listItem->setAttribute('class', 'task-list-item');
} }

View File

@@ -421,41 +421,28 @@ class PageContent
*/ */
protected function headerNodesToLevelList(DOMNodeList $nodeList): array protected function headerNodesToLevelList(DOMNodeList $nodeList): array
{ {
$minLevel = 6; $tree = collect($nodeList)->map(function (DOMElement $header) {
$headerDetails = array_map(function (DOMNode $header) use (&$minLevel) {
if (!$header instanceof DOMElement) {
return null;
}
$text = trim(str_replace("\xc2\xa0", ' ', $header->nodeValue)); $text = trim(str_replace("\xc2\xa0", ' ', $header->nodeValue));
$text = mb_substr($text, 0, 100); $text = mb_substr($text, 0, 100);
if (empty($text)) {
return null;
}
$level = intval(str_replace('h', '', $header->nodeName));
if ($level < $minLevel) {
$minLevel = $level;
}
return [ return [
'nodeName' => strtolower($header->nodeName), 'nodeName' => strtolower($header->nodeName),
'level' => $level, 'level' => intval(str_replace('h', '', $header->nodeName)),
'link' => '#' . $header->getAttribute('id'), 'link' => '#' . $header->getAttribute('id'),
'text' => $text, 'text' => $text,
]; ];
}, [...$nodeList]); })->filter(function ($header) {
return mb_strlen($header['text']) > 0;
$filtered = array_values(array_filter($headerDetails)); });
// Shift headers if only smaller headers have been used // Shift headers if only smaller headers have been used
$levelChange = ($minLevel - 1); $levelChange = ($tree->pluck('level')->min() - 1);
foreach ($filtered as $index => $header) { $tree = $tree->map(function ($header) use ($levelChange) {
$filtered[$index]['level'] -= $levelChange; $header['level'] -= ($levelChange);
}
return $filtered; return $header;
});
return $tree->toArray();
} }
} }

View File

@@ -192,10 +192,10 @@ class PageIncludeParser
/** /**
* Get the parent paragraph of the given node, if existing. * Get the parent paragraph of the given node, if existing.
*/ */
protected function getParentParagraph(DOMNode $parent): ?DOMElement protected function getParentParagraph(DOMNode $parent): ?DOMNode
{ {
do { do {
if (strtolower($parent->nodeName) === 'p' && $parent instanceof DOMElement) { if (strtolower($parent->nodeName) === 'p') {
return $parent; return $parent;
} }

View File

@@ -52,7 +52,7 @@ class SlugGenerator
{ {
$slug = Str::slug($name); $slug = Str::slug($name);
if ($slug === '') { if ($slug === '') {
$slug = substr(md5(strval(rand(1, 500))), 0, 5); $slug = substr(md5(rand(1, 500)), 0, 5);
} }
return $slug; return $slug;

View File

@@ -3,8 +3,6 @@
namespace BookStack\Exports; namespace BookStack\Exports;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
@@ -121,9 +119,6 @@ class ImportRepo
$parentModel = null; $parentModel = null;
if ($import->type === 'page' || $import->type === 'chapter') { if ($import->type === 'page' || $import->type === 'chapter') {
$parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null; $parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null;
if ($parentModel && !($parentModel instanceof Book || $parentModel instanceof Chapter)) {
throw new ZipImportException(['Selected parent is not a book or chapter']);
}
} }
DB::beginTransaction(); DB::beginTransaction();

View File

@@ -4,8 +4,6 @@ namespace BookStack\Exports;
use BookStack\Exceptions\PdfExportException; use BookStack\Exceptions\PdfExportException;
use Dompdf\Dompdf; use Dompdf\Dompdf;
use FontLib\Font;
use Illuminate\Support\Str;
use Knp\Snappy\Pdf as SnappyPdf; use Knp\Snappy\Pdf as SnappyPdf;
use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
@@ -62,65 +60,12 @@ class PdfGenerator
$domPdf = new Dompdf($options); $domPdf = new Dompdf($options);
$domPdf->setBasePath(base_path('public')); $domPdf->setBasePath(base_path('public'));
$fontMetrics = $domPdf->getFontMetrics();
$userFontfamilies = $this->getUserDomPdfFontFamilies();
foreach ($userFontfamilies as $fontFamily => $fonts) {
try {
$fontMetrics->setFontFamily($fontFamily, $fonts);
} catch (\Exception $exception) {
$expectedPath = storage_path('fonts/dompdf');
throw new PdfExportException("Failed to create required font data in {$expectedPath}, Ensure all content in this location is writable by the web server");
}
}
$domPdf->loadHTML($this->convertEntities($html)); $domPdf->loadHTML($this->convertEntities($html));
$domPdf->render(); $domPdf->render();
return (string) $domPdf->output(); return (string) $domPdf->output();
} }
/**
* @return array<string, array<string, string>>
*/
protected function getUserDomPdfFontFamilies(): array
{
$fontStore = storage_path('fonts/dompdf');
if (!is_dir($fontStore)) {
return [];
}
$fontFamilies = [];
$fontFiles = glob($fontStore . DIRECTORY_SEPARATOR . '*.ttf');
foreach ($fontFiles as $fontFile) {
$fontFileName = basename($fontFile, '.ttf');
$expectedUfm = $fontStore . DIRECTORY_SEPARATOR . $fontFileName . '.ufm';
if (!file_exists($expectedUfm)) {
$font = Font::load($fontFile);
$font->parse();
try {
$font->saveAdobeFontMetrics($expectedUfm);
} catch (\Exception $exception) {
throw new PdfExportException("Failed to create required font data at $expectedUfm, Ensure this location is writable by the web server");
}
}
$nameParts = explode('-', $fontFileName);
if (count($nameParts) === 1 || $nameParts[1] === 'Regular') {
$nameParts[1] = 'Normal';
}
$family = trim(strtolower(preg_replace('/([A-Z])/', ' $1', $nameParts[0])));
$variation = Str::snake($nameParts[1]);
if (!isset($fontFamilies[$family])) {
$fontFamilies[$family] = [];
}
$fontFamilies[$family][$variation] = $fontStore . DIRECTORY_SEPARATOR . $fontFileName;
}
return $fontFamilies;
}
/** /**
* @throws PdfExportException * @throws PdfExportException
*/ */

View File

@@ -48,7 +48,7 @@ class ZipImportRunner
* Returns the top-level entity item which was imported. * Returns the top-level entity item which was imported.
* @throws ZipImportException * @throws ZipImportException
*/ */
public function run(Import $import, Book|Chapter|null $parent = null): Entity public function run(Import $import, ?Entity $parent = null): Entity
{ {
$zipPath = $this->getZipPath($import); $zipPath = $this->getZipPath($import);
$reader = new ZipExportReader($zipPath); $reader = new ZipExportReader($zipPath);

View File

@@ -7,7 +7,6 @@ use BookStack\App\Model;
use BookStack\Exceptions\NotifyException; use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\Permissions\Permission; use BookStack\Permissions\Permission;
use BookStack\Users\Models\OwnableInterface;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@@ -81,7 +80,7 @@ abstract class Controller extends BaseController
/** /**
* Check the current user's permissions against an ownable item otherwise throw an exception. * Check the current user's permissions against an ownable item otherwise throw an exception.
*/ */
protected function checkOwnablePermission(string|Permission $permission, Model&OwnableInterface $ownable, string $redirectLocation = '/'): void protected function checkOwnablePermission(string|Permission $permission, Model $ownable, string $redirectLocation = '/'): void
{ {
if (!userCan($permission, $ownable)) { if (!userCan($permission, $ownable)) {
$this->showPermissionError($redirectLocation); $this->showPermissionError($redirectLocation);

View File

@@ -101,7 +101,6 @@ class JointPermissionBuilder
/** /**
* Get a query for fetching a book with its children. * Get a query for fetching a book with its children.
* @return Builder<Book>
*/ */
protected function bookFetchQuery(): Builder protected function bookFetchQuery(): Builder
{ {
@@ -118,11 +117,9 @@ class JointPermissionBuilder
/** /**
* Build joint permissions for the given book and role combinations. * Build joint permissions for the given book and role combinations.
* @param EloquentCollection<int, Book> $books
*/ */
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false): void protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false): void
{ {
/** @var EloquentCollection<int, Entity> $entities */
$entities = clone $books; $entities = clone $books;
/** @var Book $book */ /** @var Book $book */

View File

@@ -2,8 +2,8 @@
namespace BookStack\References; namespace BookStack\References;
use BookStack\App\Model;
use BookStack\Permissions\Models\JointPermission; use BookStack\Permissions\Models\JointPermission;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;

View File

@@ -22,7 +22,6 @@ class ReferenceFetcher
*/ */
public function getReferencesToEntity(Entity $entity, bool $withContents = false): Collection public function getReferencesToEntity(Entity $entity, bool $withContents = false): Collection
{ {
/** @var Collection<int, Reference> $references */
$references = $this->queryReferencesToEntity($entity)->get(); $references = $this->queryReferencesToEntity($entity)->get();
$this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', false, $withContents); $this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', false, $withContents);
@@ -38,9 +37,6 @@ class ReferenceFetcher
return $this->queryReferencesToEntity($entity)->count(); return $this->queryReferencesToEntity($entity)->count();
} }
/**
* @return Builder<Reference>
*/
protected function queryReferencesToEntity(Entity $entity): Builder protected function queryReferencesToEntity(Entity $entity): Builder
{ {
$baseQuery = Reference::query() $baseQuery = Reference::query()

View File

@@ -119,7 +119,7 @@ class SearchIndex
* Create a scored term array from the given text, where the keys are the terms * Create a scored term array from the given text, where the keys are the terms
* and the values are their scores. * and the values are their scores.
* *
* @return array<string, float> * @return array<string, int>
*/ */
protected function generateTermScoreMapFromText(string $text, float $scoreAdjustment = 1): array protected function generateTermScoreMapFromText(string $text, float $scoreAdjustment = 1): array
{ {
@@ -136,7 +136,7 @@ class SearchIndex
* Create a scored term array from the given HTML, where the keys are the terms * Create a scored term array from the given HTML, where the keys are the terms
* and the values are their scores. * and the values are their scores.
* *
* @return array<string, float> * @return array<string, int>
*/ */
protected function generateTermScoreMapFromHtml(string $html): array protected function generateTermScoreMapFromHtml(string $html): array
{ {
@@ -177,7 +177,7 @@ class SearchIndex
* *
* @param Tag[] $tags * @param Tag[] $tags
* *
* @return array<string, float> * @return array<string, int>
*/ */
protected function generateTermScoreMapFromTags(array $tags): array protected function generateTermScoreMapFromTags(array $tags): array
{ {
@@ -277,9 +277,9 @@ class SearchIndex
* For the given term data arrays, Merge their contents by term * For the given term data arrays, Merge their contents by term
* while combining any scores. * while combining any scores.
* *
* @param array<string, float>[] ...$scoreMaps * @param array<string, int>[] ...$scoreMaps
* *
* @return array<string, float> * @return array<string, int>
*/ */
protected function mergeTermScoreMaps(...$scoreMaps): array protected function mergeTermScoreMaps(...$scoreMaps): array
{ {

View File

@@ -4,7 +4,6 @@ namespace BookStack\Search;
use BookStack\Entities\EntityProvider; use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\EntityTable;
use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Tools\EntityHydrator; use BookStack\Entities\Tools\EntityHydrator;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
@@ -91,8 +90,6 @@ class SearchRunner
/** /**
* Get a page of result data from the given query based on the provided page parameters. * Get a page of result data from the given query based on the provided page parameters.
* @param EloquentBuilder<EntityTable> $query
* @return Collection<Entity>
*/ */
protected function getPageOfDataFromQuery(EloquentBuilder $query, int $page, int $count): Collection protected function getPageOfDataFromQuery(EloquentBuilder $query, int $page, int $count): Collection
{ {
@@ -109,7 +106,6 @@ class SearchRunner
/** /**
* Create a search query for an entity. * Create a search query for an entity.
* @param string[] $entityTypes * @param string[] $entityTypes
* @return EloquentBuilder<EntityTable>
*/ */
protected function buildQuery(SearchOptions $searchOpts, array $entityTypes): EloquentBuilder protected function buildQuery(SearchOptions $searchOpts, array $entityTypes): EloquentBuilder
{ {
@@ -124,14 +120,8 @@ class SearchRunner
$filter = function (EloquentBuilder $query) use ($exact) { $filter = function (EloquentBuilder $query) use ($exact) {
$inputTerm = str_replace('\\', '\\\\', $exact->value); $inputTerm = str_replace('\\', '\\\\', $exact->value);
$query->where('name', 'like', '%' . $inputTerm . '%') $query->where('name', 'like', '%' . $inputTerm . '%')
->orWhere(function (EloquentBuilder $query) use ($inputTerm) { ->orWhere('description', 'like', '%' . $inputTerm . '%')
$query->whereNotNull('description') ->orWhere('text', 'like', '%' . $inputTerm . '%');
->where('description', 'like', '%' . $inputTerm . '%');
})
->orWhere(function (EloquentBuilder $query) use ($inputTerm) {
$query->whereNotNull('text')
->where('text', 'like', '%' . $inputTerm . '%');
});
}; };
$exact->negated ? $entityQuery->whereNot($filter) : $entityQuery->where($filter); $exact->negated ? $entityQuery->whereNot($filter) : $entityQuery->where($filter);

View File

@@ -243,9 +243,9 @@ class SettingService
/** /**
* Convert a setting key into a user-specific key. * Convert a setting key into a user-specific key.
*/ */
protected function userKey(int $userId, string $key = ''): string protected function userKey(string $userId, string $key = ''): string
{ {
return 'user:' . strval($userId) . ':' . $key; return 'user:' . $userId . ':' . $key;
} }
/** /**
@@ -267,7 +267,7 @@ class SettingService
/** /**
* Delete settings for a given user id. * Delete settings for a given user id.
*/ */
public function deleteUserSettings(int $userId): void public function deleteUserSettings(string $userId): void
{ {
Setting::query() Setting::query()
->where('setting_key', 'like', $this->userKey($userId) . '%') ->where('setting_key', 'like', $this->userKey($userId) . '%')

View File

@@ -38,35 +38,21 @@ class SortRule extends Model implements Loggable
$this->sequence = implode(',', $values); $this->sequence = implode(',', $values);
} }
/**
* @inheritDoc
*/
public function logDescriptor(): string public function logDescriptor(): string
{ {
return "({$this->id}) {$this->name}"; return "({$this->id}) {$this->name}";
} }
/**
* Get the URL to where this rule can be managed.
*/
public function getUrl(): string public function getUrl(): string
{ {
return url("/settings/sorting/rules/{$this->id}"); return url("/settings/sorting/rules/{$this->id}");
} }
/**
* Get the books which are specifically set to use this sort rule.
* @return HasMany<Book, $this>
*/
public function books(): HasMany public function books(): HasMany
{ {
return $this->hasMany(Book::class, 'entity_container_data.sort_rule_id', 'id'); return $this->hasMany(Book::class, 'entity_container_data.sort_rule_id', 'id');
} }
/**
* Get all the available sort rules, ordered by name, with the number of books using each.
* @return Collection<SortRule>
*/
public static function allByName(): Collection public static function allByName(): Collection
{ {
return static::query() return static::query()

View File

@@ -64,7 +64,6 @@ class LocaleManager
'sq' => 'sq_AL', 'sq' => 'sq_AL',
'sr' => 'sr_RS', 'sr' => 'sr_RS',
'sv' => 'sv_SE', 'sv' => 'sv_SE',
'th' => 'th_TH',
'tk' => 'tk_TM', 'tk' => 'tk_TM',
'tr' => 'tr_TR', 'tr' => 'tr_TR',
'uk' => 'uk_UA', 'uk' => 'uk_UA',

View File

@@ -102,7 +102,7 @@ class AttachmentService
/** /**
* Updates the ordering for a listing of attached files. * Updates the ordering for a listing of attached files.
*/ */
public function updateFileOrderWithinPage(array $attachmentOrder, int $pageId) public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId)
{ {
foreach ($attachmentOrder as $index => $attachmentId) { foreach ($attachmentOrder as $index => $attachmentId) {
Attachment::query()->where('uploaded_to', '=', $pageId) Attachment::query()->where('uploaded_to', '=', $pageId)

View File

@@ -195,7 +195,6 @@ class AttachmentController extends Controller
$this->validate($request, [ $this->validate($request, [
'order' => ['required', 'array'], 'order' => ['required', 'array'],
]); ]);
$page = $this->pageQueries->findVisibleByIdOrFail($pageId); $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission(Permission::PageUpdate, $page); $this->checkOwnablePermission(Permission::PageUpdate, $page);
@@ -222,6 +221,8 @@ class AttachmentController extends Controller
throw new NotFoundException(trans('errors.attachment_not_found')); throw new NotFoundException(trans('errors.attachment_not_found'));
} }
$this->checkOwnablePermission(Permission::PageView, $page);
if ($attachment->external) { if ($attachment->external) {
return redirect($attachment->path); return redirect($attachment->path);
} }
@@ -246,13 +247,6 @@ class AttachmentController extends Controller
{ {
/** @var Attachment $attachment */ /** @var Attachment $attachment */
$attachment = Attachment::query()->findOrFail($attachmentId); $attachment = Attachment::query()->findOrFail($attachmentId);
try {
$this->pageQueries->findVisibleByIdOrFail($attachment->uploaded_to);
} catch (NotFoundException $exception) {
throw new NotFoundException(trans('errors.attachment_not_found'));
}
$this->checkOwnablePermission(Permission::AttachmentDelete, $attachment); $this->checkOwnablePermission(Permission::AttachmentDelete, $attachment);
$this->attachmentService->deleteFile($attachment); $this->attachmentService->deleteFile($attachment);

View File

@@ -2,7 +2,6 @@
namespace BookStack\Uploads\Controllers; namespace BookStack\Uploads\Controllers;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use BookStack\Permissions\Permission; use BookStack\Permissions\Permission;
@@ -15,8 +14,7 @@ use Illuminate\Http\Request;
class DrawioImageController extends Controller class DrawioImageController extends Controller
{ {
public function __construct( public function __construct(
protected ImageRepo $imageRepo, protected ImageRepo $imageRepo
protected PageQueries $pageQueries,
) { ) {
} }
@@ -55,18 +53,16 @@ class DrawioImageController extends Controller
*/ */
public function create(Request $request) public function create(Request $request)
{ {
$this->checkPermission(Permission::ImageCreateAll); $this->validate($request, [
$validated = $this->validate($request, [
'image' => ['required', 'string'], 'image' => ['required', 'string'],
'uploaded_to' => ['required', 'integer'], 'uploaded_to' => ['required', 'integer'],
]); ]);
$imageBase64Data = $validated['image']; $this->checkPermission(Permission::ImageCreateAll);
$uploadedTo = $validated['uploaded_to']; $imageBase64Data = $request->input('image');
$targetPage = $this->pageQueries->findVisibleByIdOrFail($uploadedTo);
$this->checkOwnablePermission(Permission::PageUpdate, $targetPage);
try { try {
$uploadedTo = $request->input('uploaded_to', 0);
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo); $image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
} catch (ImageUploadException $e) { } catch (ImageUploadException $e) {
return response($e->getMessage(), 500); return response($e->getMessage(), 500);

View File

@@ -2,7 +2,6 @@
namespace BookStack\Uploads\Controllers; namespace BookStack\Uploads\Controllers;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use BookStack\Permissions\Permission; use BookStack\Permissions\Permission;
@@ -15,8 +14,7 @@ use Illuminate\Validation\ValidationException;
class GalleryImageController extends Controller class GalleryImageController extends Controller
{ {
public function __construct( public function __construct(
protected ImageRepo $imageRepo, protected ImageRepo $imageRepo
protected PageQueries $pageQueries,
) { ) {
} }
@@ -58,26 +56,20 @@ class GalleryImageController extends Controller
$this->checkPermission(Permission::ImageCreateAll); $this->checkPermission(Permission::ImageCreateAll);
try { try {
$validated = $this->validate($request, [ $this->validate($request, [
'file' => $this->getImageValidationRules(), 'file' => $this->getImageValidationRules(),
'uploaded_to' => ['required', 'integer'],
]); ]);
} catch (ValidationException $exception) { } catch (ValidationException $exception) {
$errors = $exception->errors(); return $this->jsonError(implode("\n", $exception->errors()['file']));
$messages = array_merge($errors['file'] ?? [], $errors['uploaded_to'] ?? []);
return $this->jsonError(implode("\n", $messages));
} }
$uploadedTo = intval($validated['uploaded_to']);
$targetPage = $this->pageQueries->findVisibleByIdOrFail($uploadedTo);
$this->checkOwnablePermission(Permission::PageUpdate, $targetPage);
new OutOfMemoryHandler(function () { new OutOfMemoryHandler(function () {
return $this->jsonError(trans('errors.image_upload_memory_limit')); return $this->jsonError(trans('errors.image_upload_memory_limit'));
}); });
try { try {
$imageUpload = $validated['file']; $imageUpload = $request->file('file');
$uploadedTo = $request->input('uploaded_to', 0);
$image = $this->imageRepo->saveNew($imageUpload, 'gallery', $uploadedTo); $image = $this->imageRepo->saveNew($imageUpload, 'gallery', $uploadedTo);
} catch (ImageUploadException $e) { } catch (ImageUploadException $e) {
return response($e->getMessage(), 500); return response($e->getMessage(), 500);

View File

@@ -75,7 +75,6 @@ class ImageGalleryApiController extends ApiController
$this->checkPermission(Permission::ImageCreateAll); $this->checkPermission(Permission::ImageCreateAll);
$data = $this->validate($request, $this->rules()['create']); $data = $this->validate($request, $this->rules()['create']);
$page = $this->pageQueries->findVisibleByIdOrFail($data['uploaded_to']); $page = $this->pageQueries->findVisibleByIdOrFail($data['uploaded_to']);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$image = $this->imageRepo->saveNew($data['image'], $data['type'], $page->id); $image = $this->imageRepo->saveNew($data['image'], $data['type'], $page->id);

View File

@@ -87,7 +87,7 @@ class RoleApiController extends ApiController
*/ */
public function read(string $id) public function read(string $id)
{ {
$role = $this->permissionsRepo->getRoleById(intval($id)); $role = $this->permissionsRepo->getRoleById($id);
$this->singleFormatter($role); $this->singleFormatter($role);
return response()->json($role); return response()->json($role);

View File

@@ -94,7 +94,7 @@ class RoleController extends Controller
public function edit(string $id) public function edit(string $id)
{ {
$this->checkPermission(Permission::UserRolesManage); $this->checkPermission(Permission::UserRolesManage);
$role = $this->permissionsRepo->getRoleById(intval($id)); $role = $this->permissionsRepo->getRoleById($id);
$this->setPageTitle(trans('settings.role_edit')); $this->setPageTitle(trans('settings.role_edit'));
@@ -129,7 +129,7 @@ class RoleController extends Controller
public function showDelete(string $id) public function showDelete(string $id)
{ {
$this->checkPermission(Permission::UserRolesManage); $this->checkPermission(Permission::UserRolesManage);
$role = $this->permissionsRepo->getRoleById(intval($id)); $role = $this->permissionsRepo->getRoleById($id);
$roles = $this->permissionsRepo->getAllRolesExcept($role); $roles = $this->permissionsRepo->getAllRolesExcept($role);
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]); $blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
$roles->prepend($blankRole); $roles->prepend($blankRole);
@@ -151,7 +151,7 @@ class RoleController extends Controller
try { try {
$migrateRoleId = intval($request->input('migrate_role_id') ?: "0"); $migrateRoleId = intval($request->input('migrate_role_id') ?: "0");
$this->permissionsRepo->deleteRole(intval($id), $migrateRoleId); $this->permissionsRepo->deleteRole($id, $migrateRoleId);
} catch (PermissionsException $e) { } catch (PermissionsException $e) {
$this->showErrorNotification($e->getMessage()); $this->showErrorNotification($e->getMessage());

View File

@@ -110,7 +110,7 @@ class UserApiController extends ApiController
*/ */
public function read(string $id) public function read(string $id)
{ {
$user = $this->userRepo->getById(intval($id)); $user = $this->userRepo->getById($id);
$this->singleFormatter($user); $this->singleFormatter($user);
return response()->json($user); return response()->json($user);
@@ -124,8 +124,8 @@ class UserApiController extends ApiController
*/ */
public function update(Request $request, string $id) public function update(Request $request, string $id)
{ {
$data = $this->validate($request, $this->rules(intval($id))['update']); $data = $this->validate($request, $this->rules($id)['update']);
$user = $this->userRepo->getById(intval($id)); $user = $this->userRepo->getById($id);
$this->userRepo->update($user, $data, userCan(Permission::UsersManage)); $this->userRepo->update($user, $data, userCan(Permission::UsersManage));
$this->singleFormatter($user); $this->singleFormatter($user);
@@ -140,7 +140,7 @@ class UserApiController extends ApiController
*/ */
public function delete(Request $request, string $id) public function delete(Request $request, string $id)
{ {
$user = $this->userRepo->getById(intval($id)); $user = $this->userRepo->getById($id);
$newOwnerId = $request->input('migrate_ownership_id', null); $newOwnerId = $request->input('migrate_ownership_id', null);
$this->userRepo->destroy($user, $newOwnerId); $this->userRepo->destroy($user, $newOwnerId);

View File

@@ -4,7 +4,6 @@ namespace BookStack\Users\Controllers;
use BookStack\Access\SocialDriverManager; use BookStack\Access\SocialDriverManager;
use BookStack\Access\UserInviteException; use BookStack\Access\UserInviteException;
use BookStack\Activity\ActivityType;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\UserUpdateException; use BookStack\Exceptions\UserUpdateException;
use BookStack\Http\Controller; use BookStack\Http\Controller;
@@ -209,20 +208,4 @@ class UserController extends Controller
return redirect('/settings/users'); return redirect('/settings/users');
} }
/**
* Reset MFA for the specified user.
*/
public function resetMfa(Request $request, int $id)
{
$this->preventAccessInDemoMode();
$this->checkPermission(Permission::UsersManage);
$user = $this->userRepo->getById($id);
$user->mfaValues()->delete();
$this->logActivity(ActivityType::USER_MFA_RESET, $user);
return redirect("/settings/users/{$user->id}");
}
} }

View File

@@ -30,8 +30,6 @@ class CspService
$this->getFrameAncestors(), $this->getFrameAncestors(),
$this->getFrameSrc(), $this->getFrameSrc(),
$this->getScriptSrc(), $this->getScriptSrc(),
$this->getStyleSrc(),
$this->getImgSrc(),
$this->getObjectSrc(), $this->getObjectSrc(),
$this->getBaseUri(), $this->getBaseUri(),
]; ];
@@ -47,8 +45,6 @@ class CspService
$headers = [ $headers = [
$this->getFrameSrc(), $this->getFrameSrc(),
$this->getScriptSrc(), $this->getScriptSrc(),
$this->getStyleSrc(),
$this->getImgSrc(),
$this->getObjectSrc(), $this->getObjectSrc(),
$this->getBaseUri(), $this->getBaseUri(),
]; ];
@@ -119,22 +115,6 @@ class CspService
return "object-src 'self'"; return "object-src 'self'";
} }
/**
* Creates CSP 'style-src' rule to restrict where styles can be loaded from.
*/
protected function getStyleSrc(): string
{
return 'style-src ' . implode(' ', $this->getAllowedStyleSources());
}
/**
* Creates CSP 'img-src' rule to restrict where images can be loaded from.
*/
protected function getImgSrc(): string
{
return 'img-src ' . implode(' ', $this->getAllowedImageSources());
}
/** /**
* Creates CSP 'base-uri' rule to restrict what base tags can be set on * Creates CSP 'base-uri' rule to restrict what base tags can be set on
* the page to prevent manipulation of relative links. * the page to prevent manipulation of relative links.
@@ -164,59 +144,6 @@ class CspService
return array_filter($sources); return array_filter($sources);
} }
/**
* Get allowed style sources for the style-src directive.
*/
protected function getAllowedStyleSources(): array
{
$configured = config('app.style_sources');
if (is_string($configured)) {
$sources = array_filter(explode(' ', $configured));
array_unshift($sources, "'self'");
// Ensure 'unsafe-inline' is quoted if present
// This is done as attempting to pass this in env values with quotes can either
// be awkward or cause issues.
$unsafeInlineIndex = array_search('unsafe-inline', $sources, true);
if ($unsafeInlineIndex !== false) {
$sources[$unsafeInlineIndex] = "'unsafe-inline'";
}
return array_values(array_unique($sources));
}
return [
"'self'",
"'unsafe-inline'",
'http:',
'https:',
];
}
/**
* Get allowed image sources for the img-src directive.
*/
protected function getAllowedImageSources(): array
{
$configured = config('app.image_sources');
if (is_string($configured)) {
$sources = array_filter(explode(' ', $configured));
array_unshift($sources, "'self'", 'blob:', 'data:');
return array_values(array_unique($sources));
}
return [
"'self'",
'data:',
'blob:',
'http:',
'https:',
];
}
/** /**
* Extract the host name of the configured drawio URL for use in CSP. * Extract the host name of the configured drawio URL for use in CSP.
* Returns empty string if not in use. * Returns empty string if not in use.

View File

@@ -3,7 +3,6 @@
namespace BookStack\Util; namespace BookStack\Util;
use Closure; use Closure;
use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Throwable; use Throwable;
@@ -25,7 +24,7 @@ use Throwable;
class DatabaseTransaction class DatabaseTransaction
{ {
/** /**
* @param (Closure(Connection): TReturn) $callback * @param (Closure(static): TReturn) $callback
*/ */
public function __construct( public function __construct(
protected Closure $callback protected Closure $callback

View File

@@ -8,10 +8,6 @@ use BookStack\Exceptions\HttpFetchException;
* Validate the host we're connecting to when making a server-side-request. * Validate the host we're connecting to when making a server-side-request.
* Will use the given hosts config if given during construction otherwise * Will use the given hosts config if given during construction otherwise
* will look to the app configured config. * will look to the app configured config.
*
* The config format is a space-seperated list of URL prefixes which should contain the
* protocol and host. It can optionally define a path prefix as part of the URL.
* Wildcards, via a '*', can be used within these elements to match anything but a '/'.
*/ */
class SsrUrlValidator class SsrUrlValidator
{ {
@@ -52,34 +48,15 @@ class SsrUrlValidator
{ {
$pattern = rtrim(trim($pattern), '/'); $pattern = rtrim(trim($pattern), '/');
$url = trim($url); $url = trim($url);
$urlParts = parse_url($url);
if (empty($pattern) || empty($url) || $urlParts === false) { if (empty($pattern) || empty($url)) {
return false; return false;
} }
// Prevent potential tricks using percent encoded slashes
if (str_contains(strtolower($urlParts['host'] ?? ''), '%2f')) {
return false;
}
// Disregard query and fragment
$url = explode('?', $url, 2)[0];
$url = explode('#', $url, 2)[0];
// Disregard userinfo if existing
if (!empty($urlParts['user']) || !empty($urlParts['pass'])) {
[$start, $postUserinfo] = explode('@', $url, 2);
$preUserinfo = explode('//', $start, 2)[0];
$url = ($preUserinfo ? $preUserinfo . '//' : '') . $postUserinfo;
}
// Prepare pattern
$quoted = preg_quote($pattern, '/'); $quoted = preg_quote($pattern, '/');
$regexPattern = str_replace('\*', '[^\/]*', $quoted); $regexPattern = str_replace('\*', '.*', $quoted);
// Check against our URL return preg_match('/^' . $regexPattern . '($|\/.*$|#.*$)/i', $url);
return preg_match('/^' . $regexPattern . '($|\/.*$)/i', $url);
} }
/** /**

668
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,10 @@
project_id: "377219" project_id: "377219"
project_identifier: bookstack project_identifier: bookstack
api_token_env: CROWDIN_PERSONAL_TOKEN
base_path: . base_path: .
preserve_hierarchy: false preserve_hierarchy: false
pull_request_title: Updated translations with latest Crowdin changes pull_request_title: Updated translations with latest Crowdin changes
pull_request_labels: pull_request_labels:
- "Translations" - ":earth_africa: Translations"
files: files:
- source: /lang/en/*.php - source: /lang/en/*.php
translation: /lang/%two_letters_code%/%original_file_name% translation: /lang/%two_letters_code%/%original_file_name%

View File

@@ -18,7 +18,7 @@ ARG BRANCH=development
# Download BookStack & install PHP deps # Download BookStack & install PHP deps
RUN mkdir -p /var/www && \ RUN mkdir -p /var/www && \
git clone https://codeberg.org/bookstack/bookstack.git --branch "$BRANCH" --single-branch /var/www/bookstack && \ git clone https://github.com/bookstackapp/bookstack.git --branch "$BRANCH" --single-branch /var/www/bookstack && \
cd /var/www/bookstack && \ cd /var/www/bookstack && \
wget https://raw.githubusercontent.com/composer/getcomposer.org/f3108f64b4e1c1ce6eb462b159956461592b3e3e/web/installer -O - -q | php -- --quiet --filename=composer && \ wget https://raw.githubusercontent.com/composer/getcomposer.org/f3108f64b4e1c1ce6eb462b159956461592b3e3e/web/installer -O - -q | php -- --quiet --filename=composer && \
php composer install php composer install

View File

@@ -74,7 +74,7 @@ Theme::registerCommand(new SayHelloCommand());
## Available Events ## Available Events
All available events dispatched by BookStack are exposed as static properties on the `\BookStack\Theming\ThemeEvents` class, which can be found within the file `app/Theming/ThemeEvents.php` relative to your root BookStack folder. Alternatively, the events for the latest release can be [seen on Codeberg here](https://codeberg.org/bookstack/bookstack/src/branch/release/app/Theming/ThemeEvents.php). All available events dispatched by BookStack are exposed as static properties on the `\BookStack\Theming\ThemeEvents` class, which can be found within the file `app/Theming/ThemeEvents.php` relative to your root BookStack folder. Alternatively, the events for the latest release can be [seen on GitHub here](https://github.com/BookStackApp/BookStack/blob/release/app/Theming/ThemeEvents.php).
The comments above each constant with the `ThemeEvents.php` file describe the dispatch conditions of the event, in addition to the arguments the action will receive. The comments may also describe any ways the return value of the action may be used. The comments above each constant with the `ThemeEvents.php` file describe the dispatch conditions of the event, in addition to the arguments the action will receive. The comments may also describe any ways the return value of the action may be used.

View File

@@ -12,13 +12,13 @@ Feature releases are generally larger, bringing new features in addition to fixe
### Release Planning Process ### Release Planning Process
Each BookStack release will have a [milestone](https://codeberg.org/bookstack/bookstack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed. Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed.
### Release Announcements ### Release Announcements
Feature releases, and some patch releases, will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [Codeberg release page](https://codeberg.org/bookstack/bookstack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blog posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates). Feature releases, and some patch releases, will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blog posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates).
### Release Technical Process ### Release Technical Process
Deploying a release, at a high level, simply involves merging the development branch into the release branch before then building & committing any release-only assets. Deploying a release, at a high level, simply involves merging the development branch into the release branch before then building & committing any release-only assets.
A helper script [can be found in our](https://codeberg.org/bookstack/devops/src/branch/main/meta-scripts/bookstack-release-steps) devops repo which provides the steps and commands for deploying a new release. A helper script [can be found in our](https://github.com/BookStackApp/devops/blob/main/meta-scripts/bookstack-release-steps) devops repo which provides the steps and commands for deploying a new release.

View File

@@ -2,7 +2,7 @@
**Warning: This API is currently in development and may change without notice.** **Warning: This API is currently in development and may change without notice.**
Feedback is very much welcomed via this issue: https://codeberg.org/bookstack/bookstack/issues/5937 Feedback is very much welcomed via this issue: https://github.com/BookStackApp/BookStack/issues/5937
This document covers the JavaScript API for the (newer Lexical-based) WYSIWYG editor. This document covers the JavaScript API for the (newer Lexical-based) WYSIWYG editor.
This API is built and designed to abstract the internals of the editor away This API is built and designed to abstract the internals of the editor away

View File

@@ -69,9 +69,8 @@ Source: zeit/arg
Link: zeit/arg Link: zeit/arg
----------- -----------
argparse argparse
License: MIT License: Python-2.0
License File: node_modules/argparse/LICENSE License File: node_modules/argparse/LICENSE
Copyright: Copyright (C) 2012 by Vitaly Puzrin
Source: nodeca/argparse Source: nodeca/argparse
Link: nodeca/argparse Link: nodeca/argparse
----------- -----------
@@ -82,6 +81,34 @@ Copyright: Copyright (c) 2023 Inspect JS
Source: git+https://github.com/inspect-js/array-buffer-byte-length.git Source: git+https://github.com/inspect-js/array-buffer-byte-length.git
Link: https://github.com/inspect-js/array-buffer-byte-length#readme Link: https://github.com/inspect-js/array-buffer-byte-length#readme
----------- -----------
array-includes
License: MIT
License File: node_modules/array-includes/LICENSE
Copyright: Copyright (C) 2015 Jordan Harband
Source: git://github.com/es-shims/array-includes.git
Link: git://github.com/es-shims/array-includes.git
-----------
array.prototype.findlastindex
License: MIT
License File: node_modules/array.prototype.findlastindex/LICENSE
Copyright: Copyright (c) 2021 ECMAScript Shims
Source: git+https://github.com/es-shims/Array.prototype.findLastIndex.git
Link: https://github.com/es-shims/Array.prototype.findLastIndex#readme
-----------
array.prototype.flat
License: MIT
License File: node_modules/array.prototype.flat/LICENSE
Copyright: Copyright (c) 2017 ECMAScript Shims
Source: git://github.com/es-shims/Array.prototype.flat.git
Link: git://github.com/es-shims/Array.prototype.flat.git
-----------
array.prototype.flatmap
License: MIT
License File: node_modules/array.prototype.flatmap/LICENSE
Copyright: Copyright (c) 2017 ECMAScript Shims
Source: git://github.com/es-shims/Array.prototype.flatMap.git
Link: git://github.com/es-shims/Array.prototype.flatMap.git
-----------
arraybuffer.prototype.slice arraybuffer.prototype.slice
License: MIT License: MIT
License File: node_modules/arraybuffer.prototype.slice/LICENSE License File: node_modules/arraybuffer.prototype.slice/LICENSE
@@ -142,14 +169,9 @@ Link: https://github.com/jestjs/jest.git
balanced-match balanced-match
License: MIT License: MIT
License File: node_modules/balanced-match/LICENSE.md License File: node_modules/balanced-match/LICENSE.md
Copyright: Copyright (c) 2013 Julian Gruber &lt;******@************.***&gt;
Source: git://github.com/juliangruber/balanced-match.git Source: git://github.com/juliangruber/balanced-match.git
Link: git://github.com/juliangruber/balanced-match.git Link: https://github.com/juliangruber/balanced-match
-----------
baseline-browser-mapping
License: Apache-2.0
License File: node_modules/baseline-browser-mapping/LICENSE.txt
Source: git+https://github.com/web-platform-dx/baseline-browser-mapping.git
Link: git+https://github.com/web-platform-dx/baseline-browser-mapping.git
----------- -----------
binary-extensions binary-extensions
License: MIT License: MIT
@@ -162,8 +184,9 @@ Link: sindresorhus/binary-extensions
brace-expansion brace-expansion
License: MIT License: MIT
License File: node_modules/brace-expansion/LICENSE License File: node_modules/brace-expansion/LICENSE
Source: git+ssh://git@github.com/juliangruber/brace-expansion.git Copyright: Copyright (c) 2013 Julian Gruber <******@************.***>
Link: git+ssh://git@github.com/juliangruber/brace-expansion.git Source: git://github.com/juliangruber/brace-expansion.git
Link: https://github.com/juliangruber/brace-expansion
----------- -----------
braces braces
License: MIT License: MIT
@@ -449,8 +472,8 @@ Link: git://github.com/ljharb/define-properties.git
detect-libc detect-libc
License: Apache-2.0 License: Apache-2.0
License File: node_modules/detect-libc/LICENSE License File: node_modules/detect-libc/LICENSE
Source: git://github.com/lovell/detect-libc.git Source: git://github.com/lovell/detect-libc
Link: git://github.com/lovell/detect-libc.git Link: git://github.com/lovell/detect-libc
----------- -----------
detect-newline detect-newline
License: MIT License: MIT
@@ -466,6 +489,12 @@ Copyright: Copyright (c) 2009-2015, Kevin Decker <********@*****.***>
Source: git://github.com/kpdecker/jsdiff.git Source: git://github.com/kpdecker/jsdiff.git
Link: git://github.com/kpdecker/jsdiff.git Link: git://github.com/kpdecker/jsdiff.git
----------- -----------
doctrine
License: Apache-2.0
License File: node_modules/doctrine/LICENSE
Source: eslint/doctrine
Link: https://github.com/eslint/doctrine
-----------
dunder-proto dunder-proto
License: MIT License: MIT
License File: node_modules/dunder-proto/LICENSE License File: node_modules/dunder-proto/LICENSE
@@ -482,8 +511,8 @@ electron-to-chromium
License: ISC License: ISC
License File: node_modules/electron-to-chromium/LICENSE License File: node_modules/electron-to-chromium/LICENSE
Copyright: Copyright 2018 Kilian Valkhof Copyright: Copyright 2018 Kilian Valkhof
Source: git+https://github.com/kilian/electron-to-chromium.git Source: https://github.com/kilian/electron-to-chromium/
Link: git+https://github.com/kilian/electron-to-chromium.git Link: https://github.com/kilian/electron-to-chromium/
----------- -----------
emittery emittery
License: MIT License: MIT
@@ -548,6 +577,13 @@ Copyright: Copyright (c) 2022 ECMAScript Shims
Source: git+https://github.com/es-shims/es-set-tostringtag.git Source: git+https://github.com/es-shims/es-set-tostringtag.git
Link: https://github.com/es-shims/es-set-tostringtag#readme Link: https://github.com/es-shims/es-set-tostringtag#readme
----------- -----------
es-shim-unscopables
License: MIT
License File: node_modules/es-shim-unscopables/LICENSE
Copyright: Copyright (c) 2022 Jordan Harband
Source: git+https://github.com/ljharb/es-shim-unscopables.git
Link: https://github.com/ljharb/es-shim-unscopables#readme
-----------
es-to-primitive es-to-primitive
License: MIT License: MIT
License File: node_modules/es-to-primitive/LICENSE License File: node_modules/es-to-primitive/LICENSE
@@ -576,6 +612,27 @@ Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindres
Source: sindresorhus/escape-string-regexp Source: sindresorhus/escape-string-regexp
Link: sindresorhus/escape-string-regexp Link: sindresorhus/escape-string-regexp
----------- -----------
eslint-import-resolver-node
License: MIT
License File: node_modules/eslint-import-resolver-node/LICENSE
Copyright: Copyright (c) 2015 Ben Mosher
Source: https://github.com/import-js/eslint-plugin-import
Link: https://github.com/import-js/eslint-plugin-import
-----------
eslint-module-utils
License: MIT
License File: node_modules/eslint-module-utils/LICENSE
Copyright: Copyright (c) 2015 Ben Mosher
Source: git+https://github.com/import-js/eslint-plugin-import.git
Link: https://github.com/import-js/eslint-plugin-import#readme
-----------
eslint-plugin-import
License: MIT
License File: node_modules/eslint-plugin-import/LICENSE
Copyright: Copyright (c) 2015 Ben Mosher
Source: https://github.com/import-js/eslint-plugin-import
Link: https://github.com/import-js/eslint-plugin-import
-----------
eslint-scope eslint-scope
License: BSD-2-Clause License: BSD-2-Clause
License File: node_modules/eslint-scope/LICENSE License File: node_modules/eslint-scope/LICENSE
@@ -757,13 +814,6 @@ Copyright: Copyright (c) 2019 Jordan Harband
Source: git+https://github.com/inspect-js/functions-have-names.git Source: git+https://github.com/inspect-js/functions-have-names.git
Link: https://github.com/inspect-js/functions-have-names#readme Link: https://github.com/inspect-js/functions-have-names#readme
----------- -----------
generator-function
License: MIT
License File: node_modules/generator-function/LICENSE.md
Copyright: Copyright (c) 2015 Tiancheng “Timothy” Gu
Source: git+https://github.com/TimothyGu/generator-function.git
Link: https://github.com/TimothyGu/generator-function#readme
-----------
gensync gensync
License: MIT License: MIT
License File: node_modules/gensync/LICENSE License File: node_modules/gensync/LICENSE
@@ -976,6 +1026,13 @@ Copyright: Copyright (c) 2014-present, Lee Byron and other contributors.
Source: git://github.com/immutable-js/immutable-js.git Source: git://github.com/immutable-js/immutable-js.git
Link: https://immutable-js.com Link: https://immutable-js.com
----------- -----------
import-fresh
License: MIT
License File: node_modules/import-fresh/license
Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com)
Source: sindresorhus/import-fresh
Link: sindresorhus/import-fresh
-----------
import-local import-local
License: MIT License: MIT
License File: node_modules/import-local/license License File: node_modules/import-local/license
@@ -1472,7 +1529,7 @@ License: MIT
License File: node_modules/js-yaml/LICENSE License File: node_modules/js-yaml/LICENSE
Copyright: Copyright (C) 2011-2015 by Vitaly Puzrin Copyright: Copyright (C) 2011-2015 by Vitaly Puzrin
Source: nodeca/js-yaml Source: nodeca/js-yaml
Link: https://github.com/nodeca/js-yaml Link: nodeca/js-yaml
----------- -----------
jsdom jsdom
License: MIT License: MIT
@@ -1588,6 +1645,12 @@ License File: node_modules/lodash.memoize/LICENSE
Source: lodash/lodash Source: lodash/lodash
Link: https://lodash.com/ Link: https://lodash.com/
----------- -----------
lodash.merge
License: MIT
License File: node_modules/lodash.merge/LICENSE
Source: lodash/lodash
Link: https://lodash.com/
-----------
lodash.throttle lodash.throttle
License: MIT License: MIT
License File: node_modules/lodash.throttle/LICENSE License File: node_modules/lodash.throttle/LICENSE
@@ -1678,10 +1741,11 @@ Source: sindresorhus/mimic-fn
Link: sindresorhus/mimic-fn Link: sindresorhus/mimic-fn
----------- -----------
minimatch minimatch
License: BlueOak-1.0.0 License: ISC
License File: node_modules/minimatch/LICENSE.md License File: node_modules/minimatch/LICENSE
Source: git@github.com:isaacs/minimatch Copyright: Copyright (c) Isaac Z. Schlueter and Contributors
Link: git@github.com:isaacs/minimatch Source: git://github.com/isaacs/minimatch.git
Link: git://github.com/isaacs/minimatch.git
----------- -----------
minimist minimist
License: MIT License: MIT
@@ -1690,8 +1754,9 @@ Source: git://github.com/minimistjs/minimist.git
Link: https://github.com/minimistjs/minimist Link: https://github.com/minimistjs/minimist
----------- -----------
minipass minipass
License: BlueOak-1.0.0 License: ISC
License File: node_modules/minipass/LICENSE.md License File: node_modules/minipass/LICENSE
Copyright: Copyright (c) 2017-2023 npm, Inc., Isaac Z. Schlueter, and Contributors
Source: https://github.com/isaacs/minipass Source: https://github.com/isaacs/minipass
Link: https://github.com/isaacs/minipass Link: https://github.com/isaacs/minipass
----------- -----------
@@ -1807,6 +1872,27 @@ Copyright: Copyright (c) 2014 Jordan Harband
Source: git://github.com/ljharb/object.assign.git Source: git://github.com/ljharb/object.assign.git
Link: git://github.com/ljharb/object.assign.git Link: git://github.com/ljharb/object.assign.git
----------- -----------
object.fromentries
License: MIT
License File: node_modules/object.fromentries/LICENSE
Copyright: Copyright (c) 2018 Jordan Harband
Source: git://github.com/es-shims/Object.fromEntries.git
Link: git://github.com/es-shims/Object.fromEntries.git
-----------
object.groupby
License: MIT
License File: node_modules/object.groupby/LICENSE
Copyright: Copyright (c) 2023 ECMAScript Shims
Source: git+https://github.com/es-shims/Object.groupBy.git
Link: https://github.com/es-shims/Object.groupBy#readme
-----------
object.values
License: MIT
License File: node_modules/object.values/LICENSE
Copyright: Copyright (c) 2015 Jordan Harband
Source: git://github.com/es-shims/Object.values.git
Link: git://github.com/es-shims/Object.values.git
-----------
once once
License: ISC License: ISC
License File: node_modules/once/LICENSE License File: node_modules/once/LICENSE
@@ -1862,6 +1948,13 @@ License File: node_modules/package-json-from-dist/LICENSE.md
Source: git+https://github.com/isaacs/package-json-from-dist.git Source: git+https://github.com/isaacs/package-json-from-dist.git
Link: git+https://github.com/isaacs/package-json-from-dist.git Link: git+https://github.com/isaacs/package-json-from-dist.git
----------- -----------
parent-module
License: MIT
License File: node_modules/parent-module/license
Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)
Source: sindresorhus/parent-module
Link: sindresorhus/parent-module
-----------
parse-json parse-json
License: MIT License: MIT
License File: node_modules/parse-json/license License File: node_modules/parse-json/license
@@ -2066,8 +2159,8 @@ resolve
License: MIT License: MIT
License File: node_modules/resolve/LICENSE License File: node_modules/resolve/LICENSE
Copyright: Copyright (c) 2012 James Halliday Copyright: Copyright (c) 2012 James Halliday
Source: ssh://github.com/browserify/resolve.git Source: git://github.com/browserify/resolve.git
Link: ssh://github.com/browserify/resolve.git Link: git://github.com/browserify/resolve.git
----------- -----------
rrweb-cssom rrweb-cssom
License: MIT License: MIT
@@ -2312,7 +2405,7 @@ Link: sindresorhus/string-width
string-width string-width
License: MIT License: MIT
License File: node_modules/string-width/license License File: node_modules/string-width/license
Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com) Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)
Source: sindresorhus/string-width Source: sindresorhus/string-width
Link: sindresorhus/string-width Link: sindresorhus/string-width
----------- -----------
@@ -2477,6 +2570,13 @@ Copyright: Copyright (c) 2014 Blake Embrey (*****@***********.***)
Source: git://github.com/TypeStrong/ts-node.git Source: git://github.com/TypeStrong/ts-node.git
Link: https://typestrong.org/ts-node Link: https://typestrong.org/ts-node
----------- -----------
tsconfig-paths
License: MIT
License File: node_modules/tsconfig-paths/LICENSE
Copyright: Copyright (c) 2016 Jonas Kello
Source: https://github.com/dividab/tsconfig-paths
Link: https://github.com/dividab/tsconfig-paths
-----------
type-check type-check
License: MIT License: MIT
License File: node_modules/type-check/LICENSE License File: node_modules/type-check/LICENSE
@@ -2710,7 +2810,7 @@ Link: chalk/wrap-ansi
wrap-ansi wrap-ansi
License: MIT License: MIT
License File: node_modules/wrap-ansi/license License File: node_modules/wrap-ansi/license
Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com) Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)
Source: chalk/wrap-ansi Source: chalk/wrap-ansi
Link: chalk/wrap-ansi Link: chalk/wrap-ansi
----------- -----------
@@ -2792,6 +2892,12 @@ Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindres
Source: sindresorhus/yocto-queue Source: sindresorhus/yocto-queue
Link: sindresorhus/yocto-queue Link: sindresorhus/yocto-queue
----------- -----------
@ampproject/remapping
License: Apache-2.0
License File: node_modules/@ampproject/remapping/LICENSE
Source: git+https://github.com/ampproject/remapping.git
Link: git+https://github.com/ampproject/remapping.git
-----------
@asamuzakjp/css-color @asamuzakjp/css-color
License: MIT License: MIT
License File: node_modules/@asamuzakjp/css-color/LICENSE License File: node_modules/@asamuzakjp/css-color/LICENSE
@@ -3066,15 +3172,15 @@ Link: https://demurgos.github.io/v8-coverage
License: MIT License: MIT
License File: node_modules/@codemirror/autocomplete/LICENSE License File: node_modules/@codemirror/autocomplete/LICENSE
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others
Source: git+https://github.com/codemirror/autocomplete.git Source: https://github.com/codemirror/autocomplete.git
Link: git+https://github.com/codemirror/autocomplete.git Link: https://github.com/codemirror/autocomplete.git
----------- -----------
@codemirror/commands @codemirror/commands
License: MIT License: MIT
License File: node_modules/@codemirror/commands/LICENSE License File: node_modules/@codemirror/commands/LICENSE
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others
Source: git+https://github.com/codemirror/commands.git Source: https://github.com/codemirror/commands.git
Link: git+https://github.com/codemirror/commands.git Link: https://github.com/codemirror/commands.git
----------- -----------
@codemirror/lang-css @codemirror/lang-css
License: MIT License: MIT
@@ -3094,8 +3200,8 @@ Link: https://github.com/codemirror/lang-html.git
License: MIT License: MIT
License File: node_modules/@codemirror/lang-javascript/LICENSE License File: node_modules/@codemirror/lang-javascript/LICENSE
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others
Source: git+https://github.com/codemirror/lang-javascript.git Source: https://github.com/codemirror/lang-javascript.git
Link: git+https://github.com/codemirror/lang-javascript.git Link: https://github.com/codemirror/lang-javascript.git
----------- -----------
@codemirror/lang-json @codemirror/lang-json
License: MIT License: MIT
@@ -3129,8 +3235,8 @@ Link: https://github.com/codemirror/lang-xml.git
License: MIT License: MIT
License File: node_modules/@codemirror/language/LICENSE License File: node_modules/@codemirror/language/LICENSE
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others
Source: git+https://github.com/codemirror/language.git Source: https://github.com/codemirror/language.git
Link: git+https://github.com/codemirror/language.git Link: https://github.com/codemirror/language.git
----------- -----------
@codemirror/legacy-modes @codemirror/legacy-modes
License: MIT License: MIT
@@ -3143,22 +3249,22 @@ Link: https://github.com/codemirror/legacy-modes.git
License: MIT License: MIT
License File: node_modules/@codemirror/lint/LICENSE License File: node_modules/@codemirror/lint/LICENSE
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others
Source: git+https://github.com/codemirror/lint.git Source: https://github.com/codemirror/lint.git
Link: git+https://github.com/codemirror/lint.git Link: https://github.com/codemirror/lint.git
----------- -----------
@codemirror/search @codemirror/search
License: MIT License: MIT
License File: node_modules/@codemirror/search/LICENSE License File: node_modules/@codemirror/search/LICENSE
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others
Source: git+https://github.com/codemirror/search.git Source: https://github.com/codemirror/search.git
Link: git+https://github.com/codemirror/search.git Link: https://github.com/codemirror/search.git
----------- -----------
@codemirror/state @codemirror/state
License: MIT License: MIT
License File: node_modules/@codemirror/state/LICENSE License File: node_modules/@codemirror/state/LICENSE
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others
Source: git+https://github.com/codemirror/state.git Source: https://github.com/codemirror/state.git
Link: git+https://github.com/codemirror/state.git Link: https://github.com/codemirror/state.git
----------- -----------
@codemirror/theme-one-dark @codemirror/theme-one-dark
License: MIT License: MIT
@@ -3171,8 +3277,8 @@ Link: https://github.com/codemirror/theme-one-dark.git
License: MIT License: MIT
License File: node_modules/@codemirror/view/LICENSE License File: node_modules/@codemirror/view/LICENSE
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others
Source: git+https://github.com/codemirror/view.git Source: https://github.com/codemirror/view.git
Link: git+https://github.com/codemirror/view.git Link: https://github.com/codemirror/view.git
----------- -----------
@cspotcode/source-map-support @cspotcode/source-map-support
License: MIT License: MIT
@@ -3252,6 +3358,12 @@ License File: node_modules/@eslint/core/LICENSE
Source: git+https://github.com/eslint/rewrite.git Source: git+https://github.com/eslint/rewrite.git
Link: https://github.com/eslint/rewrite/tree/main/packages/core#readme Link: https://github.com/eslint/rewrite/tree/main/packages/core#readme
----------- -----------
@eslint/eslintrc
License: MIT
License File: node_modules/@eslint/eslintrc/LICENSE
Source: eslint/eslintrc
Link: https://github.com/eslint/eslintrc#readme
-----------
@eslint/js @eslint/js
License: MIT License: MIT
License File: node_modules/@eslint/js/LICENSE License File: node_modules/@eslint/js/LICENSE
@@ -3455,13 +3567,6 @@ Copyright: Copyright 2024 Justin Ridgewell <******@*********.****>
Source: git+https://github.com/jridgewell/sourcemaps.git Source: git+https://github.com/jridgewell/sourcemaps.git
Link: https://github.com/jridgewell/sourcemaps/tree/main/packages/gen-mapping Link: https://github.com/jridgewell/sourcemaps/tree/main/packages/gen-mapping
----------- -----------
@jridgewell/remapping
License: MIT
License File: node_modules/@jridgewell/remapping/LICENSE
Copyright: Copyright 2024 Justin Ridgewell <******@*********.****>
Source: git+https://github.com/jridgewell/sourcemaps.git
Link: https://github.com/jridgewell/sourcemaps/tree/main/packages/remapping
-----------
@jridgewell/resolve-uri @jridgewell/resolve-uri
License: MIT License: MIT
License File: node_modules/@jridgewell/resolve-uri/LICENSE License File: node_modules/@jridgewell/resolve-uri/LICENSE
@@ -3599,12 +3704,18 @@ License: MIT
Source: git+https://github.com/un-ts/pkgr.git Source: git+https://github.com/un-ts/pkgr.git
Link: https://github.com/un-ts/pkgr/blob/master/packages/core Link: https://github.com/un-ts/pkgr/blob/master/packages/core
----------- -----------
@rtsao/scc
License: MIT
License File: node_modules/@rtsao/scc/LICENSE
Copyright: Copyright (c) 2019 Ryan Tsao
Source: rtsao/scc
Link: rtsao/scc
-----------
@sinclair/typebox @sinclair/typebox
License: MIT License: MIT
License File: node_modules/@sinclair/typebox/license License File: node_modules/@sinclair/typebox/license
Copyright: Copyright (c) 2017-2026 Haydn Paterson Source: https://github.com/sinclairzx81/typebox
Source: https://github.com/sinclairzx81/typebox-legacy Link: https://github.com/sinclairzx81/typebox
Link: https://github.com/sinclairzx81/typebox-legacy
----------- -----------
@sinonjs/commons @sinonjs/commons
License: BSD-3-Clause License: BSD-3-Clause
@@ -3687,13 +3798,6 @@ Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/babel__traverse Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/babel__traverse
----------- -----------
@types/esrecurse
License: MIT
License File: node_modules/@types/esrecurse/LICENSE
Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/esrecurse
-----------
@types/estree @types/estree
License: MIT License: MIT
License File: node_modules/@types/estree/LICENSE License File: node_modules/@types/estree/LICENSE
@@ -3743,6 +3847,11 @@ Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/json-schema Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/json-schema
----------- -----------
@types/json5
License: MIT
Source: https://www.github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://www.github.com/DefinitelyTyped/DefinitelyTyped.git
-----------
@types/linkify-it @types/linkify-it
License: MIT License: MIT
License File: node_modules/@types/linkify-it/LICENSE License File: node_modules/@types/linkify-it/LICENSE

View File

@@ -109,7 +109,7 @@ firebase/php-jwt
License: BSD-3-Clause License: BSD-3-Clause
License File: vendor/firebase/php-jwt/LICENSE License File: vendor/firebase/php-jwt/LICENSE
Copyright: Copyright (c) 2011, Neuman Vong Copyright: Copyright (c) 2011, Neuman Vong
Source: https://github.com/googleapis/php-jwt.git Source: https://github.com/firebase/php-jwt.git
Link: https://github.com/firebase/php-jwt Link: https://github.com/firebase/php-jwt
----------- -----------
fruitcake/php-cors fruitcake/php-cors

View File

@@ -99,8 +99,6 @@ return [
'user_update_notification' => 'تم تحديث المستخدم بنجاح', 'user_update_notification' => 'تم تحديث المستخدم بنجاح',
'user_delete' => 'المستخدم المحذوف', 'user_delete' => 'المستخدم المحذوف',
'user_delete_notification' => 'تم إزالة المستخدم بنجاح', 'user_delete_notification' => 'تم إزالة المستخدم بنجاح',
'user_mfa_reset' => 'reset MFA for user',
'user_mfa_reset_notification' => 'Multi-factor authentication methods reset',
// API Tokens // API Tokens
'api_token_create' => 'تم إنشاء رمز واجهة برمجة التطبيقات -API-', 'api_token_create' => 'تم إنشاء رمز واجهة برمجة التطبيقات -API-',

View File

@@ -8,7 +8,6 @@ return [
'failed' => 'البيانات المعطاة لا توافق سجلاتنا.', 'failed' => 'البيانات المعطاة لا توافق سجلاتنا.',
'throttle' => 'تجاوزت الحد الأقصى من المحاولات. الرجاء المحاولة مرة أخرى بعد :seconds ثانية/ثواني.', 'throttle' => 'تجاوزت الحد الأقصى من المحاولات. الرجاء المحاولة مرة أخرى بعد :seconds ثانية/ثواني.',
'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.',
// Login & Register // Login & Register
'sign_up' => 'إنشاء حساب', 'sign_up' => 'إنشاء حساب',

View File

@@ -173,7 +173,6 @@ return [
'books_sort_desc' => 'نقل الفصول والصفحات داخل الكتاب لإعادة تنظيم محتوياته. يمكن إضافة كتب أخرى مما يسمح بنقل الفصول والصفحات بسهولة بين الكتب. اختياريًا، يمكن تعيين قاعدة فرز تلقائي لفرز محتويات هذا الكتاب تلقائيًا عند حدوث تغييرات.', 'books_sort_desc' => 'نقل الفصول والصفحات داخل الكتاب لإعادة تنظيم محتوياته. يمكن إضافة كتب أخرى مما يسمح بنقل الفصول والصفحات بسهولة بين الكتب. اختياريًا، يمكن تعيين قاعدة فرز تلقائي لفرز محتويات هذا الكتاب تلقائيًا عند حدوث تغييرات.',
'books_sort_auto_sort' => 'خِيار الفرز التلقائي', 'books_sort_auto_sort' => 'خِيار الفرز التلقائي',
'books_sort_auto_sort_active' => 'الفرز التلقائي الشَغَّال: :sortName', 'books_sort_auto_sort_active' => 'الفرز التلقائي الشَغَّال: :sortName',
'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.',
'books_sort_named' => 'فرز كتاب :bookName', 'books_sort_named' => 'فرز كتاب :bookName',
'books_sort_name' => 'ترتيب حسب الإسم', 'books_sort_name' => 'ترتيب حسب الإسم',
'books_sort_created' => 'ترتيب حسب تاريخ الإنشاء', 'books_sort_created' => 'ترتيب حسب تاريخ الإنشاء',
@@ -331,9 +330,6 @@ return [
// Editor Sidebar // Editor Sidebar
'toggle_sidebar' => 'تبديل الشريط الجانبي', 'toggle_sidebar' => 'تبديل الشريط الجانبي',
'page_contents' => 'Page Contents',
'page_contents_none' => 'No headings were found in the page content.',
'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.',
'page_tags' => 'وسوم الصفحة', 'page_tags' => 'وسوم الصفحة',
'chapter_tags' => 'وسوم الفصل', 'chapter_tags' => 'وسوم الفصل',
'book_tags' => 'وسوم الكتاب', 'book_tags' => 'وسوم الكتاب',

View File

@@ -207,7 +207,6 @@ return [
'role_all' => 'الكل', 'role_all' => 'الكل',
'role_own' => 'ما يخص', 'role_own' => 'ما يخص',
'role_controlled_by_asset' => 'يتحكم فيها الأصول التي يتم رفعها إلى', 'role_controlled_by_asset' => 'يتحكم فيها الأصول التي يتم رفعها إلى',
'role_controlled_by_page_delete' => 'Controlled by page delete permissions',
'role_save' => 'حفظ الدور', 'role_save' => 'حفظ الدور',
'role_users' => 'مستخدمون داخل هذا الدور', 'role_users' => 'مستخدمون داخل هذا الدور',
'role_users_none' => 'لم يتم تعيين أي مستخدمين لهذا الدور', 'role_users_none' => 'لم يتم تعيين أي مستخدمين لهذا الدور',
@@ -264,9 +263,6 @@ return [
'users_mfa_desc' => 'إعداد المصادقة متعددة العوامل كطبقة إضافية من الأمان لحساب المستخدم الخاص بك.', 'users_mfa_desc' => 'إعداد المصادقة متعددة العوامل كطبقة إضافية من الأمان لحساب المستخدم الخاص بك.',
'users_mfa_x_methods' => ':count طريقة مُهيأة | :count طرق مُهيأة', 'users_mfa_x_methods' => ':count طريقة مُهيأة | :count طرق مُهيأة',
'users_mfa_configure' => 'إعداد الطرق', 'users_mfa_configure' => 'إعداد الطرق',
'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods',
'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.',
'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?',
// API Tokens // API Tokens
'user_api_token_create' => 'قم بإنشاء رمز API', 'user_api_token_create' => 'قم بإنشاء رمز API',
@@ -367,7 +363,6 @@ return [
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',
'uk' => 'Українська', 'uk' => 'Українська',
'uz' => 'Ozbekcha', 'uz' => 'Ozbekcha',

View File

@@ -99,8 +99,6 @@ return [
'user_update_notification' => 'Потребителят е обновен успешно', 'user_update_notification' => 'Потребителят е обновен успешно',
'user_delete' => 'deleted user', 'user_delete' => 'deleted user',
'user_delete_notification' => 'Потребителят е премахнат успешно', 'user_delete_notification' => 'Потребителят е премахнат успешно',
'user_mfa_reset' => 'reset MFA for user',
'user_mfa_reset_notification' => 'Multi-factor authentication methods reset',
// API Tokens // API Tokens
'api_token_create' => 'created API token', 'api_token_create' => 'created API token',

View File

@@ -8,7 +8,6 @@ return [
'failed' => 'Въведените данни не съвпадат с информацията в системата.', 'failed' => 'Въведените данни не съвпадат с информацията в системата.',
'throttle' => 'Твърде много опити за влизане. Опитайте пак след :seconds секунди.', 'throttle' => 'Твърде много опити за влизане. Опитайте пак след :seconds секунди.',
'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.',
// Login & Register // Login & Register
'sign_up' => 'Регистриране', 'sign_up' => 'Регистриране',

View File

@@ -173,7 +173,6 @@ return [
'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.',
'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort' => 'Auto Sort Option',
'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',
'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.',
'books_sort_named' => 'Сортирай книга :bookName', 'books_sort_named' => 'Сортирай книга :bookName',
'books_sort_name' => 'Сортиране по име', 'books_sort_name' => 'Сортиране по име',
'books_sort_created' => 'Сортирай по дата на създаване', 'books_sort_created' => 'Сортирай по дата на създаване',
@@ -331,9 +330,6 @@ return [
// Editor Sidebar // Editor Sidebar
'toggle_sidebar' => 'Toggle Sidebar', 'toggle_sidebar' => 'Toggle Sidebar',
'page_contents' => 'Page Contents',
'page_contents_none' => 'No headings were found in the page content.',
'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.',
'page_tags' => 'Тагове на страницата', 'page_tags' => 'Тагове на страницата',
'chapter_tags' => 'Тагове на главата', 'chapter_tags' => 'Тагове на главата',
'book_tags' => 'Тагове на книгата', 'book_tags' => 'Тагове на книгата',

View File

@@ -207,7 +207,6 @@ return [
'role_all' => 'Всички', 'role_all' => 'Всички',
'role_own' => 'Собствени', 'role_own' => 'Собствени',
'role_controlled_by_asset' => 'Контролирани от актива, към който са качени', 'role_controlled_by_asset' => 'Контролирани от актива, към който са качени',
'role_controlled_by_page_delete' => 'Controlled by page delete permissions',
'role_save' => 'Запази ролята', 'role_save' => 'Запази ролята',
'role_users' => 'Потребители в тази роля', 'role_users' => 'Потребители в тази роля',
'role_users_none' => 'В момента няма потребители, назначени за тази роля', 'role_users_none' => 'В момента няма потребители, назначени за тази роля',
@@ -264,9 +263,6 @@ return [
'users_mfa_desc' => 'Настрой многофакторно удостверяване като втори слой сигурност на твоя профил.', 'users_mfa_desc' => 'Настрой многофакторно удостверяване като втори слой сигурност на твоя профил.',
'users_mfa_x_methods' => ':count метод е настроен|:count методи са настроени', 'users_mfa_x_methods' => ':count метод е настроен|:count методи са настроени',
'users_mfa_configure' => 'Конфигурирай методи', 'users_mfa_configure' => 'Конфигурирай методи',
'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods',
'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.',
'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?',
// API Tokens // API Tokens
'user_api_token_create' => 'Създай API маркер', 'user_api_token_create' => 'Създай API маркер',
@@ -367,7 +363,6 @@ return [
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',
'uk' => 'Українська', 'uk' => 'Українська',
'uz' => 'Ozbekcha', 'uz' => 'Ozbekcha',

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