mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-05-04 18:08:46 +03:00
Compare commits
37 Commits
sort_rule_
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ddfa9b948 | ||
|
|
55317039ac | ||
|
|
24e6087ef8 | ||
|
|
7c1d30bc8f | ||
|
|
c1610c4532 | ||
|
|
2e2f59fa0f | ||
|
|
cc6e9e0546 | ||
|
|
0f59981932 | ||
|
|
a37f903dc7 | ||
|
|
74aa897626 | ||
|
|
4b624596c8 | ||
|
|
00239bb6c8 | ||
|
|
241563e8fc | ||
|
|
e91747785b | ||
|
|
4f370ccddb | ||
|
|
743a21a02f | ||
|
|
0c9fabb6de | ||
|
|
426f9ac493 | ||
|
|
ec0b0384a2 | ||
|
|
e7e019d3d4 | ||
|
|
1339f668eb | ||
|
|
befa3a8fbb | ||
|
|
083fb1a600 | ||
|
|
a2bb5bdf10 | ||
|
|
e274a5fa4e | ||
|
|
18364d1e6e | ||
|
|
0760e677b2 | ||
|
|
208629ee1f | ||
|
|
346dc27979 | ||
|
|
1c1ad1d1b7 | ||
|
|
f14fc68b66 | ||
|
|
93f84a81b2 | ||
|
|
4feb50e7ee | ||
|
|
c7e2b487c1 | ||
|
|
abed4eae0c | ||
|
|
c7d3775bb9 | ||
|
|
0b659671fe |
2
.forgejo/CODE_OF_CONDUCT.md
Normal file
2
.forgejo/CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Please find our community rules on our website here:
|
||||||
|
https://www.bookstackapp.com/about/community-rules/
|
||||||
4
.forgejo/FUNDING.yml
Normal file
4
.forgejo/FUNDING.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [ssddanbrown]
|
||||||
|
ko_fi: ssddanbrown
|
||||||
13
.forgejo/ISSUE_TEMPLATE/config.yml
Normal file
13
.forgejo/ISSUE_TEMPLATE/config.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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.
|
||||||
@@ -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://github.com/BookStackApp/BookStack/issues?q=is%3Aissue) 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://codeberg.org/bookstack/bookstack/issues) 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
|
||||||
@@ -56,3 +56,13 @@ body:
|
|||||||
description: Add any other context or screenshots about the feature request here.
|
description: Add any other context or screenshots about the feature request here.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: ai-thoughts
|
||||||
|
attributes:
|
||||||
|
label: Have you used generative AI/LLMs to create any thoughts in this request?
|
||||||
|
description: |
|
||||||
|
We ask that no machine generated thoughts or ideas are provided, to avoid us spending time considering the ideas
|
||||||
|
of a machine instead of a human. Further guidance on this can be found [in the BookStack community rules](https://www.bookstackapp.com/about/community-rules/#use-of-llmsai).
|
||||||
|
options:
|
||||||
|
- label: This request only contains the thoughts & ideas of a human
|
||||||
|
required: true
|
||||||
@@ -15,11 +15,11 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: searchissue
|
id: searchissue
|
||||||
attributes:
|
attributes:
|
||||||
label: Searched GitHub Issues
|
label: Searched Existing Issues
|
||||||
description: |
|
description: |
|
||||||
I have searched for the issue and potential resolutions within the [project's GitHub issue list](https://github.com/BookStackApp/BookStack/issues)
|
I have searched for the issue and potential resolutions within the [project's issue list](https://codeberg.org/bookstack/bookstack/issues)
|
||||||
options:
|
options:
|
||||||
- label: I have searched GitHub for the issue.
|
- label: I have searched for the issue.
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: scenario
|
id: scenario
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Only the [latest version](https://github.com/BookStackApp/BookStack/releases) of BookStack is supported.
|
Only the [latest version](https://codeberg.org/bookstack/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,16 +12,14 @@ 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, in a development-only branch)
|
If you've found an issue that likely has no impact to existing users (For example, an issue only in the development branch)
|
||||||
feel free to raise it via a standard GitHub bug report issue.
|
feel free to raise it via a standard Codeberg 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 [@ssddanbrown](https://github.com/ssddanbrown).
|
please directly contact the lead maintainer via email Dan Brown using the [details found here](https://www.bookstackapp.com/links/contact/).
|
||||||
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!
|
||||||
11
.forgejo/pull_request_template.md
Normal file
11
.forgejo/pull_request_template.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
## 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.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
name: analyse-php
|
name: analyse-php
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- '**.php'
|
- '**.php'
|
||||||
@@ -11,14 +12,16 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: docker.io/library/node:24-trixie
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: https://github.com/shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: 8.3
|
php-version: 8.5
|
||||||
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
|
||||||
@@ -27,14 +30,16 @@ 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: actions/cache@v4
|
uses: https://code.forgejo.org/actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
key: ${{ runner.os }}-composer-8.3
|
key: ${{ runner.os }}-composer-8.5
|
||||||
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
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
name: lint-js
|
name: lint-js
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- '**.js'
|
- '**.js'
|
||||||
@@ -13,9 +14,11 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: docker.io/library/node:24-trixie
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||||
|
|
||||||
- name: Install NPM deps
|
- name: Install NPM deps
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
name: lint-php
|
name: lint-php
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- '**.php'
|
- '**.php'
|
||||||
@@ -11,14 +12,16 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: docker.io/library/node:24-trixie
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: https://github.com/shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: 8.3
|
php-version: 8.5
|
||||||
tools: phpcs
|
tools: phpcs
|
||||||
|
|
||||||
- name: Run formatting check
|
- name: Run formatting check
|
||||||
33
.forgejo/workflows/sync-translations.yml
Normal file
33
.forgejo/workflows/sync-translations.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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:
|
||||||
|
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 }}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
name: test-js
|
name: test-js
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- '**.js'
|
- '**.js'
|
||||||
@@ -15,9 +16,11 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: docker.io/library/node:24-trixie
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||||
|
|
||||||
- name: Install NPM deps
|
- name: Install NPM deps
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
name: test-migrations
|
name: test-migrations
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- '**.php'
|
- '**.php'
|
||||||
@@ -13,15 +14,25 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: docker
|
||||||
|
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: actions/checkout@v4
|
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: https://github.com/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
|
||||||
@@ -32,34 +43,31 @@ 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: actions/cache@v4
|
uses: https://code.forgejo.org/actions/cache@v5
|
||||||
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
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
name: test-php
|
name: test-php
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- '**.php'
|
- '**.php'
|
||||||
@@ -13,15 +14,25 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: docker
|
||||||
|
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: actions/checkout@v4
|
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: https://github.com/shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php }}
|
php-version: ${{ matrix.php }}
|
||||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp
|
extensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp
|
||||||
@@ -32,30 +43,25 @@ 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: actions/cache@v4
|
uses: https://code.forgejo.org/actions/cache@v5
|
||||||
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 Database
|
|
||||||
run: |
|
|
||||||
sudo systemctl start mysql
|
|
||||||
|
|
||||||
- name: Setup Database
|
|
||||||
run: |
|
|
||||||
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
|
|
||||||
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED 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: Migrate and seed the database
|
- name: Migrate and seed the database
|
||||||
|
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
|
||||||
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@mysql/bookstack-test'
|
||||||
run: php${{ matrix.php }} ./vendor/bin/phpunit
|
run: php${{ matrix.php }} ./vendor/bin/phpunit
|
||||||
86
.github/CODE_OF_CONDUCT.md
vendored
86
.github/CODE_OF_CONDUCT.md
vendored
@@ -1,84 +1,2 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
Please find our community rules on our website here:
|
||||||
|
https://www.bookstackapp.com/about/community-rules/
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
|
||||||
contributors and maintainers pledge to making participation in our project and
|
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
||||||
education, socio-economic status, nationality, personal appearance, race,
|
|
||||||
religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
|
||||||
include:
|
|
||||||
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
||||||
advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or electronic
|
|
||||||
address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
### Project Maintainer Standards
|
|
||||||
|
|
||||||
Project maintainers should generally follow these additional standards:
|
|
||||||
|
|
||||||
* Avoid using a negative or harsh tone in communication, Even if the other party
|
|
||||||
is being negative themselves.
|
|
||||||
* When providing criticism, try to make it constructive to lead the other person
|
|
||||||
down the correct path.
|
|
||||||
* Keep the [project definition](https://github.com/BookStackApp/BookStack#project-definition)
|
|
||||||
in mind when deciding what's in scope of the Project.
|
|
||||||
|
|
||||||
## Our Responsibilities
|
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
|
||||||
response to any instances of unacceptable behavior. In addition, Project
|
|
||||||
maintainers are responsible for following the standards themselves.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
||||||
permanently any contributor for other behaviors that they deem inappropriate,
|
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
|
||||||
when an individual is representing the project or its community. Examples of
|
|
||||||
representing a project or community include using an official project e-mail
|
|
||||||
address, posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event. Representation of a project may be
|
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported by contacting the project team at the email address shown on [the profile here](https://github.com/ssddanbrown). All
|
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
||||||
Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
||||||
faith may face temporary or permanent repercussions as determined by other
|
|
||||||
members of the project's leadership.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
||||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +1,8 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Discord Chat Support
|
- name: Open Issues Here Instead
|
||||||
url: https://discord.gg/ztkBqR2
|
url: https://codeberg.org/bookstack/bookstack/issues
|
||||||
about: Realtime support & chat with the BookStack community and the team.
|
about: This project has migrated to Codeberg, please open issues there instead.
|
||||||
|
|
||||||
- name: Debugging & Common Issues
|
- name: Debugging & Common Issues
|
||||||
url: https://www.bookstackapp.com/docs/admin/debugging/
|
url: https://www.bookstackapp.com/docs/admin/debugging/
|
||||||
|
|||||||
10
.github/pull_request_template.md
vendored
Normal file
10
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
**Warning:**
|
||||||
|
|
||||||
|
This project has migrated to Codeberg:
|
||||||
|
https://codeberg.org/bookstack/bookstack
|
||||||
|
|
||||||
|
Please open pull requests here instead.
|
||||||
|
|
||||||
|
ANY PULL REQUESTS OPENED HERE WILL BE CLOSED WITHOUT COMMENT OR MERGE.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -45,11 +45,11 @@ class ForgotPasswordController extends Controller
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ($response === Password::RESET_LINK_SENT) {
|
if ($response === Password::RESET_LINK_SENT) {
|
||||||
$this->logActivity(ActivityType::AUTH_PASSWORD_RESET, $request->get('email'));
|
$this->logActivity(ActivityType::AUTH_PASSWORD_RESET, $request->input('email'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array($response, [Password::RESET_LINK_SENT, Password::INVALID_USER, Password::RESET_THROTTLED])) {
|
if (in_array($response, [Password::RESET_LINK_SENT, Password::INVALID_USER, Password::RESET_THROTTLED])) {
|
||||||
$message = trans('auth.reset_password_sent', ['email' => $request->get('email')]);
|
$message = trans('auth.reset_password_sent', ['email' => $request->input('email')]);
|
||||||
$this->showSuccessNotification($message);
|
$this->showSuccessNotification($message);
|
||||||
|
|
||||||
return redirect('/password/email')->with('status', trans($response));
|
return redirect('/password/email')->with('status', trans($response));
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ class LoginController extends Controller
|
|||||||
{
|
{
|
||||||
$socialDrivers = $this->socialDriverManager->getActive();
|
$socialDrivers = $this->socialDriverManager->getActive();
|
||||||
$authMethod = config('auth.method');
|
$authMethod = config('auth.method');
|
||||||
$preventInitiation = $request->get('prevent_auto_init') === 'true';
|
$preventInitiation = $request->input('prevent_auto_init') === 'true';
|
||||||
|
|
||||||
if ($request->has('email')) {
|
if ($request->has('email')) {
|
||||||
session()->flashInput([
|
session()->flashInput([
|
||||||
'email' => $request->get('email'),
|
'email' => $request->input('email'),
|
||||||
'password' => (config('app.env') === 'demo') ? $request->get('password', '') : '',
|
'password' => (config('app.env') === 'demo') ? $request->input('password', '') : '',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ class LoginController extends Controller
|
|||||||
public function login(Request $request)
|
public function login(Request $request)
|
||||||
{
|
{
|
||||||
$this->validateLogin($request);
|
$this->validateLogin($request);
|
||||||
$username = $request->get($this->username());
|
$username = $request->input($this->username());
|
||||||
|
|
||||||
// Check login throttling attempts to see if they've gone over the limit
|
// Check login throttling attempts to see if they've gone over the limit
|
||||||
if ($this->hasTooManyLoginAttempts($request)) {
|
if ($this->hasTooManyLoginAttempts($request)) {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class MfaBackupCodesController extends Controller
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$updatedCodes = $codeService->removeInputCodeFromSet($request->get('code'), $codes);
|
$updatedCodes = $codeService->removeInputCodeFromSet($request->input('code'), $codes);
|
||||||
MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, $updatedCodes);
|
MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, $updatedCodes);
|
||||||
|
|
||||||
$mfaSession->markVerifiedForUser($user);
|
$mfaSession->markVerifiedForUser($user);
|
||||||
|
|||||||
@@ -51,14 +51,14 @@ class MfaController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function verify(Request $request)
|
public function verify(Request $request)
|
||||||
{
|
{
|
||||||
$desiredMethod = $request->get('method');
|
$desiredMethod = $request->input('method');
|
||||||
$userMethods = $this->currentOrLastAttemptedUser()
|
$userMethods = $this->currentOrLastAttemptedUser()
|
||||||
->mfaValues()
|
->mfaValues()
|
||||||
->get(['id', 'method'])
|
->get(['id', 'method'])
|
||||||
->groupBy('method');
|
->groupBy('method');
|
||||||
|
|
||||||
// Basic search for the default option for a user.
|
// Basic search for the default option for a user.
|
||||||
// (Prioritises totp over backup codes)
|
// (Prioritises TOTP over backup codes)
|
||||||
$method = $userMethods->has($desiredMethod) ? $desiredMethod : $userMethods->keys()->sort()->reverse()->first();
|
$method = $userMethods->has($desiredMethod) ? $desiredMethod : $userMethods->keys()->sort()->reverse()->first();
|
||||||
$otherMethods = $userMethods->keys()->filter(function ($userMethod) use ($method) {
|
$otherMethods = $userMethods->keys()->filter(function ($userMethod) use ($method) {
|
||||||
return $method !== $userMethod;
|
return $method !== $userMethod;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ResetPasswordController extends Controller
|
|||||||
|
|
||||||
// Here we will attempt to reset the user's password. If it is successful we
|
// Here we will attempt to reset the user's password. If it is successful we
|
||||||
// will update the password on an actual user model and persist it to the
|
// will update the password on an actual user model and persist it to the
|
||||||
// database. Otherwise we will parse the error and return the response.
|
// database. Otherwise, we will parse the error and return the response.
|
||||||
$credentials = $request->only('email', 'password', 'password_confirmation', 'token');
|
$credentials = $request->only('email', 'password', 'password_confirmation', 'token');
|
||||||
$response = Password::broker()->reset($credentials, function (User $user, string $password) {
|
$response = Password::broker()->reset($credentials, function (User $user, string $password) {
|
||||||
$user->password = Hash::make($password);
|
$user->password = Hash::make($password);
|
||||||
@@ -63,7 +63,7 @@ class ResetPasswordController extends Controller
|
|||||||
// redirect them back to where they came from with their error message.
|
// redirect them back to where they came from with their error message.
|
||||||
return $response === Password::PASSWORD_RESET
|
return $response === Password::PASSWORD_RESET
|
||||||
? $this->sendResetResponse()
|
? $this->sendResetResponse()
|
||||||
: $this->sendResetFailedResponse($request, $response, $request->get('token'));
|
: $this->sendResetFailedResponse($request, $response, $request->input('token'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class Saml2Controller extends Controller
|
|||||||
*/
|
*/
|
||||||
public function startAcs(Request $request)
|
public function startAcs(Request $request)
|
||||||
{
|
{
|
||||||
$samlResponse = $request->get('SAMLResponse', null);
|
$samlResponse = $request->input('SAMLResponse', null);
|
||||||
|
|
||||||
if (empty($samlResponse)) {
|
if (empty($samlResponse)) {
|
||||||
$this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')]));
|
$this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')]));
|
||||||
@@ -100,7 +100,7 @@ class Saml2Controller extends Controller
|
|||||||
*/
|
*/
|
||||||
public function processAcs(Request $request)
|
public function processAcs(Request $request)
|
||||||
{
|
{
|
||||||
$acsId = $request->get('id', null);
|
$acsId = $request->input('id', null);
|
||||||
$cacheKey = 'saml2_acs:' . $acsId;
|
$cacheKey = 'saml2_acs:' . $acsId;
|
||||||
$samlResponse = null;
|
$samlResponse = null;
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class SocialController extends Controller
|
|||||||
if ($request->has('error') && $request->has('error_description')) {
|
if ($request->has('error') && $request->has('error_description')) {
|
||||||
throw new SocialSignInException(trans('errors.social_login_bad_response', [
|
throw new SocialSignInException(trans('errors.social_login_bad_response', [
|
||||||
'socialAccount' => $socialDriver,
|
'socialAccount' => $socialDriver,
|
||||||
'error' => $request->get('error_description'),
|
'error' => $request->input('error_description'),
|
||||||
]), '/login');
|
]), '/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class UserInviteController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$user = $this->userRepo->getById($userId);
|
$user = $this->userRepo->getById($userId);
|
||||||
$user->password = Hash::make($request->get('password'));
|
$user->password = Hash::make($request->input('password'));
|
||||||
$user->email_confirmed = true;
|
$user->email_confirmed = true;
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
|
|||||||
@@ -17,19 +17,19 @@ class AuditLogController extends Controller
|
|||||||
$this->checkPermission(Permission::SettingsManage);
|
$this->checkPermission(Permission::SettingsManage);
|
||||||
$this->checkPermission(Permission::UsersManage);
|
$this->checkPermission(Permission::UsersManage);
|
||||||
|
|
||||||
$sort = $request->get('sort', 'activity_date');
|
$sort = $request->input('sort', 'activity_date');
|
||||||
$order = $request->get('order', 'desc');
|
$order = $request->input('order', 'desc');
|
||||||
$listOptions = (new SimpleListOptions('', $sort, $order))->withSortOptions([
|
$listOptions = (new SimpleListOptions('', $sort, $order))->withSortOptions([
|
||||||
'created_at' => trans('settings.audit_table_date'),
|
'created_at' => trans('settings.audit_table_date'),
|
||||||
'type' => trans('settings.audit_table_event'),
|
'type' => trans('settings.audit_table_event'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$filters = [
|
$filters = [
|
||||||
'event' => $request->get('event', ''),
|
'event' => $request->input('event', ''),
|
||||||
'date_from' => $request->get('date_from', ''),
|
'date_from' => $request->input('date_from', ''),
|
||||||
'date_to' => $request->get('date_to', ''),
|
'date_to' => $request->input('date_to', ''),
|
||||||
'user' => $request->get('user', ''),
|
'user' => $request->input('user', ''),
|
||||||
'ip' => $request->get('ip', ''),
|
'ip' => $request->input('ip', ''),
|
||||||
];
|
];
|
||||||
|
|
||||||
$query = Activity::query()
|
$query = Activity::query()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class FavouriteController extends Controller
|
|||||||
public function index(Request $request, QueryTopFavourites $topFavourites)
|
public function index(Request $request, QueryTopFavourites $topFavourites)
|
||||||
{
|
{
|
||||||
$viewCount = 20;
|
$viewCount = 20;
|
||||||
$page = intval($request->get('page', 1));
|
$page = intval($request->input('page', 1));
|
||||||
$favourites = $topFavourites->run($viewCount + 1, (($page - 1) * $viewCount));
|
$favourites = $topFavourites->run($viewCount + 1, (($page - 1) * $viewCount));
|
||||||
|
|
||||||
$hasMoreLink = ($favourites->count() > $viewCount) ? url('/favourites?page=' . ($page + 1)) : null;
|
$hasMoreLink = ($favourites->count() > $viewCount) ? url('/favourites?page=' . ($page + 1)) : null;
|
||||||
|
|||||||
68
app/Activity/Controllers/TagApiController.php
Normal file
68
app/Activity/Controllers/TagApiController.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Controllers;
|
||||||
|
|
||||||
|
use BookStack\Activity\TagRepo;
|
||||||
|
use BookStack\Http\ApiController;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoints to query data about tags in the system.
|
||||||
|
* You'll only see results based on tags applied to content you have access to.
|
||||||
|
* There are no general create/update/delete endpoints here since tags do not exist
|
||||||
|
* by themselves, they are managed via the items they are assigned to.
|
||||||
|
*/
|
||||||
|
class TagApiController extends ApiController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected TagRepo $tagRepo,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'listValues' => [
|
||||||
|
'name' => ['required', 'string'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of tag names used in the system.
|
||||||
|
* Only the name field can be used in filters.
|
||||||
|
*/
|
||||||
|
public function listNames(): JsonResponse
|
||||||
|
{
|
||||||
|
$tagQuery = $this->tagRepo
|
||||||
|
->queryWithTotalsForApi('');
|
||||||
|
|
||||||
|
return $this->apiListingResponse($tagQuery, [
|
||||||
|
'name', 'values', 'usages', 'page_count', 'chapter_count', 'book_count', 'shelf_count',
|
||||||
|
], [], [
|
||||||
|
'name'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of tag values, which have been set for the given tag name,
|
||||||
|
* which must be provided as a query parameter on the request.
|
||||||
|
* Only the value field can be used in filters.
|
||||||
|
*/
|
||||||
|
public function listValues(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = $this->validate($request, $this->rules()['listValues']);
|
||||||
|
$name = $data['name'];
|
||||||
|
|
||||||
|
$tagQuery = $this->tagRepo->queryWithTotalsForApi($name);
|
||||||
|
|
||||||
|
return $this->apiListingResponse($tagQuery, [
|
||||||
|
'name', 'value', 'usages', 'page_count', 'chapter_count', 'book_count', 'shelf_count',
|
||||||
|
], [], [
|
||||||
|
'value',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,9 +24,9 @@ class TagController extends Controller
|
|||||||
'usages' => trans('entities.tags_usages'),
|
'usages' => trans('entities.tags_usages'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$nameFilter = $request->get('name', '');
|
$nameFilter = $request->input('name', '');
|
||||||
$tags = $this->tagRepo
|
$tags = $this->tagRepo
|
||||||
->queryWithTotals($listOptions, $nameFilter)
|
->queryWithTotalsForList($listOptions, $nameFilter)
|
||||||
->paginate(50)
|
->paginate(50)
|
||||||
->appends(array_filter(array_merge($listOptions->getPaginationAppends(), [
|
->appends(array_filter(array_merge($listOptions->getPaginationAppends(), [
|
||||||
'name' => $nameFilter,
|
'name' => $nameFilter,
|
||||||
@@ -46,7 +46,7 @@ class TagController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function getNameSuggestions(Request $request)
|
public function getNameSuggestions(Request $request)
|
||||||
{
|
{
|
||||||
$searchTerm = $request->get('search', '');
|
$searchTerm = $request->input('search', '');
|
||||||
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
|
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
|
||||||
|
|
||||||
return response()->json($suggestions);
|
return response()->json($suggestions);
|
||||||
@@ -57,8 +57,8 @@ class TagController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function getValueSuggestions(Request $request)
|
public function getValueSuggestions(Request $request)
|
||||||
{
|
{
|
||||||
$searchTerm = $request->get('search', '');
|
$searchTerm = $request->input('search', '');
|
||||||
$tagName = $request->get('name', '');
|
$tagName = $request->input('name', '');
|
||||||
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
|
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
|
||||||
|
|
||||||
return response()->json($suggestions);
|
return response()->json($suggestions);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use BookStack\Users\Models\HasCreatorAndUpdater;
|
|||||||
use BookStack\Users\Models\OwnableInterface;
|
use BookStack\Users\Models\OwnableInterface;
|
||||||
use BookStack\Util\HtmlContentFilter;
|
use BookStack\Util\HtmlContentFilter;
|
||||||
use BookStack\Util\HtmlContentFilterConfig;
|
use BookStack\Util\HtmlContentFilterConfig;
|
||||||
|
use BookStack\Util\HtmlToPlainText;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
@@ -87,6 +88,12 @@ class Comment extends Model implements Loggable, OwnableInterface
|
|||||||
return $filter->filterString($this->html ?? '');
|
return $filter->filterString($this->html ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getPlainText(): string
|
||||||
|
{
|
||||||
|
$converter = new HtmlToPlainText();
|
||||||
|
return $converter->convert($this->html ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
public function jointPermissions(): HasMany
|
public function jointPermissions(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(JointPermission::class, 'entity_id', 'commentable_id')
|
return $this->hasMany(JointPermission::class, 'entity_id', 'commentable_id')
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class CommentCreationNotification extends BaseActivityNotification
|
|||||||
$locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),
|
$locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),
|
||||||
$locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),
|
$locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),
|
||||||
$locale->trans('notifications.detail_commenter') => $this->user->name,
|
$locale->trans('notifications.detail_commenter') => $this->user->name,
|
||||||
$locale->trans('notifications.detail_comment') => strip_tags($comment->html),
|
$locale->trans('notifications.detail_comment') => $comment->getPlainText(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->newMailMessage($locale)
|
return $this->newMailMessage($locale)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class CommentMentionNotification extends BaseActivityNotification
|
|||||||
$locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),
|
$locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),
|
||||||
$locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),
|
$locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),
|
||||||
$locale->trans('notifications.detail_commenter') => $this->user->name,
|
$locale->trans('notifications.detail_commenter') => $this->user->name,
|
||||||
$locale->trans('notifications.detail_comment') => strip_tags($comment->html),
|
$locale->trans('notifications.detail_comment') => $comment->getPlainText(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->newMailMessage($locale)
|
return $this->newMailMessage($locale)
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ class TagRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a query against all tags in the system.
|
* Start a query against all tags in the system, with total counts for their usage,
|
||||||
|
* suitable for a system interface list with listing options.
|
||||||
*/
|
*/
|
||||||
public function queryWithTotals(SimpleListOptions $listOptions, string $nameFilter): Builder
|
public function queryWithTotalsForList(SimpleListOptions $listOptions, string $nameFilter): Builder
|
||||||
{
|
{
|
||||||
$searchTerm = $listOptions->getSearch();
|
$searchTerm = $listOptions->getSearch();
|
||||||
$sort = $listOptions->getSort();
|
$sort = $listOptions->getSort();
|
||||||
@@ -28,17 +29,34 @@ class TagRepo
|
|||||||
$sort = 'value';
|
$sort = 'value';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$query = $this->baseQueryWithTotals($nameFilter, $searchTerm)
|
||||||
|
->orderBy($sort, $listOptions->getOrder());
|
||||||
|
|
||||||
|
return $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a query against all tags in the system, with total counts for their usage,
|
||||||
|
* which can be used via the API.
|
||||||
|
*/
|
||||||
|
public function queryWithTotalsForApi(string $nameFilter): Builder
|
||||||
|
{
|
||||||
|
$query = $this->baseQueryWithTotals($nameFilter, '');
|
||||||
|
return $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function baseQueryWithTotals(string $nameFilter, string $searchTerm): Builder
|
||||||
|
{
|
||||||
$query = Tag::query()
|
$query = Tag::query()
|
||||||
->select([
|
->select([
|
||||||
'name',
|
'name',
|
||||||
($searchTerm || $nameFilter) ? 'value' : DB::raw('COUNT(distinct value) as `values`'),
|
($searchTerm || $nameFilter) ? 'value' : DB::raw('COUNT(distinct value) as `values`'),
|
||||||
DB::raw('COUNT(id) as usages'),
|
DB::raw('COUNT(id) as usages'),
|
||||||
DB::raw('SUM(IF(entity_type = \'page\', 1, 0)) as page_count'),
|
DB::raw('CAST(SUM(IF(entity_type = \'page\', 1, 0)) as UNSIGNED) as page_count'),
|
||||||
DB::raw('SUM(IF(entity_type = \'chapter\', 1, 0)) as chapter_count'),
|
DB::raw('CAST(SUM(IF(entity_type = \'chapter\', 1, 0)) as UNSIGNED) as chapter_count'),
|
||||||
DB::raw('SUM(IF(entity_type = \'book\', 1, 0)) as book_count'),
|
DB::raw('CAST(SUM(IF(entity_type = \'book\', 1, 0)) as UNSIGNED) as book_count'),
|
||||||
DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'),
|
DB::raw('CAST(SUM(IF(entity_type = \'bookshelf\', 1, 0)) as UNSIGNED) as shelf_count'),
|
||||||
])
|
])
|
||||||
->orderBy($sort, $listOptions->getOrder())
|
|
||||||
->whereHas('entity');
|
->whereHas('entity');
|
||||||
|
|
||||||
if ($nameFilter) {
|
if ($nameFilter) {
|
||||||
@@ -57,7 +75,7 @@ class TagRepo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type');
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -195,11 +195,12 @@ class ApiDocsGenerator
|
|||||||
protected function getFlatApiRoutes(): Collection
|
protected function getFlatApiRoutes(): Collection
|
||||||
{
|
{
|
||||||
return collect(Route::getRoutes()->getRoutes())->filter(function ($route) {
|
return collect(Route::getRoutes()->getRoutes())->filter(function ($route) {
|
||||||
return strpos($route->uri, 'api/') === 0;
|
return str_starts_with($route->uri, 'api/');
|
||||||
})->map(function ($route) {
|
})->map(function ($route) {
|
||||||
[$controller, $controllerMethod] = explode('@', $route->action['uses']);
|
[$controller, $controllerMethod] = explode('@', $route->action['uses']);
|
||||||
$baseModelName = explode('.', explode('/', $route->uri)[1])[0];
|
$baseModelName = explode('.', explode('/', $route->uri)[1])[0];
|
||||||
$shortName = $baseModelName . '-' . $controllerMethod;
|
$controllerMethodKebab = Str::kebab($controllerMethod);
|
||||||
|
$shortName = $baseModelName . '-' . $controllerMethodKebab;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => $shortName,
|
'name' => $shortName,
|
||||||
@@ -207,7 +208,7 @@ class ApiDocsGenerator
|
|||||||
'method' => $route->methods[0],
|
'method' => $route->methods[0],
|
||||||
'controller' => $controller,
|
'controller' => $controller,
|
||||||
'controller_method' => $controllerMethod,
|
'controller_method' => $controllerMethod,
|
||||||
'controller_method_kebab' => Str::kebab($controllerMethod),
|
'controller_method_kebab' => $controllerMethodKebab,
|
||||||
'base_model' => $baseModelName,
|
'base_model' => $baseModelName,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ class ListingResponseBuilder
|
|||||||
*/
|
*/
|
||||||
protected array $fields;
|
protected array $fields;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which fields are filterable.
|
||||||
|
* When null, the $fields above are used instead (Allow all fields).
|
||||||
|
* @var string[]|null
|
||||||
|
*/
|
||||||
|
protected array|null $filterableFields = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array<callable>
|
* @var array<callable>
|
||||||
*/
|
*/
|
||||||
@@ -54,7 +61,7 @@ class ListingResponseBuilder
|
|||||||
{
|
{
|
||||||
$filteredQuery = $this->filterQuery($this->query);
|
$filteredQuery = $this->filterQuery($this->query);
|
||||||
|
|
||||||
$total = $filteredQuery->count();
|
$total = $filteredQuery->getCountForPagination();
|
||||||
$data = $this->fetchData($filteredQuery)->each(function ($model) {
|
$data = $this->fetchData($filteredQuery)->each(function ($model) {
|
||||||
foreach ($this->resultModifiers as $modifier) {
|
foreach ($this->resultModifiers as $modifier) {
|
||||||
$modifier($model);
|
$modifier($model);
|
||||||
@@ -77,6 +84,14 @@ class ListingResponseBuilder
|
|||||||
$this->resultModifiers[] = $modifier;
|
$this->resultModifiers[] = $modifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit filtering to just the given set of fields.
|
||||||
|
*/
|
||||||
|
public function setFilterableFields(array $fields): void
|
||||||
|
{
|
||||||
|
$this->filterableFields = $fields;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the data to return within the response.
|
* Fetch the data to return within the response.
|
||||||
*/
|
*/
|
||||||
@@ -94,7 +109,7 @@ class ListingResponseBuilder
|
|||||||
protected function filterQuery(Builder $query): Builder
|
protected function filterQuery(Builder $query): Builder
|
||||||
{
|
{
|
||||||
$query = clone $query;
|
$query = clone $query;
|
||||||
$requestFilters = $this->request->get('filter', []);
|
$requestFilters = $this->request->input('filter', []);
|
||||||
if (!is_array($requestFilters)) {
|
if (!is_array($requestFilters)) {
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
@@ -114,10 +129,11 @@ class ListingResponseBuilder
|
|||||||
protected function requestFilterToQueryFilter($fieldKey, $value): ?array
|
protected function requestFilterToQueryFilter($fieldKey, $value): ?array
|
||||||
{
|
{
|
||||||
$splitKey = explode(':', $fieldKey);
|
$splitKey = explode(':', $fieldKey);
|
||||||
$field = $splitKey[0];
|
$field = strtolower($splitKey[0]);
|
||||||
$filterOperator = $splitKey[1] ?? 'eq';
|
$filterOperator = $splitKey[1] ?? 'eq';
|
||||||
|
|
||||||
if (!in_array($field, $this->fields)) {
|
$filterFields = $this->filterableFields ?? $this->fields;
|
||||||
|
if (!in_array($field, $filterFields)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,8 +156,8 @@ class ListingResponseBuilder
|
|||||||
$defaultSortName = $this->fields[0];
|
$defaultSortName = $this->fields[0];
|
||||||
$direction = 'asc';
|
$direction = 'asc';
|
||||||
|
|
||||||
$sort = $this->request->get('sort', '');
|
$sort = $this->request->input('sort', '');
|
||||||
if (strpos($sort, '-') === 0) {
|
if (str_starts_with($sort, '-')) {
|
||||||
$direction = 'desc';
|
$direction = 'desc';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,9 +176,9 @@ class ListingResponseBuilder
|
|||||||
protected function countAndOffsetQuery(Builder $query): Builder
|
protected function countAndOffsetQuery(Builder $query): Builder
|
||||||
{
|
{
|
||||||
$query = clone $query;
|
$query = clone $query;
|
||||||
$offset = max(0, $this->request->get('offset', 0));
|
$offset = max(0, $this->request->input('offset', 0));
|
||||||
$maxCount = config('api.max_item_count');
|
$maxCount = config('api.max_item_count');
|
||||||
$count = $this->request->get('count', config('api.default_item_count'));
|
$count = $this->request->input('count', config('api.default_item_count'));
|
||||||
$count = max(min($maxCount, $count), 1);
|
$count = max(min($maxCount, $count), 1);
|
||||||
|
|
||||||
return $query->skip($offset)->take($count);
|
return $query->skip($offset)->take($count);
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ class UserApiTokenController extends Controller
|
|||||||
$secret = Str::random(32);
|
$secret = Str::random(32);
|
||||||
|
|
||||||
$token = (new ApiToken())->forceFill([
|
$token = (new ApiToken())->forceFill([
|
||||||
'name' => $request->get('name'),
|
'name' => $request->input('name'),
|
||||||
'token_id' => Str::random(32),
|
'token_id' => Str::random(32),
|
||||||
'secret' => Hash::make($secret),
|
'secret' => Hash::make($secret),
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(),
|
'expires_at' => $request->input('expires_at') ?: ApiToken::defaultExpiry(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
while (ApiToken::query()->where('token_id', '=', $token->token_id)->exists()) {
|
while (ApiToken::query()->where('token_id', '=', $token->token_id)->exists()) {
|
||||||
@@ -100,8 +100,8 @@ class UserApiTokenController extends Controller
|
|||||||
|
|
||||||
[$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
|
[$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
|
||||||
$token->fill([
|
$token->fill([
|
||||||
'name' => $request->get('name'),
|
'name' => $request->input('name'),
|
||||||
'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(),
|
'expires_at' => $request->input('expires_at') ?: ApiToken::defaultExpiry(),
|
||||||
])->save();
|
])->save();
|
||||||
|
|
||||||
$this->logActivity(ActivityType::API_TOKEN_UPDATE, $token);
|
$this->logActivity(ActivityType::API_TOKEN_UPDATE, $token);
|
||||||
|
|||||||
@@ -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://github.com/BookStackApp/BookStack/issues/4649
|
// Context: https://codeberg.org/bookstack/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');
|
||||||
|
|
||||||
|
|||||||
@@ -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/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
'font_dir' => storage_path('fonts/dompdf'), // 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/'),
|
'font_cache' => storage_path('fonts/dompdf/cache'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The location of a temporary directory.
|
* The location of a temporary directory.
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class BookController extends Controller
|
|||||||
|
|
||||||
View::incrementFor($book);
|
View::incrementFor($book);
|
||||||
if ($request->has('shelf')) {
|
if ($request->has('shelf')) {
|
||||||
$this->shelfContext->setShelfContext(intval($request->get('shelf')));
|
$this->shelfContext->setShelfContext(intval($request->input('shelf')));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->setPageTitle($book->getShortName());
|
$this->setPageTitle($book->getShortName());
|
||||||
@@ -263,7 +263,7 @@ class BookController extends Controller
|
|||||||
$this->checkOwnablePermission(Permission::BookView, $book);
|
$this->checkOwnablePermission(Permission::BookView, $book);
|
||||||
$this->checkPermission(Permission::BookCreateAll);
|
$this->checkPermission(Permission::BookCreateAll);
|
||||||
|
|
||||||
$newName = $request->get('name') ?: $book->name;
|
$newName = $request->input('name') ?: $book->name;
|
||||||
$bookCopy = $cloner->cloneBook($book, $newName);
|
$bookCopy = $cloner->cloneBook($book, $newName);
|
||||||
$this->showSuccessNotification(trans('entities.books_copy_success'));
|
$this->showSuccessNotification(trans('entities.books_copy_success'));
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class BookshelfApiController extends ApiController
|
|||||||
$this->checkPermission(Permission::BookshelfCreateAll);
|
$this->checkPermission(Permission::BookshelfCreateAll);
|
||||||
$requestData = $this->validate($request, $this->rules()['create']);
|
$requestData = $this->validate($request, $this->rules()['create']);
|
||||||
|
|
||||||
$bookIds = $request->get('books', []);
|
$bookIds = $request->input('books', []);
|
||||||
$shelf = $this->bookshelfRepo->create($requestData, $bookIds);
|
$shelf = $this->bookshelfRepo->create($requestData, $bookIds);
|
||||||
|
|
||||||
return response()->json($this->forJsonDisplay($shelf));
|
return response()->json($this->forJsonDisplay($shelf));
|
||||||
@@ -88,7 +88,7 @@ class BookshelfApiController extends ApiController
|
|||||||
$this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);
|
$this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);
|
||||||
|
|
||||||
$requestData = $this->validate($request, $this->rules()['update']);
|
$requestData = $this->validate($request, $this->rules()['update']);
|
||||||
$bookIds = $request->get('books', null);
|
$bookIds = $request->input('books', null);
|
||||||
|
|
||||||
$shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
|
$shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class BookshelfController extends Controller
|
|||||||
'tags' => ['array'],
|
'tags' => ['array'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$bookIds = explode(',', $request->get('books', ''));
|
$bookIds = explode(',', $request->input('books', ''));
|
||||||
$shelf = $this->shelfRepo->create($validated, $bookIds);
|
$shelf = $this->shelfRepo->create($validated, $bookIds);
|
||||||
|
|
||||||
return redirect($shelf->getUrl());
|
return redirect($shelf->getUrl());
|
||||||
@@ -196,7 +196,7 @@ class BookshelfController extends Controller
|
|||||||
unset($validated['image']);
|
unset($validated['image']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$bookIds = explode(',', $request->get('books', ''));
|
$bookIds = explode(',', $request->input('books', ''));
|
||||||
$shelf = $this->shelfRepo->update($shelf, $validated, $bookIds);
|
$shelf = $this->shelfRepo->update($shelf, $validated, $bookIds);
|
||||||
|
|
||||||
return redirect($shelf->getUrl());
|
return redirect($shelf->getUrl());
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class ChapterApiController extends ApiController
|
|||||||
{
|
{
|
||||||
$requestData = $this->validate($request, $this->rules['create']);
|
$requestData = $this->validate($request, $this->rules['create']);
|
||||||
|
|
||||||
$bookId = $request->get('book_id');
|
$bookId = $request->input('book_id');
|
||||||
$book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId));
|
$book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId));
|
||||||
$this->checkOwnablePermission(Permission::ChapterCreate, $book);
|
$this->checkOwnablePermission(Permission::ChapterCreate, $book);
|
||||||
|
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ class ChapterController extends Controller
|
|||||||
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
|
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
|
||||||
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
|
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
|
||||||
|
|
||||||
$entitySelection = $request->get('entity_selection', null);
|
$entitySelection = $request->input('entity_selection', null);
|
||||||
if ($entitySelection === null || $entitySelection === '') {
|
if ($entitySelection === null || $entitySelection === '') {
|
||||||
return redirect($chapter->getUrl());
|
return redirect($chapter->getUrl());
|
||||||
}
|
}
|
||||||
@@ -248,7 +248,7 @@ class ChapterController extends Controller
|
|||||||
{
|
{
|
||||||
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||||
|
|
||||||
$entitySelection = $request->get('entity_selection') ?: null;
|
$entitySelection = $request->input('entity_selection') ?: null;
|
||||||
$newParentBook = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $chapter->getParent();
|
$newParentBook = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $chapter->getParent();
|
||||||
|
|
||||||
if (!$newParentBook instanceof Book) {
|
if (!$newParentBook instanceof Book) {
|
||||||
@@ -259,7 +259,7 @@ class ChapterController extends Controller
|
|||||||
|
|
||||||
$this->checkOwnablePermission(Permission::ChapterCreate, $newParentBook);
|
$this->checkOwnablePermission(Permission::ChapterCreate, $newParentBook);
|
||||||
|
|
||||||
$newName = $request->get('name') ?: $chapter->name;
|
$newName = $request->input('name') ?: $chapter->name;
|
||||||
$chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName);
|
$chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName);
|
||||||
$this->showSuccessNotification(trans('entities.chapters_copy_success'));
|
$this->showSuccessNotification(trans('entities.chapters_copy_success'));
|
||||||
|
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ class PageApiController extends ApiController
|
|||||||
$this->validate($request, $this->rules['create']);
|
$this->validate($request, $this->rules['create']);
|
||||||
|
|
||||||
if ($request->has('chapter_id')) {
|
if ($request->has('chapter_id')) {
|
||||||
$parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->get('chapter_id')));
|
$parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->input('chapter_id')));
|
||||||
} else {
|
} else {
|
||||||
$parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id')));
|
$parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->input('book_id')));
|
||||||
}
|
}
|
||||||
$this->checkOwnablePermission(Permission::PageCreate, $parent);
|
$this->checkOwnablePermission(Permission::PageCreate, $parent);
|
||||||
|
|
||||||
@@ -133,9 +133,9 @@ class PageApiController extends ApiController
|
|||||||
|
|
||||||
$parent = null;
|
$parent = null;
|
||||||
if ($request->has('chapter_id')) {
|
if ($request->has('chapter_id')) {
|
||||||
$parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->get('chapter_id')));
|
$parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->input('chapter_id')));
|
||||||
} elseif ($request->has('book_id')) {
|
} elseif ($request->has('book_id')) {
|
||||||
$parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id')));
|
$parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->input('book_id')));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($parent && !$parent->matches($page->getParent())) {
|
if ($parent && !$parent->matches($page->getParent())) {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class PageController extends Controller
|
|||||||
|
|
||||||
$page = $this->pageRepo->getNewDraftPage($parent);
|
$page = $this->pageRepo->getNewDraftPage($parent);
|
||||||
$this->pageRepo->publishDraft($page, [
|
$this->pageRepo->publishDraft($page, [
|
||||||
'name' => $request->get('name'),
|
'name' => $request->input('name'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect($page->getUrl('/edit'));
|
return redirect($page->getUrl('/edit'));
|
||||||
@@ -408,7 +408,7 @@ class PageController extends Controller
|
|||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||||
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
||||||
|
|
||||||
$entitySelection = $request->get('entity_selection', null);
|
$entitySelection = $request->input('entity_selection', null);
|
||||||
if ($entitySelection === null || $entitySelection === '') {
|
if ($entitySelection === null || $entitySelection === '') {
|
||||||
return redirect($page->getUrl());
|
return redirect($page->getUrl());
|
||||||
}
|
}
|
||||||
@@ -453,7 +453,7 @@ class PageController extends Controller
|
|||||||
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$this->checkOwnablePermission(Permission::PageView, $page);
|
$this->checkOwnablePermission(Permission::PageView, $page);
|
||||||
|
|
||||||
$entitySelection = $request->get('entity_selection') ?: null;
|
$entitySelection = $request->input('entity_selection') ?: null;
|
||||||
$newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent();
|
$newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent();
|
||||||
|
|
||||||
if (!$newParent instanceof Book && !$newParent instanceof Chapter) {
|
if (!$newParent instanceof Book && !$newParent instanceof Chapter) {
|
||||||
@@ -464,7 +464,7 @@ class PageController extends Controller
|
|||||||
|
|
||||||
$this->checkOwnablePermission(Permission::PageCreate, $newParent);
|
$this->checkOwnablePermission(Permission::PageCreate, $newParent);
|
||||||
|
|
||||||
$newName = $request->get('name') ?: $page->name;
|
$newName = $request->input('name') ?: $page->name;
|
||||||
$pageCopy = $cloner->clonePage($page, $newParent, $newName);
|
$pageCopy = $cloner->clonePage($page, $newParent, $newName);
|
||||||
$this->showSuccessNotification(trans('entities.pages_copy_success'));
|
$this->showSuccessNotification(trans('entities.pages_copy_success'));
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class PageRevisionController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request, string $bookSlug, string $pageSlug)
|
public function index(Request $request, string $bookSlug, string $pageSlug)
|
||||||
{
|
{
|
||||||
|
$this->checkPermission(Permission::RevisionViewAll);
|
||||||
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([
|
$listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([
|
||||||
'id' => trans('entities.pages_revisions_sort_number')
|
'id' => trans('entities.pages_revisions_sort_number')
|
||||||
@@ -65,6 +66,8 @@ class PageRevisionController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function show(string $bookSlug, string $pageSlug, int $revisionId)
|
public function show(string $bookSlug, string $pageSlug, int $revisionId)
|
||||||
{
|
{
|
||||||
|
$this->checkPermission(Permission::RevisionViewAll);
|
||||||
|
|
||||||
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
/** @var ?PageRevision $revision */
|
/** @var ?PageRevision $revision */
|
||||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||||
@@ -94,6 +97,8 @@ class PageRevisionController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function changes(string $bookSlug, string $pageSlug, int $revisionId)
|
public function changes(string $bookSlug, string $pageSlug, int $revisionId)
|
||||||
{
|
{
|
||||||
|
$this->checkPermission(Permission::RevisionViewAll);
|
||||||
|
|
||||||
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
/** @var ?PageRevision $revision */
|
/** @var ?PageRevision $revision */
|
||||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||||
@@ -129,6 +134,7 @@ class PageRevisionController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function restore(string $bookSlug, string $pageSlug, int $revisionId)
|
public function restore(string $bookSlug, string $pageSlug, int $revisionId)
|
||||||
{
|
{
|
||||||
|
$this->checkPermission(Permission::RevisionViewAll);
|
||||||
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||||
|
|
||||||
@@ -144,6 +150,7 @@ class PageRevisionController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroy(string $bookSlug, string $pageSlug, int $revId)
|
public function destroy(string $bookSlug, string $pageSlug, int $revId)
|
||||||
{
|
{
|
||||||
|
$this->checkPermission(Permission::RevisionViewAll);
|
||||||
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||||
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ class PageTemplateController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function list(Request $request)
|
public function list(Request $request)
|
||||||
{
|
{
|
||||||
$page = $request->get('page', 1);
|
$page = $request->input('page', 1);
|
||||||
$search = $request->get('search', '');
|
$search = $request->input('search', '');
|
||||||
$count = 10;
|
$count = 10;
|
||||||
|
|
||||||
$query = $this->pageQueries->visibleTemplates()
|
$query = $this->pageQueries->visibleTemplates()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use BookStack\References\ReferenceUpdater;
|
|||||||
use BookStack\Sorting\BookSorter;
|
use BookStack\Sorting\BookSorter;
|
||||||
use BookStack\Uploads\ImageRepo;
|
use BookStack\Uploads\ImageRepo;
|
||||||
use BookStack\Util\HtmlDescriptionFilter;
|
use BookStack\Util\HtmlDescriptionFilter;
|
||||||
|
use BookStack\Util\HtmlToPlainText;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
|
|
||||||
class BaseRepo
|
class BaseRepo
|
||||||
@@ -151,9 +152,10 @@ class BaseRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($input['description_html'])) {
|
if (isset($input['description_html'])) {
|
||||||
|
$plainTextConverter = new HtmlToPlainText();
|
||||||
$entity->descriptionInfo()->set(
|
$entity->descriptionInfo()->set(
|
||||||
HtmlDescriptionFilter::filterFromString($input['description_html']),
|
HtmlDescriptionFilter::filterFromString($input['description_html']),
|
||||||
html_entity_decode(strip_tags($input['description_html']))
|
$plainTextConverter->convert($input['description_html']),
|
||||||
);
|
);
|
||||||
} else if (isset($input['description'])) {
|
} else if (isset($input['description'])) {
|
||||||
$entity->descriptionInfo()->set('', $input['description']);
|
$entity->descriptionInfo()->set('', $input['description']);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use BookStack\Users\Models\User;
|
|||||||
use BookStack\Util\HtmlContentFilter;
|
use BookStack\Util\HtmlContentFilter;
|
||||||
use BookStack\Util\HtmlContentFilterConfig;
|
use BookStack\Util\HtmlContentFilterConfig;
|
||||||
use BookStack\Util\HtmlDocument;
|
use BookStack\Util\HtmlDocument;
|
||||||
|
use BookStack\Util\HtmlToPlainText;
|
||||||
use BookStack\Util\WebSafeMimeSniffer;
|
use BookStack\Util\WebSafeMimeSniffer;
|
||||||
use Closure;
|
use Closure;
|
||||||
use DOMElement;
|
use DOMElement;
|
||||||
@@ -303,8 +304,8 @@ class PageContent
|
|||||||
public function toPlainText(): string
|
public function toPlainText(): string
|
||||||
{
|
{
|
||||||
$html = $this->render(true);
|
$html = $this->render(true);
|
||||||
|
$converter = new HtmlToPlainText();
|
||||||
return html_entity_decode(strip_tags($html));
|
return $converter->convert($html);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ class PermissionsUpdater
|
|||||||
*/
|
*/
|
||||||
public function updateFromPermissionsForm(Entity $entity, Request $request): void
|
public function updateFromPermissionsForm(Entity $entity, Request $request): void
|
||||||
{
|
{
|
||||||
$permissions = $request->get('permissions', null);
|
$permissions = $request->input('permissions', null);
|
||||||
$ownerId = $request->get('owned_by', null);
|
$ownerId = $request->input('owned_by', null);
|
||||||
|
|
||||||
$entity->permissions()->delete();
|
$entity->permissions()->delete();
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use BookStack\Entities\Tools\PageContent;
|
|||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
use BookStack\Util\CspService;
|
use BookStack\Util\CspService;
|
||||||
use BookStack\Util\HtmlDocument;
|
use BookStack\Util\HtmlDocument;
|
||||||
|
use BookStack\Util\HtmlToPlainText;
|
||||||
use DOMElement;
|
use DOMElement;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@@ -242,24 +243,13 @@ class ExportFormatter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the page contents into simple plain text.
|
* Converts the page contents into simple plain text.
|
||||||
* This method filters any bad looking content to provide a nice final output.
|
* We re-generate the plain text from HTML at this point, post-page-content rendering.
|
||||||
*/
|
*/
|
||||||
public function pageToPlainText(Page $page, bool $pageRendered = false, bool $fromParent = false): string
|
public function pageToPlainText(Page $page, bool $pageRendered = false, bool $fromParent = false): string
|
||||||
{
|
{
|
||||||
$html = $pageRendered ? $page->html : (new PageContent($page))->render();
|
$html = $pageRendered ? $page->html : (new PageContent($page))->render();
|
||||||
// Add proceeding spaces before tags so spaces remain between
|
$contentText = (new HtmlToPlainText())->convert($html);
|
||||||
// text within elements after stripping tags.
|
return $page->name . ($fromParent ? "\n" : "\n\n") . $contentText;
|
||||||
$html = str_replace('<', " <", $html);
|
|
||||||
$text = trim(strip_tags($html));
|
|
||||||
// Replace multiple spaces with single spaces
|
|
||||||
$text = preg_replace('/ {2,}/', ' ', $text);
|
|
||||||
// Reduce multiple horrid whitespace characters.
|
|
||||||
$text = preg_replace('/(\x0A|\xA0|\x0A|\r|\n){2,}/su', "\n\n", $text);
|
|
||||||
$text = html_entity_decode($text);
|
|
||||||
// Add title
|
|
||||||
$text = $page->name . ($fromParent ? "\n" : "\n\n") . $text;
|
|
||||||
|
|
||||||
return $text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -267,7 +257,7 @@ class ExportFormatter
|
|||||||
*/
|
*/
|
||||||
public function chapterToPlainText(Chapter $chapter): string
|
public function chapterToPlainText(Chapter $chapter): string
|
||||||
{
|
{
|
||||||
$text = $chapter->name . "\n" . $chapter->description;
|
$text = $chapter->name . "\n" . $chapter->descriptionInfo()->getPlain();
|
||||||
$text = trim($text) . "\n\n";
|
$text = trim($text) . "\n\n";
|
||||||
|
|
||||||
$parts = [];
|
$parts = [];
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ 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;
|
||||||
@@ -60,12 +62,65 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ final class ZipExportAttachment extends ZipExportModel
|
|||||||
$rules = [
|
$rules = [
|
||||||
'id' => ['nullable', 'int', $context->uniqueIdRule('attachment')],
|
'id' => ['nullable', 'int', $context->uniqueIdRule('attachment')],
|
||||||
'name' => ['required', 'string', 'min:1'],
|
'name' => ['required', 'string', 'min:1'],
|
||||||
'link' => ['required_without:file', 'nullable', 'string'],
|
'link' => ['required_without:file', 'nullable', 'string', 'max:2000', 'safe_url'],
|
||||||
'file' => ['required_without:link', 'nullable', 'string', $context->fileReferenceRule()],
|
'file' => ['required_without:link', 'nullable', 'string', $context->fileReferenceRule()],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ abstract class ApiController extends Controller
|
|||||||
* Provide a paginated listing JSON response in a standard format
|
* Provide a paginated listing JSON response in a standard format
|
||||||
* taking into account any pagination parameters passed by the user.
|
* taking into account any pagination parameters passed by the user.
|
||||||
*/
|
*/
|
||||||
protected function apiListingResponse(Builder $query, array $fields, array $modifiers = []): JsonResponse
|
protected function apiListingResponse(Builder $query, array $fields, array $modifiers = [], array $filterableFields = []): JsonResponse
|
||||||
{
|
{
|
||||||
$listing = new ListingResponseBuilder($query, request(), $fields);
|
$listing = new ListingResponseBuilder($query, request(), $fields);
|
||||||
|
|
||||||
|
if (count($filterableFields) > 0) {
|
||||||
|
$listing->setFilterableFields($filterableFields);
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($modifiers as $modifier) {
|
foreach ($modifiers as $modifier) {
|
||||||
$listing->modifyResults($modifier);
|
$listing->modifyResults($modifier);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ enum Permission: string
|
|||||||
case PageViewAll = 'page-view-all';
|
case PageViewAll = 'page-view-all';
|
||||||
case PageViewOwn = 'page-view-own';
|
case PageViewOwn = 'page-view-own';
|
||||||
|
|
||||||
|
case RevisionViewAll = 'revision-view-all';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the generic permissions which may be queried for entities.
|
* Get the generic permissions which may be queried for entities.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ class SearchApiController extends ApiController
|
|||||||
{
|
{
|
||||||
$this->validate($request, $this->rules['all']);
|
$this->validate($request, $this->rules['all']);
|
||||||
|
|
||||||
$options = SearchOptions::fromString($request->get('query') ?? '');
|
$options = SearchOptions::fromString($request->input('query') ?? '');
|
||||||
$page = intval($request->get('page', '0')) ?: 1;
|
$page = intval($request->input('page', '0')) ?: 1;
|
||||||
$count = min(intval($request->get('count', '0')) ?: 20, 100);
|
$count = min(intval($request->input('count', '0')) ?: 20, 100);
|
||||||
|
|
||||||
$results = $this->searchRunner->searchEntities($options, 'all', $page, $count);
|
$results = $this->searchRunner->searchEntities($options, 'all', $page, $count);
|
||||||
$this->resultsFormatter->format($results['results']->all(), $options);
|
$this->resultsFormatter->format($results['results']->all(), $options);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class SearchController extends Controller
|
|||||||
{
|
{
|
||||||
$searchOpts = SearchOptions::fromRequest($request);
|
$searchOpts = SearchOptions::fromRequest($request);
|
||||||
$fullSearchString = $searchOpts->toString();
|
$fullSearchString = $searchOpts->toString();
|
||||||
$page = intval($request->get('page', '0')) ?: 1;
|
$page = intval($request->input('page', '0')) ?: 1;
|
||||||
$count = setting()->getInteger('lists-page-count-search', 18, 1, 1000);
|
$count = setting()->getInteger('lists-page-count-search', 18, 1, 1000);
|
||||||
|
|
||||||
$results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, $count);
|
$results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, $count);
|
||||||
@@ -49,7 +49,7 @@ class SearchController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function searchBook(Request $request, int $bookId)
|
public function searchBook(Request $request, int $bookId)
|
||||||
{
|
{
|
||||||
$term = $request->get('term', '');
|
$term = $request->input('term', '');
|
||||||
$results = $this->searchRunner->searchBook($bookId, $term);
|
$results = $this->searchRunner->searchBook($bookId, $term);
|
||||||
|
|
||||||
return view('entities.list', ['entities' => $results]);
|
return view('entities.list', ['entities' => $results]);
|
||||||
@@ -60,7 +60,7 @@ class SearchController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function searchChapter(Request $request, int $chapterId)
|
public function searchChapter(Request $request, int $chapterId)
|
||||||
{
|
{
|
||||||
$term = $request->get('term', '');
|
$term = $request->input('term', '');
|
||||||
$results = $this->searchRunner->searchChapter($chapterId, $term);
|
$results = $this->searchRunner->searchChapter($chapterId, $term);
|
||||||
|
|
||||||
return view('entities.list', ['entities' => $results]);
|
return view('entities.list', ['entities' => $results]);
|
||||||
@@ -72,9 +72,9 @@ class SearchController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function searchForSelector(Request $request, QueryPopular $queryPopular)
|
public function searchForSelector(Request $request, QueryPopular $queryPopular)
|
||||||
{
|
{
|
||||||
$entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
|
$entityTypes = $request->filled('types') ? explode(',', $request->input('types')) : ['page', 'chapter', 'book'];
|
||||||
$searchTerm = $request->get('term', false);
|
$searchTerm = $request->input('term', false);
|
||||||
$permission = $request->get('permission', 'view');
|
$permission = $request->input('permission', 'view');
|
||||||
|
|
||||||
// Search for entities otherwise show most popular
|
// Search for entities otherwise show most popular
|
||||||
if ($searchTerm !== false) {
|
if ($searchTerm !== false) {
|
||||||
@@ -93,7 +93,7 @@ class SearchController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function templatesForSelector(Request $request)
|
public function templatesForSelector(Request $request)
|
||||||
{
|
{
|
||||||
$searchTerm = $request->get('term', false);
|
$searchTerm = $request->input('term', false);
|
||||||
|
|
||||||
if ($searchTerm !== false) {
|
if ($searchTerm !== false) {
|
||||||
$searchOptions = SearchOptions::fromString($searchTerm);
|
$searchOptions = SearchOptions::fromString($searchTerm);
|
||||||
@@ -119,7 +119,7 @@ class SearchController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function searchSuggestions(Request $request)
|
public function searchSuggestions(Request $request)
|
||||||
{
|
{
|
||||||
$searchTerm = $request->get('term', '');
|
$searchTerm = $request->input('term', '');
|
||||||
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 5)['results'];
|
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 5)['results'];
|
||||||
|
|
||||||
foreach ($entities as $entity) {
|
foreach ($entities as $entity) {
|
||||||
@@ -136,8 +136,8 @@ class SearchController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function searchSiblings(Request $request, SiblingFetcher $siblingFetcher)
|
public function searchSiblings(Request $request, SiblingFetcher $siblingFetcher)
|
||||||
{
|
{
|
||||||
$type = $request->get('entity_type', null);
|
$type = $request->input('entity_type', null);
|
||||||
$id = $request->get('entity_id', null);
|
$id = $request->input('entity_id', null);
|
||||||
|
|
||||||
$entities = $siblingFetcher->fetch($type, $id);
|
$entities = $siblingFetcher->fetch($type, $id);
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class SearchOptions
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($request->has('term')) {
|
if ($request->has('term')) {
|
||||||
return static::fromString($request->get('term'));
|
return static::fromString($request->input('term'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$instance = new SearchOptions();
|
$instance = new SearchOptions();
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class AppSettingsStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear icon image if requested
|
// Clear icon image if requested
|
||||||
if ($request->get('app_icon_reset')) {
|
if ($request->input('app_icon_reset')) {
|
||||||
$this->destroyExistingSettingImage('app-icon');
|
$this->destroyExistingSettingImage('app-icon');
|
||||||
setting()->remove('app-icon');
|
setting()->remove('app-icon');
|
||||||
foreach ($sizes as $size) {
|
foreach ($sizes as $size) {
|
||||||
@@ -67,7 +67,7 @@ class AppSettingsStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear logo image if requested
|
// Clear logo image if requested
|
||||||
if ($request->get('app_logo_reset')) {
|
if ($request->input('app_logo_reset')) {
|
||||||
$this->destroyExistingSettingImage('app-logo');
|
$this->destroyExistingSettingImage('app-logo');
|
||||||
setting()->remove('app-logo');
|
setting()->remove('app-logo');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class MaintenanceController extends Controller
|
|||||||
$this->checkPermission(Permission::SettingsManage);
|
$this->checkPermission(Permission::SettingsManage);
|
||||||
$this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'cleanup-images');
|
$this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'cleanup-images');
|
||||||
|
|
||||||
$checkRevisions = !($request->get('ignore_revisions', 'false') === 'true');
|
$checkRevisions = !($request->input('ignore_revisions', 'false') === 'true');
|
||||||
$dryRun = !($request->has('confirm'));
|
$dryRun = !($request->has('confirm'));
|
||||||
|
|
||||||
$imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
|
$imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class BookSortController extends Controller
|
|||||||
// Sort via map
|
// Sort via map
|
||||||
if ($request->filled('sort-tree')) {
|
if ($request->filled('sort-tree')) {
|
||||||
(new DatabaseTransaction(function () use ($book, $request, $sorter, &$loggedActivityForBook) {
|
(new DatabaseTransaction(function () use ($book, $request, $sorter, &$loggedActivityForBook) {
|
||||||
$sortMap = BookSortMap::fromJson($request->get('sort-tree'));
|
$sortMap = BookSortMap::fromJson($request->input('sort-tree'));
|
||||||
$booksInvolved = $sorter->sortUsingMap($sortMap);
|
$booksInvolved = $sorter->sortUsingMap($sortMap);
|
||||||
|
|
||||||
// Add activity for involved books.
|
// Add activity for involved books.
|
||||||
@@ -72,7 +72,7 @@ class BookSortController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('auto-sort')) {
|
if ($request->filled('auto-sort')) {
|
||||||
$sortSetId = intval($request->get('auto-sort')) ?: null;
|
$sortSetId = intval($request->input('auto-sort')) ?: null;
|
||||||
if ($sortSetId && SortRule::query()->find($sortSetId) === null) {
|
if ($sortSetId && SortRule::query()->find($sortSetId) === null) {
|
||||||
$sortSetId = null;
|
$sortSetId = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class AttachmentApiController extends ApiController
|
|||||||
$this->checkPermission(Permission::AttachmentCreateAll);
|
$this->checkPermission(Permission::AttachmentCreateAll);
|
||||||
$requestData = $this->validate($request, $this->rules()['create']);
|
$requestData = $this->validate($request, $this->rules()['create']);
|
||||||
|
|
||||||
$pageId = $request->get('uploaded_to');
|
$pageId = $request->input('uploaded_to');
|
||||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ class AttachmentApiController extends ApiController
|
|||||||
|
|
||||||
$page = $attachment->page;
|
$page = $attachment->page;
|
||||||
if ($requestData['uploaded_to'] ?? false) {
|
if ($requestData['uploaded_to'] ?? false) {
|
||||||
$pageId = $request->get('uploaded_to');
|
$pageId = $request->input('uploaded_to');
|
||||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||||
$attachment->uploaded_to = $requestData['uploaded_to'];
|
$attachment->uploaded_to = $requestData['uploaded_to'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class AttachmentController extends Controller
|
|||||||
'file' => array_merge(['required'], $this->attachmentService->getFileValidationRules()),
|
'file' => array_merge(['required'], $this->attachmentService->getFileValidationRules()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$pageId = $request->get('uploaded_to');
|
$pageId = $request->input('uploaded_to');
|
||||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||||
|
|
||||||
$this->checkPermission(Permission::AttachmentCreateAll);
|
$this->checkPermission(Permission::AttachmentCreateAll);
|
||||||
@@ -125,8 +125,8 @@ class AttachmentController extends Controller
|
|||||||
$this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment);
|
$this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment);
|
||||||
|
|
||||||
$attachment = $this->attachmentService->updateFile($attachment, [
|
$attachment = $this->attachmentService->updateFile($attachment, [
|
||||||
'name' => $request->get('attachment_edit_name'),
|
'name' => $request->input('attachment_edit_name'),
|
||||||
'link' => $request->get('attachment_edit_url'),
|
'link' => $request->input('attachment_edit_url'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return view('attachments.manager-edit-form', [
|
return view('attachments.manager-edit-form', [
|
||||||
@@ -141,7 +141,7 @@ class AttachmentController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function attachLink(Request $request)
|
public function attachLink(Request $request)
|
||||||
{
|
{
|
||||||
$pageId = $request->get('attachment_link_uploaded_to');
|
$pageId = $request->input('attachment_link_uploaded_to');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
@@ -161,8 +161,8 @@ class AttachmentController extends Controller
|
|||||||
$this->checkPermission(Permission::AttachmentCreateAll);
|
$this->checkPermission(Permission::AttachmentCreateAll);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||||
|
|
||||||
$attachmentName = $request->get('attachment_link_name');
|
$attachmentName = $request->input('attachment_link_name');
|
||||||
$link = $request->get('attachment_link_url');
|
$link = $request->input('attachment_link_url');
|
||||||
$this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
|
$this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
|
||||||
|
|
||||||
return view('attachments.manager-link-form', [
|
return view('attachments.manager-link-form', [
|
||||||
@@ -198,7 +198,7 @@ class AttachmentController extends Controller
|
|||||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||||
|
|
||||||
$attachmentOrder = $request->get('order');
|
$attachmentOrder = $request->input('order');
|
||||||
$this->attachmentService->updateFileOrderWithinPage($attachmentOrder, $pageId);
|
$this->attachmentService->updateFileOrderWithinPage($attachmentOrder, $pageId);
|
||||||
|
|
||||||
return response()->json(['message' => trans('entities.attachments_order_updated')]);
|
return response()->json(['message' => trans('entities.attachments_order_updated')]);
|
||||||
@@ -231,7 +231,7 @@ class AttachmentController extends Controller
|
|||||||
$attachmentStream = $this->attachmentService->streamAttachmentFromStorage($attachment);
|
$attachmentStream = $this->attachmentService->streamAttachmentFromStorage($attachment);
|
||||||
$attachmentSize = $this->attachmentService->getAttachmentFileSize($attachment);
|
$attachmentSize = $this->attachmentService->getAttachmentFileSize($attachment);
|
||||||
|
|
||||||
if ($request->get('open') === 'true') {
|
if ($request->input('open') === 'true') {
|
||||||
return $this->download()->streamedInline($attachmentStream, $fileName, $attachmentSize);
|
return $this->download()->streamedInline($attachmentStream, $fileName, $attachmentSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ class DrawioImageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function list(Request $request, ImageResizer $resizer)
|
public function list(Request $request, ImageResizer $resizer)
|
||||||
{
|
{
|
||||||
$page = $request->get('page', 1);
|
$page = $request->input('page', 1);
|
||||||
$searchTerm = $request->get('search', null);
|
$searchTerm = $request->input('search', null);
|
||||||
$uploadedToFilter = $request->get('uploaded_to', null);
|
$uploadedToFilter = $request->input('uploaded_to', null);
|
||||||
$parentTypeFilter = $request->get('filter_type', null);
|
$parentTypeFilter = $request->input('filter_type', null);
|
||||||
|
|
||||||
$imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
|
$imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
|
||||||
$viewData = [
|
$viewData = [
|
||||||
@@ -59,10 +59,10 @@ class DrawioImageController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->checkPermission(Permission::ImageCreateAll);
|
$this->checkPermission(Permission::ImageCreateAll);
|
||||||
$imageBase64Data = $request->get('image');
|
$imageBase64Data = $request->input('image');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$uploadedTo = $request->get('uploaded_to', 0);
|
$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);
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ class GalleryImageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function list(Request $request, ImageResizer $resizer)
|
public function list(Request $request, ImageResizer $resizer)
|
||||||
{
|
{
|
||||||
$page = $request->get('page', 1);
|
$page = $request->input('page', 1);
|
||||||
$searchTerm = $request->get('search', null);
|
$searchTerm = $request->input('search', null);
|
||||||
$uploadedToFilter = $request->get('uploaded_to', null);
|
$uploadedToFilter = $request->input('uploaded_to', null);
|
||||||
$parentTypeFilter = $request->get('filter_type', null);
|
$parentTypeFilter = $request->input('filter_type', null);
|
||||||
|
|
||||||
$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 30, $uploadedToFilter, $searchTerm);
|
$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 30, $uploadedToFilter, $searchTerm);
|
||||||
$viewData = [
|
$viewData = [
|
||||||
@@ -69,7 +69,7 @@ class GalleryImageController extends Controller
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$imageUpload = $request->file('file');
|
$imageUpload = $request->file('file');
|
||||||
$uploadedTo = $request->get('uploaded_to', 0);
|
$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);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class RoleController extends Controller
|
|||||||
/** @var ?Role $role */
|
/** @var ?Role $role */
|
||||||
$role = null;
|
$role = null;
|
||||||
if ($request->has('copy_from')) {
|
if ($request->has('copy_from')) {
|
||||||
$role = Role::query()->find($request->get('copy_from'));
|
$role = Role::query()->find($request->input('copy_from'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($role) {
|
if ($role) {
|
||||||
@@ -150,7 +150,7 @@ class RoleController extends Controller
|
|||||||
$this->checkPermission(Permission::UserRolesManage);
|
$this->checkPermission(Permission::UserRolesManage);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$migrateRoleId = intval($request->get('migrate_role_id') ?: "0");
|
$migrateRoleId = intval($request->input('migrate_role_id') ?: "0");
|
||||||
$this->permissionsRepo->deleteRole($id, $migrateRoleId);
|
$this->permissionsRepo->deleteRole($id, $migrateRoleId);
|
||||||
} catch (PermissionsException $e) {
|
} catch (PermissionsException $e) {
|
||||||
$this->showErrorNotification($e->getMessage());
|
$this->showErrorNotification($e->getMessage());
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ class UserAccountController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function updateShortcuts(Request $request)
|
public function updateShortcuts(Request $request)
|
||||||
{
|
{
|
||||||
$enabled = $request->get('enabled') === 'true';
|
$enabled = $request->input('enabled') === 'true';
|
||||||
$providedShortcuts = $request->get('shortcut', []);
|
$providedShortcuts = $request->input('shortcut', []);
|
||||||
$shortcuts = new UserShortcutMap($providedShortcuts);
|
$shortcuts = new UserShortcutMap($providedShortcuts);
|
||||||
|
|
||||||
setting()->putForCurrentUser('ui-shortcuts', $shortcuts->toJson());
|
setting()->putForCurrentUser('ui-shortcuts', $shortcuts->toJson());
|
||||||
@@ -218,7 +218,7 @@ class UserAccountController extends Controller
|
|||||||
{
|
{
|
||||||
$this->preventAccessInDemoMode();
|
$this->preventAccessInDemoMode();
|
||||||
|
|
||||||
$requestNewOwnerId = intval($request->get('new_owner_id')) ?: null;
|
$requestNewOwnerId = intval($request->input('new_owner_id')) ?: null;
|
||||||
$newOwnerId = userCan(Permission::UsersManage) ? $requestNewOwnerId : null;
|
$newOwnerId = userCan(Permission::UsersManage) ? $requestNewOwnerId : null;
|
||||||
|
|
||||||
$this->userRepo->destroy(user(), $newOwnerId);
|
$this->userRepo->destroy(user(), $newOwnerId);
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class UserApiController extends ApiController
|
|||||||
public function delete(Request $request, string $id)
|
public function delete(Request $request, string $id)
|
||||||
{
|
{
|
||||||
$user = $this->userRepo->getById($id);
|
$user = $this->userRepo->getById($id);
|
||||||
$newOwnerId = $request->get('migrate_ownership_id', null);
|
$newOwnerId = $request->input('migrate_ownership_id', null);
|
||||||
|
|
||||||
$this->userRepo->destroy($user, $newOwnerId);
|
$this->userRepo->destroy($user, $newOwnerId);
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class UserController extends Controller
|
|||||||
$this->checkPermission(Permission::UsersManage);
|
$this->checkPermission(Permission::UsersManage);
|
||||||
|
|
||||||
$authMethod = config('auth.method');
|
$authMethod = config('auth.method');
|
||||||
$sendInvite = ($request->get('send_invite', 'false') === 'true');
|
$sendInvite = ($request->input('send_invite', 'false') === 'true');
|
||||||
$externalAuth = $authMethod === 'ldap' || $authMethod === 'saml2' || $authMethod === 'oidc';
|
$externalAuth = $authMethod === 'ldap' || $authMethod === 'saml2' || $authMethod === 'oidc';
|
||||||
$passwordRequired = ($authMethod === 'standard' && !$sendInvite);
|
$passwordRequired = ($authMethod === 'standard' && !$sendInvite);
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ class UserController extends Controller
|
|||||||
$this->checkPermission(Permission::UsersManage);
|
$this->checkPermission(Permission::UsersManage);
|
||||||
|
|
||||||
$user = $this->userRepo->getById($id);
|
$user = $this->userRepo->getById($id);
|
||||||
$newOwnerId = intval($request->get('new_owner_id')) ?: null;
|
$newOwnerId = intval($request->input('new_owner_id')) ?: null;
|
||||||
|
|
||||||
$this->userRepo->destroy($user, $newOwnerId);
|
$this->userRepo->destroy($user, $newOwnerId);
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class UserPreferencesController extends Controller
|
|||||||
return $this->redirectToRequest($request);
|
return $this->redirectToRequest($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
$view = $request->get('view');
|
$view = $request->input('view');
|
||||||
if (!in_array($view, ['grid', 'list'])) {
|
if (!in_array($view, ['grid', 'list'])) {
|
||||||
$view = 'list';
|
$view = 'list';
|
||||||
}
|
}
|
||||||
@@ -44,8 +44,8 @@ class UserPreferencesController extends Controller
|
|||||||
return $this->redirectToRequest($request);
|
return $this->redirectToRequest($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
$sort = substr($request->get('sort') ?: 'name', 0, 50);
|
$sort = substr($request->input('sort') ?: 'name', 0, 50);
|
||||||
$order = $request->get('order') === 'desc' ? 'desc' : 'asc';
|
$order = $request->input('order') === 'desc' ? 'desc' : 'asc';
|
||||||
|
|
||||||
$sortKey = $type . '_sort';
|
$sortKey = $type . '_sort';
|
||||||
$orderKey = $type . '_sort_order';
|
$orderKey = $type . '_sort_order';
|
||||||
@@ -76,7 +76,7 @@ class UserPreferencesController extends Controller
|
|||||||
return response('Invalid key', 500);
|
return response('Invalid key', 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
$newState = $request->get('expand', 'false');
|
$newState = $request->input('expand', 'false');
|
||||||
setting()->putForCurrentUser('section_expansion#' . $type, $newState);
|
setting()->putForCurrentUser('section_expansion#' . $type, $newState);
|
||||||
|
|
||||||
return response('', 204);
|
return response('', 204);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class UserSearchController extends Controller
|
|||||||
$this->showPermissionError();
|
$this->showPermissionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
$search = $request->get('search', '');
|
$search = $request->input('search', '');
|
||||||
$query = User::query()
|
$query = User::query()
|
||||||
->orderBy('name', 'asc')
|
->orderBy('name', 'asc')
|
||||||
->take(20);
|
->take(20);
|
||||||
@@ -58,7 +58,7 @@ class UserSearchController extends Controller
|
|||||||
$this->showPermissionError();
|
$this->showPermissionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
$search = $request->get('search', '');
|
$search = $request->input('search', '');
|
||||||
$query = User::query()
|
$query = User::query()
|
||||||
->orderBy('name', 'asc')
|
->orderBy('name', 'asc')
|
||||||
->take(20);
|
->take(20);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class HtmlDescriptionFilter
|
|||||||
'span' => [],
|
'span' => [],
|
||||||
'em' => [],
|
'em' => [],
|
||||||
'br' => [],
|
'br' => [],
|
||||||
|
'code' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
public static function filterFromString(string $html): string
|
public static function filterFromString(string $html): string
|
||||||
|
|||||||
47
app/Util/HtmlToPlainText.php
Normal file
47
app/Util/HtmlToPlainText.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Util;
|
||||||
|
|
||||||
|
class HtmlToPlainText
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Inline tags types where the content should not be put on a new line.
|
||||||
|
*/
|
||||||
|
protected array $inlineTags = [
|
||||||
|
'a', 'b', 'i', 'u', 'strong', 'em', 'small', 'sup', 'sub', 'span', 'div',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the provided HTML to relatively clean plain text.
|
||||||
|
*/
|
||||||
|
public function convert(string $html): string
|
||||||
|
{
|
||||||
|
$doc = new HtmlDocument($html);
|
||||||
|
$text = $this->nodeToText($doc->getBody());
|
||||||
|
|
||||||
|
// Remove repeated newlines
|
||||||
|
$text = preg_replace('/\n+/', "\n", $text);
|
||||||
|
// Remove leading/trailing whitespace
|
||||||
|
$text = trim($text);
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function nodeToText(\DOMNode $node): string
|
||||||
|
{
|
||||||
|
if ($node->nodeType === XML_TEXT_NODE) {
|
||||||
|
return $node->textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
$text = '';
|
||||||
|
if (!in_array($node->nodeName, $this->inlineTags)) {
|
||||||
|
$text .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($node->childNodes as $childNode) {
|
||||||
|
$text .= $this->nodeToText($childNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ class SimpleListOptions
|
|||||||
*/
|
*/
|
||||||
public static function fromRequest(Request $request, string $typeKey, bool $sortDescDefault = false): self
|
public static function fromRequest(Request $request, string $typeKey, bool $sortDescDefault = false): self
|
||||||
{
|
{
|
||||||
$search = $request->get('search', '');
|
$search = $request->input('search', '');
|
||||||
$sort = setting()->getForCurrentUser($typeKey . '_sort', '');
|
$sort = setting()->getForCurrentUser($typeKey . '_sort', '');
|
||||||
$order = setting()->getForCurrentUser($typeKey . '_sort_order', $sortDescDefault ? 'desc' : 'asc');
|
$order = setting()->getForCurrentUser($typeKey . '_sort_order', $sortDescDefault ? 'desc' : 'asc');
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
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:
|
||||||
- ":earth_africa: Translations"
|
- "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%
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Create new revision-view-all permission
|
||||||
|
$permissionId = DB::table('role_permissions')->insertGetId([
|
||||||
|
'name' => 'revision-view-all',
|
||||||
|
'created_at' => Carbon::now()->toDateTimeString(),
|
||||||
|
'updated_at' => Carbon::now()->toDateTimeString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get ids of page view permissions
|
||||||
|
$pageViewPermissions = DB::table('role_permissions')
|
||||||
|
->whereIn('name', [
|
||||||
|
'page-view-own',
|
||||||
|
'page-view-all',
|
||||||
|
])->get();
|
||||||
|
|
||||||
|
if ($pageViewPermissions->count() === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get role ids which have page view permission
|
||||||
|
$applicableRoleIds = DB::table('permission_role')
|
||||||
|
->whereIn('permission_id', $pageViewPermissions->pluck('id'))
|
||||||
|
->pluck('role_id')
|
||||||
|
->unique()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
// Assign the new permission to relevant roles
|
||||||
|
$newPermissionRoles = array_values(array_map(function (int $roleId) use ($permissionId) {
|
||||||
|
return [
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'permission_id' => $permissionId,
|
||||||
|
];
|
||||||
|
}, $applicableRoleIds));
|
||||||
|
|
||||||
|
DB::table('permission_role')->insert($newPermissionRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Get the permission to remove
|
||||||
|
$revisionViewPermission = DB::table('role_permissions')
|
||||||
|
->where('name', '=', 'revision-view-all')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$revisionViewPermission) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the permission, and its use on roles, from the database
|
||||||
|
DB::table('permission_role')->where('permission_id', '=', $revisionViewPermission->id)->delete();
|
||||||
|
DB::table('role_permissions')->where('id', '=', $revisionViewPermission->id)->delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
1
dev/api/requests/tags-list-values.http
Normal file
1
dev/api/requests/tags-list-values.http
Normal file
@@ -0,0 +1 @@
|
|||||||
|
GET /api/tags/values-for-name?name=Category
|
||||||
32
dev/api/responses/tags-list-names.json
Normal file
32
dev/api/responses/tags-list-names.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"name": "Category",
|
||||||
|
"values": 8,
|
||||||
|
"usages": 184,
|
||||||
|
"page_count": 3,
|
||||||
|
"chapter_count": 8,
|
||||||
|
"book_count": 171,
|
||||||
|
"shelf_count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Review Due",
|
||||||
|
"values": 2,
|
||||||
|
"usages": 2,
|
||||||
|
"page_count": 1,
|
||||||
|
"chapter_count": 0,
|
||||||
|
"book_count": 1,
|
||||||
|
"shelf_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Type",
|
||||||
|
"values": 2,
|
||||||
|
"usages": 2,
|
||||||
|
"page_count": 0,
|
||||||
|
"chapter_count": 1,
|
||||||
|
"book_count": 1,
|
||||||
|
"shelf_count": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 3
|
||||||
|
}
|
||||||
32
dev/api/responses/tags-list-values.json
Normal file
32
dev/api/responses/tags-list-values.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"name": "Category",
|
||||||
|
"value": "Cool Stuff",
|
||||||
|
"usages": 3,
|
||||||
|
"page_count": 1,
|
||||||
|
"chapter_count": 0,
|
||||||
|
"book_count": 2,
|
||||||
|
"shelf_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Category",
|
||||||
|
"value": "Top Content",
|
||||||
|
"usages": 168,
|
||||||
|
"page_count": 0,
|
||||||
|
"chapter_count": 3,
|
||||||
|
"book_count": 165,
|
||||||
|
"shelf_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Category",
|
||||||
|
"value": "Learning",
|
||||||
|
"usages": 2,
|
||||||
|
"page_count": 0,
|
||||||
|
"chapter_count": 0,
|
||||||
|
"book_count": 0,
|
||||||
|
"shelf_count": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 3
|
||||||
|
}
|
||||||
@@ -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://github.com/bookstackapp/bookstack.git --branch "$BRANCH" --single-branch /var/www/bookstack && \
|
git clone https://codeberg.org/bookstack/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
|
||||||
|
|||||||
@@ -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 GitHub here](https://github.com/BookStackApp/BookStack/blob/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 Codeberg here](https://codeberg.org/bookstack/bookstack/src/branch/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.
|
||||||
|
|
||||||
|
|||||||
@@ -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://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.
|
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.
|
||||||
|
|
||||||
### 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 [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).
|
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).
|
||||||
|
|
||||||
### 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://github.com/BookStackApp/devops/blob/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://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.
|
||||||
@@ -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://github.com/BookStackApp/BookStack/issues/5937
|
Feedback is very much welcomed via this issue: https://codeberg.org/bookstack/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
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ return [
|
|||||||
'role_all' => 'All',
|
'role_all' => 'All',
|
||||||
'role_own' => 'Own',
|
'role_own' => 'Own',
|
||||||
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
|
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
|
||||||
|
'role_controlled_by_page_delete' => 'Controlled by page delete permissions',
|
||||||
'role_save' => 'Save Role',
|
'role_save' => 'Save Role',
|
||||||
'role_users' => 'Users in this role',
|
'role_users' => 'Users in this role',
|
||||||
'role_users_none' => 'No users are currently assigned to this role',
|
'role_users_none' => 'No users are currently assigned to this role',
|
||||||
|
|||||||
35
readme.md
35
readme.md
@@ -1,15 +1,14 @@
|
|||||||
# BookStack
|
# BookStack
|
||||||
|
|
||||||
[](https://github.com/BookStackApp/BookStack/releases/latest)
|
[](https://codeberg.org/bookstack/bookstack/releases/latest)
|
||||||
[](https://github.com/BookStackApp/BookStack/blob/development/LICENSE)
|
[](https://codeberg.org/bookstack/bookstack/src/branch/development/LICENSE)
|
||||||
[](https://crowdin.com/project/bookstack)
|
[](https://crowdin.com/project/bookstack)
|
||||||
[](https://github.com/BookStackApp/BookStack/actions)
|
[](https://codeberg.org/bookstack/bookstack/actions?workflow=test-php.yml)
|
||||||
[](https://github.com/BookStackApp/BookStack/actions)
|
[](https://codeberg.org/bookstack/bookstack/actions?workflow=lint-php.yml)
|
||||||
[](https://source.bookstackapp.com/php-stats/index.html)
|
[](https://source.bookstackapp.com/php-stats/index.html)
|
||||||
<br>
|
<br>
|
||||||
[](https://source.bookstackapp.com/)
|
[](https://source.bookstackapp.com/)
|
||||||
[](https://gh-stats.bookstackapp.com/)
|
[](https://community.bookstackapp.com/)
|
||||||
[](https://www.bookstackapp.com/links/discord)
|
|
||||||
[](https://www.bookstackapp.com/links/mastodon)
|
[](https://www.bookstackapp.com/links/mastodon)
|
||||||
<br>
|
<br>
|
||||||
[](https://foss.video/c/bookstack)
|
[](https://foss.video/c/bookstack)
|
||||||
@@ -20,11 +19,10 @@ A platform for storing and organising information and documentation. Details for
|
|||||||
* [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
|
* [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
|
||||||
* [Documentation](https://www.bookstackapp.com/docs)
|
* [Documentation](https://www.bookstackapp.com/docs)
|
||||||
* [Demo Instance](https://demo.bookstackapp.com)
|
* [Demo Instance](https://demo.bookstackapp.com)
|
||||||
* [Admin Login](https://demo.bookstackapp.com/login?email=admin@example.com&password=password)
|
|
||||||
* [Screenshots](https://www.bookstackapp.com/#screenshots)
|
* [Screenshots](https://www.bookstackapp.com/#screenshots)
|
||||||
* [BookStack Blog](https://www.bookstackapp.com/blog)
|
* [BookStack Blog](https://www.bookstackapp.com/blog)
|
||||||
* [Issue List](https://github.com/BookStackApp/BookStack/issues)
|
* [Issue List](https://codeberg.org/bookstack/bookstack/issues)
|
||||||
* [Discord Chat](https://www.bookstackapp.com/links/discord)
|
* [Community Discussions](https://community.bookstackapp.com/)
|
||||||
* [Support Options](https://www.bookstackapp.com/support/)
|
* [Support Options](https://www.bookstackapp.com/support/)
|
||||||
|
|
||||||
## 📚 Project Definition
|
## 📚 Project Definition
|
||||||
@@ -72,7 +70,7 @@ Big thanks to these companies for supporting the project.
|
|||||||
<td align="center"><a href="https://www.stellarhosted.com/bookstack/" target="_blank">
|
<td align="center"><a href="https://www.stellarhosted.com/bookstack/" target="_blank">
|
||||||
<img width="240" src="https://www.bookstackapp.com/images/sponsors/stellarhosted.png" alt="Stellar Hosted">
|
<img width="240" src="https://www.bookstackapp.com/images/sponsors/stellarhosted.png" alt="Stellar Hosted">
|
||||||
</a></td>
|
</a></td>
|
||||||
<td align="center" style="text-align: center"><a href="https://nws.netways.de/apps/bookstack/" target="_blank">
|
<td align="center" style="text-align: center"><a href="https://nws.netways.de" target="_blank">
|
||||||
<img width="240" src="https://www.bookstackapp.com/images/sponsors/netways.png" alt="NETWAYS Web Services">
|
<img width="240" src="https://www.bookstackapp.com/images/sponsors/netways.png" alt="NETWAYS Web Services">
|
||||||
</a></td>
|
</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -113,19 +111,20 @@ Translations for text within BookStack are managed through the [BookStack projec
|
|||||||
|
|
||||||
Please use [Crowdin](https://crowdin.com/project/bookstack) to contribute translations instead of opening a pull request. The translations within the working codebase can be out-of-date, and merging via code can cause conflicts & sync issues. If for some reason you can't use Crowdin feel free to open an issue to discuss alternative options.
|
Please use [Crowdin](https://crowdin.com/project/bookstack) to contribute translations instead of opening a pull request. The translations within the working codebase can be out-of-date, and merging via code can cause conflicts & sync issues. If for some reason you can't use Crowdin feel free to open an issue to discuss alternative options.
|
||||||
|
|
||||||
If you'd like a new language to be added to Crowdin, for you to be able to provide translations for, please [open a new issue here](https://github.com/BookStackApp/BookStack/issues/new?template=language_request.yml).
|
If you'd like a new language to be added to Crowdin, for you to be able to provide translations for, please [open a new issue here](https://codeberg.org/bookstack/bookstack/issues/new?template=.forgejo%2fISSUE_TEMPLATE%2flanguage_request.yml).
|
||||||
|
|
||||||
Please note, translations in BookStack are provided to the "Crowdin Global Translation Memory" which helps BookStack and other projects with finding translations. If you are not happy with contributing to this then providing translations to BookStack, even manually via GitHub, is not advised.
|
Please note, translations in BookStack are provided to the "Crowdin Global Translation Memory" which helps BookStack and other projects with finding translations. If you are not happy with contributing to this then providing translations to BookStack, even manually via code, is not advised.
|
||||||
|
|
||||||
## 🎁 Contributing, Issues & Pull Requests
|
## 🎁 Contributing, Issues & Pull Requests
|
||||||
|
|
||||||
Feel free to [create issues](https://github.com/BookStackApp/BookStack/issues/new/choose) to request new features or to report bugs & problems. Just please follow the template given when creating the issue.
|
Feel free to [create issues](https://codeberg.org/bookstack/bookstack/issues/new/choose) to request new features or to report bugs & problems. Just please follow the template given when creating the issue.
|
||||||
|
|
||||||
Pull requests are welcome but, unless it's a small tweak, it may be best to open the pull request early or create an issue for your intended change to discuss how it will fit into the project and plan out the merge. Just because a feature request exists, or is tagged, does not mean that feature would be accepted into the core project.
|
Pull requests are welcome but, unless it's a small tweak, it may be best to open the pull request early or create an issue for your intended change to discuss how it will fit into the project and plan out the merge. Just because a feature request exists, or is tagged, does not mean that feature would be accepted into the core project.
|
||||||
|
|
||||||
Pull requests should be created from the `development` branch since they will be merged back into `development` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
|
Pull requests should be created from the `development` branch since they will be merged back into `development` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
|
||||||
|
See the [Development & Testing](#-development--testing) section above for further development guidance.
|
||||||
|
|
||||||
The project's code of conduct [can be found here](https://github.com/BookStackApp/BookStack/blob/development/.github/CODE_OF_CONDUCT.md).
|
The project's community rules, including those for raising issues and making code contributions, [can be found here](https://www.bookstackapp.com/about/community-rules/).
|
||||||
|
|
||||||
## 🔒 Security
|
## 🔒 Security
|
||||||
|
|
||||||
@@ -133,7 +132,7 @@ Security information for administering a BookStack instance can be found on the
|
|||||||
|
|
||||||
If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](https://updates.bookstackapp.com/signup/bookstack-security-updates).
|
If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](https://updates.bookstackapp.com/signup/bookstack-security-updates).
|
||||||
|
|
||||||
If you would like to report a security concern, details of doing so [can be found here](https://github.com/BookStackApp/BookStack/blob/development/.github/SECURITY.md).
|
If you would like to report a security concern, details of doing so [can be found here](.forgejo/SECURITY.md).
|
||||||
|
|
||||||
## ♿ Accessibility
|
## ♿ Accessibility
|
||||||
|
|
||||||
@@ -141,18 +140,18 @@ We want BookStack to remain accessible to as many people as possible. We aim for
|
|||||||
|
|
||||||
## 🖥️ Website, Docs & Blog
|
## 🖥️ Website, Docs & Blog
|
||||||
|
|
||||||
The website which contains the project docs & blog can be found in the [BookStackApp/website](https://codeberg.org/bookstack/website) repo.
|
The website which contains the project docs & blog can be found in the [bookstack/website](https://codeberg.org/bookstack/website) repo.
|
||||||
|
|
||||||
## ⚖️ License
|
## ⚖️ License
|
||||||
|
|
||||||
The BookStack source is provided under the [MIT License](https://github.com/BookStackApp/BookStack/blob/development/LICENSE).
|
The BookStack source is provided under the [MIT License](https://codeberg.org/bookstack/bookstack/src/branch/development/LICENSE).
|
||||||
|
|
||||||
The libraries used by, and included with, BookStack are provided under their own licenses and copyright.
|
The libraries used by, and included with, BookStack are provided under their own licenses and copyright.
|
||||||
The licenses for many of our core dependencies can be found in the attribution list below, but this is not an exhaustive list of all projects used within BookStack.
|
The licenses for many of our core dependencies can be found in the attribution list below, but this is not an exhaustive list of all projects used within BookStack.
|
||||||
|
|
||||||
## 👪 Attribution
|
## 👪 Attribution
|
||||||
|
|
||||||
The great people that have worked to build and improve BookStack can [be seen here](https://github.com/BookStackApp/BookStack/graphs/contributors). The wonderful people that have provided translations, either through GitHub or via Crowdin [can be seen here](https://github.com/BookStackApp/BookStack/blob/development/.github/translators.txt).
|
The great people that have worked to build and improve BookStack can [be seen here](https://codeberg.org/bookstack/bookstack/activity/contributors). The wonderful people that have provided translations, either through GitHub, Codeberg or via Crowdin [can be seen here](https://codeberg.org/bookstack/bookstack/src/branch/development/.github/translators.txt).
|
||||||
|
|
||||||
Below are the great open-source projects used to help build BookStack.
|
Below are the great open-source projects used to help build BookStack.
|
||||||
Note: This is not an exhaustive list of all libraries and projects that would be used in an active BookStack instance.
|
Note: This is not an exhaustive list of all libraries and projects that would be used in an active BookStack instance.
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
|
|||||||
mergeRegister(
|
mergeRegister(
|
||||||
registerRichText(editor),
|
registerRichText(editor),
|
||||||
registerHistory(editor, createEmptyHistoryState(), 300),
|
registerHistory(editor, createEmptyHistoryState(), 300),
|
||||||
registerShortcuts(context),
|
registerShortcuts(context, true),
|
||||||
registerKeyboardHandling(context),
|
registerKeyboardHandling(context),
|
||||||
registerMouseHandling(context),
|
registerMouseHandling(context),
|
||||||
registerSelectionHandling(context),
|
registerSelectionHandling(context),
|
||||||
@@ -123,7 +123,7 @@ export function createBasicEditorInstance(container: HTMLElement, htmlContent: s
|
|||||||
const editorTeardown = mergeRegister(
|
const editorTeardown = mergeRegister(
|
||||||
registerRichText(editor),
|
registerRichText(editor),
|
||||||
registerHistory(editor, createEmptyHistoryState(), 300),
|
registerHistory(editor, createEmptyHistoryState(), 300),
|
||||||
registerShortcuts(context),
|
registerShortcuts(context, false),
|
||||||
registerAutoLinks(editor),
|
registerAutoLinks(editor),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ export function createCommentEditorInstance(container: HTMLElement, htmlContent:
|
|||||||
const editorTeardown = mergeRegister(
|
const editorTeardown = mergeRegister(
|
||||||
registerRichText(editor),
|
registerRichText(editor),
|
||||||
registerHistory(editor, createEmptyHistoryState(), 300),
|
registerHistory(editor, createEmptyHistoryState(), 300),
|
||||||
registerShortcuts(context),
|
registerShortcuts(context, false),
|
||||||
registerAutoLinks(editor),
|
registerAutoLinks(editor),
|
||||||
registerMentions(context),
|
registerMentions(context),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,29 +38,9 @@ type ShortcutAction = (editor: LexicalEditor, context: EditorUiContext) => boole
|
|||||||
* List of action functions by their shortcut combo.
|
* List of action functions by their shortcut combo.
|
||||||
* We use "meta" as an abstraction for ctrl/cmd depending on platform.
|
* We use "meta" as an abstraction for ctrl/cmd depending on platform.
|
||||||
*/
|
*/
|
||||||
const actionsByKeys: Record<string, ShortcutAction> = {
|
const baseActionsByKeys: Record<string, ShortcutAction> = {
|
||||||
'meta+s': () => {
|
|
||||||
window.$events.emit('editor-save-draft');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
'meta+enter': () => {
|
|
||||||
window.$events.emit('editor-save-page');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
'meta+1': (editor, context) => headerHandler(context, 'h2'),
|
|
||||||
'meta+2': (editor, context) => headerHandler(context, 'h3'),
|
|
||||||
'meta+3': (editor, context) => headerHandler(context, 'h4'),
|
|
||||||
'meta+4': (editor, context) => headerHandler(context, 'h5'),
|
|
||||||
'meta+5': wrapFormatAction(toggleSelectionAsParagraph),
|
|
||||||
'meta+d': wrapFormatAction(toggleSelectionAsParagraph),
|
|
||||||
'meta+6': wrapFormatAction(toggleSelectionAsBlockquote),
|
|
||||||
'meta+q': wrapFormatAction(toggleSelectionAsBlockquote),
|
|
||||||
'meta+7': wrapFormatAction(formatCodeBlock),
|
|
||||||
'meta+e': wrapFormatAction(formatCodeBlock),
|
|
||||||
'meta+8': toggleInlineCode,
|
'meta+8': toggleInlineCode,
|
||||||
'meta+shift+e': toggleInlineCode,
|
'meta+shift+e': toggleInlineCode,
|
||||||
'meta+9': wrapFormatAction(cycleSelectionCalloutFormats),
|
|
||||||
|
|
||||||
'meta+o': wrapFormatAction((e) => toggleSelectionAsList(e, 'number')),
|
'meta+o': wrapFormatAction((e) => toggleSelectionAsList(e, 'number')),
|
||||||
'meta+p': wrapFormatAction((e) => toggleSelectionAsList(e, 'bullet')),
|
'meta+p': wrapFormatAction((e) => toggleSelectionAsList(e, 'bullet')),
|
||||||
'meta+k': (editor, context) => {
|
'meta+k': (editor, context) => {
|
||||||
@@ -87,12 +67,39 @@ const actionsByKeys: Record<string, ShortcutAction> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function createKeyDownListener(context: EditorUiContext): (e: KeyboardEvent) => void {
|
/**
|
||||||
|
* An extended set of the above, used for fuller-featured editors with heavier block-level formatting.
|
||||||
|
*/
|
||||||
|
const extendedActionsByKeys: Record<string, ShortcutAction> = {
|
||||||
|
...baseActionsByKeys,
|
||||||
|
'meta+s': () => {
|
||||||
|
window.$events.emit('editor-save-draft');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'meta+enter': () => {
|
||||||
|
window.$events.emit('editor-save-page');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'meta+1': (editor, context) => headerHandler(context, 'h2'),
|
||||||
|
'meta+2': (editor, context) => headerHandler(context, 'h3'),
|
||||||
|
'meta+3': (editor, context) => headerHandler(context, 'h4'),
|
||||||
|
'meta+4': (editor, context) => headerHandler(context, 'h5'),
|
||||||
|
'meta+5': wrapFormatAction(toggleSelectionAsParagraph),
|
||||||
|
'meta+d': wrapFormatAction(toggleSelectionAsParagraph),
|
||||||
|
'meta+6': wrapFormatAction(toggleSelectionAsBlockquote),
|
||||||
|
'meta+7': wrapFormatAction(formatCodeBlock),
|
||||||
|
'meta+e': wrapFormatAction(formatCodeBlock),
|
||||||
|
'meta+q': wrapFormatAction(toggleSelectionAsBlockquote),
|
||||||
|
'meta+9': wrapFormatAction(cycleSelectionCalloutFormats),
|
||||||
|
};
|
||||||
|
|
||||||
|
function createKeyDownListener(context: EditorUiContext, useExtended: boolean): (e: KeyboardEvent) => void {
|
||||||
|
const keySetToUse = useExtended ? extendedActionsByKeys : baseActionsByKeys;
|
||||||
return (event: KeyboardEvent) => {
|
return (event: KeyboardEvent) => {
|
||||||
const combo = keyboardEventToKeyComboString(event);
|
const combo = keyboardEventToKeyComboString(event);
|
||||||
// console.log(`pressed: ${combo}`);
|
// console.log(`pressed: ${combo}`);
|
||||||
if (actionsByKeys[combo]) {
|
if (keySetToUse[combo]) {
|
||||||
const handled = actionsByKeys[combo](context.editor, context);
|
const handled = keySetToUse[combo](context.editor, context);
|
||||||
if (handled) {
|
if (handled) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -127,8 +134,8 @@ function overrideDefaultCommands(editor: LexicalEditor) {
|
|||||||
}, COMMAND_PRIORITY_HIGH);
|
}, COMMAND_PRIORITY_HIGH);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerShortcuts(context: EditorUiContext) {
|
export function registerShortcuts(context: EditorUiContext, useExtended: boolean) {
|
||||||
const listener = createKeyDownListener(context);
|
const listener = createKeyDownListener(context, useExtended);
|
||||||
overrideDefaultCommands(context.editor);
|
overrideDefaultCommands(context.editor);
|
||||||
|
|
||||||
return context.editor.registerRootListener((rootElement: null | HTMLElement, prevRootElement: null | HTMLElement) => {
|
return context.editor.registerRootListener((rootElement: null | HTMLElement, prevRootElement: null | HTMLElement) => {
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ export function getBasicEditorToolbar(context: EditorUiContext): EditorContainer
|
|||||||
new EditorButton(bold),
|
new EditorButton(bold),
|
||||||
new EditorButton(italic),
|
new EditorButton(italic),
|
||||||
new EditorButton(link),
|
new EditorButton(link),
|
||||||
|
new EditorButton(code),
|
||||||
new EditorButton(bulletList),
|
new EditorButton(bulletList),
|
||||||
new EditorButton(numberList),
|
new EditorButton(numberList),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'DejaVu Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set fonts to common system fonts, starting with DejaVu Sans due to support in DOMPDF
|
||||||
|
body, h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: 'DejaVu Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -100,4 +104,4 @@ body.export-format-pdf.export-engine-dompdf {
|
|||||||
.page-content td a > img {
|
.page-content td a > img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="flex-container-row items-center gap-m">
|
<div class="flex-container-row items-center gap-m">
|
||||||
<span class="api-method text-mono" data-method="{{ $endpoint['method'] }}">{{ $endpoint['method'] }}</span>
|
<span class="api-method text-mono" data-method="{{ $endpoint['method'] }}">{{ $endpoint['method'] }}</span>
|
||||||
<h5 id="{{ $endpoint['name'] }}" class="text-mono pb-xs">
|
<h5 id="{{ $endpoint['name'] }}" class="text-mono pb-xs">
|
||||||
@if($endpoint['controller_method_kebab'] === 'list')
|
@if(str_starts_with($endpoint['controller_method_kebab'], 'list') && !str_contains($endpoint['uri'], '{'))
|
||||||
<a style="color: inherit;" target="_blank" rel="noopener" href="{{ url($endpoint['uri']) }}">{{ url($endpoint['uri']) }}</a>
|
<a style="color: inherit;" target="_blank" rel="noopener" href="{{ url($endpoint['uri']) }}">{{ url($endpoint['uri']) }}</a>
|
||||||
@else
|
@else
|
||||||
<span>{{ url($endpoint['uri']) }}</span>
|
<span>{{ url($endpoint['uri']) }}</span>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<p class="mb-none">
|
<p class="mb-none">
|
||||||
This documentation covers use of the REST API. <br>
|
This documentation covers use of the REST API. <br>
|
||||||
Examples of API usage, in a variety of programming languages, can be found in the <a href="https://codeberg.org/bookstack/api-scripts" target="_blank" rel="noopener noreferrer">BookStack api-scripts repo on GitHub</a>.
|
Examples of API usage, in a variety of programming languages, can be found in the <a href="https://codeberg.org/bookstack/api-scripts" target="_blank" rel="noopener noreferrer">BookStack api-scripts repo on Codeberg</a>.
|
||||||
|
|
||||||
<br> <br>
|
<br> <br>
|
||||||
Some alternative options for extension and customization can be found below:
|
Some alternative options for extension and customization can be found below:
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
HTTP POST calls upon events occurring in BookStack.
|
HTTP POST calls upon events occurring in BookStack.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://github.com/BookStackApp/BookStack/blob/development/dev/docs/visual-theme-system.md" target="_blank" rel="noopener noreferrer">Visual Theme System</a> -
|
<a href="https://codeberg.org/bookstack/bookstack/src/branch/development/dev/docs/visual-theme-system.md" target="_blank" rel="noopener noreferrer">Visual Theme System</a> -
|
||||||
Methods to override views, translations and icons within BookStack.
|
Methods to override views, translations and icons within BookStack.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://github.com/BookStackApp/BookStack/blob/development/dev/docs/logical-theme-system.md" target="_blank" rel="noopener noreferrer">Logical Theme System</a> -
|
<a href="https://codeberg.org/bookstack/bookstack/src/branch/development/dev/docs/logical-theme-system.md" target="_blank" rel="noopener noreferrer">Logical Theme System</a> -
|
||||||
Methods to extend back-end functionality within BookStack.
|
Methods to extend back-end functionality within BookStack.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if ($entity->isA('page'))
|
@if ($entity->isA('page') && userCan(\BookStack\Permissions\Permission::RevisionViewAll))
|
||||||
<a href="{{ $entity->getUrl('/revisions') }}" class="entity-meta-item">
|
<a href="{{ $entity->getUrl('/revisions') }}" class="entity-meta-item">
|
||||||
@icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }}
|
@icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -113,13 +113,13 @@
|
|||||||
<a href="https://www.bookstackapp.com/docs/admin/debugging/" target="_blank">Review BookStack debugging documentation »</a>
|
<a href="https://www.bookstackapp.com/docs/admin/debugging/" target="_blank">Review BookStack debugging documentation »</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://github.com/BookStackApp/BookStack/releases" target="_blank">Ensure your instance is up-to-date »</a>
|
<a href="https://codeberg.org/bookstack/bookstack/releases" target="_blank">Ensure your instance is up-to-date »</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://github.com/BookStackApp/BookStack/issues?q=is%3Aissue+{{ urlencode($error) }}" target="_blank">Search for the issue on GitHub »</a>
|
<a href="https://codeberg.org/bookstack/bookstack/issues?q={{ urlencode($error) }}" target="_blank">Search for the issue on GitHub »</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://discord.gg/ztkBqR2" target="_blank">Ask for help via Discord »</a>
|
<a href="https://community.bookstackapp.com" target="_blank">Ask for help in our community forums »</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://duckduckgo.com/?q={{urlencode("BookStack {$error}")}}" target="_blank">Search the error message »</a>
|
<a href="https://duckduckgo.com/?q={{urlencode("BookStack {$error}")}}" target="_blank">Search the error message »</a>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="entity-meta">
|
<div class="entity-meta">
|
||||||
@if ($entity->isA('page'))
|
@if ($entity->isA('page') && userCan(\BookStack\Permissions\Permission::RevisionViewAll))
|
||||||
@icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br>
|
@icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,12 @@
|
|||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
<a href="{{ $page->getUrl('/revisions') }}" data-shortcut="revisions" class="icon-list-item">
|
@if(userCan(\BookStack\Permissions\Permission::RevisionViewAll))
|
||||||
<span>@icon('history')</span>
|
<a href="{{ $page->getUrl('/revisions') }}" data-shortcut="revisions" class="icon-list-item">
|
||||||
<span>{{ trans('entities.revisions') }}</span>
|
<span>@icon('history')</span>
|
||||||
</a>
|
<span>{{ trans('entities.revisions') }}</span>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
@if(userCan(\BookStack\Permissions\Permission::RestrictionsManage, $page))
|
@if(userCan(\BookStack\Permissions\Permission::RestrictionsManage, $page))
|
||||||
<a href="{{ $page->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
|
<a href="{{ $page->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
|
||||||
<span>@icon('lock')</span>
|
<span>@icon('lock')</span>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user