mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-05-04 18:08:46 +03:00
Compare commits
36 Commits
release
...
codeberg-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4e3fa4822f | ||
|
|
684a94c419 | ||
|
|
c3c8577f05 | ||
|
|
5fbaab4740 | ||
|
|
3d9d5fef51 | ||
|
|
5e78dc6ed5 | ||
|
|
c33853ed84 | ||
|
|
abed4eae0c | ||
|
|
c7d3775bb9 | ||
|
|
0b659671fe |
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
|
||||||
@@ -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: node:24-bullseye
|
||||||
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: node:24-bullseye
|
||||||
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: node:24-bullseye
|
||||||
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
|
||||||
@@ -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: node:24-bullseye
|
||||||
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: 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: node:24-bullseye
|
||||||
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:
|
||||||
|
DB_HOST: mysql
|
||||||
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:
|
||||||
|
DB_HOST: mysql
|
||||||
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:
|
||||||
|
DB_HOST: mysql
|
||||||
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: node:24-bullseye
|
||||||
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:
|
||||||
|
DB_HOST: mysql
|
||||||
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:
|
||||||
|
DB_HOST: mysql
|
||||||
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
|
|
||||||
10
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
10
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -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
|
||||||
|
|||||||
11
.github/pull_request_template.md
vendored
Normal file
11
.github/pull_request_template.md
vendored
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.
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -2,16 +2,17 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/.vscode
|
/.vscode
|
||||||
/composer
|
/composer
|
||||||
|
/composer.phar
|
||||||
/coverage
|
/coverage
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
.env
|
.env
|
||||||
.idea
|
.idea
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
/public/dist/*.map
|
/public/dist
|
||||||
/public/plugins
|
/public/plugins
|
||||||
/public/css/*.map
|
/public/css
|
||||||
/public/js/*.map
|
/public/js
|
||||||
/public/bower
|
/public/bower
|
||||||
/public/build/
|
/public/build/
|
||||||
/public/favicon.ico
|
/public/favicon.ico
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace BookStack\Access;
|
|||||||
use BookStack\Access\Notifications\ConfirmEmailNotification;
|
use BookStack\Access\Notifications\ConfirmEmailNotification;
|
||||||
use BookStack\Exceptions\ConfirmationEmailException;
|
use BookStack\Exceptions\ConfirmationEmailException;
|
||||||
use BookStack\Users\Models\User;
|
use BookStack\Users\Models\User;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
class EmailConfirmationService extends UserTokenService
|
class EmailConfirmationService extends UserTokenService
|
||||||
{
|
{
|
||||||
@@ -16,6 +17,7 @@ class EmailConfirmationService extends UserTokenService
|
|||||||
* Also removes any existing old ones.
|
* Also removes any existing old ones.
|
||||||
*
|
*
|
||||||
* @throws ConfirmationEmailException
|
* @throws ConfirmationEmailException
|
||||||
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function sendConfirmation(User $user): void
|
public function sendConfirmation(User $user): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class LoginService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$lastLoginDetails = $this->getLastLoginAttemptDetails();
|
$lastLoginDetails = $this->getLastLoginAttemptDetails();
|
||||||
$this->login($user, $lastLoginDetails['method'], $lastLoginDetails['remember'] ?? false);
|
$this->login($user, $lastLoginDetails['method'], $lastLoginDetails['remember']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -48,17 +48,16 @@ class MfaValue extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Easily get the decrypted MFA value for the given user and method.
|
* Get the decrypted MFA value for the given user and method.
|
||||||
*/
|
*/
|
||||||
public static function getValueForUser(User $user, string $method): ?string
|
public static function getValueForUser(User $user, string $method): ?string
|
||||||
{
|
{
|
||||||
/** @var MfaValue $mfaVal */
|
|
||||||
$mfaVal = static::query()
|
$mfaVal = static::query()
|
||||||
->where('user_id', '=', $user->id)
|
->where('user_id', '=', $user->id)
|
||||||
->where('method', '=', $method)
|
->where('method', '=', $method)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
return $mfaVal ? $mfaVal->getValue() : null;
|
return $mfaVal?->getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ use phpseclib3\Math\BigInteger;
|
|||||||
|
|
||||||
class OidcJwtSigningKey
|
class OidcJwtSigningKey
|
||||||
{
|
{
|
||||||
/**
|
protected PublicKey $key;
|
||||||
* @var PublicKey
|
|
||||||
*/
|
|
||||||
protected $key;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can be created either from a JWK parameter array or local file path to load a certificate from.
|
* Can be created either from a JWK parameter array or local file path to load a certificate from.
|
||||||
@@ -20,15 +17,13 @@ class OidcJwtSigningKey
|
|||||||
* 'file:///var/www/cert.pem'
|
* 'file:///var/www/cert.pem'
|
||||||
* ['kty' => 'RSA', 'alg' => 'RS256', 'n' => 'abc123...'].
|
* ['kty' => 'RSA', 'alg' => 'RS256', 'n' => 'abc123...'].
|
||||||
*
|
*
|
||||||
* @param array|string $jwkOrKeyPath
|
|
||||||
*
|
|
||||||
* @throws OidcInvalidKeyException
|
* @throws OidcInvalidKeyException
|
||||||
*/
|
*/
|
||||||
public function __construct($jwkOrKeyPath)
|
public function __construct(array|string $jwkOrKeyPath)
|
||||||
{
|
{
|
||||||
if (is_array($jwkOrKeyPath)) {
|
if (is_array($jwkOrKeyPath)) {
|
||||||
$this->loadFromJwkArray($jwkOrKeyPath);
|
$this->loadFromJwkArray($jwkOrKeyPath);
|
||||||
} elseif (is_string($jwkOrKeyPath) && strpos($jwkOrKeyPath, 'file://') === 0) {
|
} elseif (str_starts_with($jwkOrKeyPath, 'file://')) {
|
||||||
$this->loadFromPath($jwkOrKeyPath);
|
$this->loadFromPath($jwkOrKeyPath);
|
||||||
} else {
|
} else {
|
||||||
throw new OidcInvalidKeyException('Unexpected type of key value provided');
|
throw new OidcInvalidKeyException('Unexpected type of key value provided');
|
||||||
@@ -38,7 +33,7 @@ class OidcJwtSigningKey
|
|||||||
/**
|
/**
|
||||||
* @throws OidcInvalidKeyException
|
* @throws OidcInvalidKeyException
|
||||||
*/
|
*/
|
||||||
protected function loadFromPath(string $path)
|
protected function loadFromPath(string $path): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$key = PublicKeyLoader::load(
|
$key = PublicKeyLoader::load(
|
||||||
@@ -58,7 +53,7 @@ class OidcJwtSigningKey
|
|||||||
/**
|
/**
|
||||||
* @throws OidcInvalidKeyException
|
* @throws OidcInvalidKeyException
|
||||||
*/
|
*/
|
||||||
protected function loadFromJwkArray(array $jwk)
|
protected function loadFromJwkArray(array $jwk): void
|
||||||
{
|
{
|
||||||
// 'alg' is optional for a JWK, but we will still attempt to validate if
|
// 'alg' is optional for a JWK, but we will still attempt to validate if
|
||||||
// it exists otherwise presume it will be compatible.
|
// it exists otherwise presume it will be compatible.
|
||||||
@@ -82,7 +77,7 @@ class OidcJwtSigningKey
|
|||||||
throw new OidcInvalidKeyException('A "n" parameter on the provided key is expected');
|
throw new OidcInvalidKeyException('A "n" parameter on the provided key is expected');
|
||||||
}
|
}
|
||||||
|
|
||||||
$n = strtr($jwk['n'] ?? '', '-_', '+/');
|
$n = strtr($jwk['n'], '-_', '+/');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$key = PublicKeyLoader::load([
|
$key = PublicKeyLoader::load([
|
||||||
|
|||||||
@@ -102,12 +102,12 @@ class OidcJwtWithClaims implements ProvidesClaims
|
|||||||
protected function validateTokenStructure(): void
|
protected function validateTokenStructure(): void
|
||||||
{
|
{
|
||||||
foreach (['header', 'payload'] as $prop) {
|
foreach (['header', 'payload'] as $prop) {
|
||||||
if (empty($this->$prop) || !is_array($this->$prop)) {
|
if (empty($this->$prop)) {
|
||||||
throw new OidcInvalidTokenException("Could not parse out a valid {$prop} within the provided token");
|
throw new OidcInvalidTokenException("Could not parse out a valid {$prop} within the provided token");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($this->signature) || !is_string($this->signature)) {
|
if (empty($this->signature)) {
|
||||||
throw new OidcInvalidTokenException('Could not parse out a valid signature within the provided token');
|
throw new OidcInvalidTokenException('Could not parse out a valid signature within the provided token');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class OidcUserDetails
|
|||||||
): void {
|
): void {
|
||||||
$this->externalId = $claims->getClaim($idClaim) ?? $this->externalId;
|
$this->externalId = $claims->getClaim($idClaim) ?? $this->externalId;
|
||||||
$this->email = $claims->getClaim('email') ?? $this->email;
|
$this->email = $claims->getClaim('email') ?? $this->email;
|
||||||
$this->name = static::getUserDisplayName($displayNameClaims, $claims) ?? $this->name;
|
$this->name = static::getUserDisplayName($displayNameClaims, $claims) ?: $this->name;
|
||||||
$this->groups = static::getUserGroups($groupsClaim, $claims) ?? $this->groups;
|
$this->groups = static::getUserGroups($groupsClaim, $claims) ?? $this->groups;
|
||||||
$this->picture = static::getPicture($claims) ?: $this->picture;
|
$this->picture = static::getPicture($claims) ?: $this->picture;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ class Saml2Service
|
|||||||
/**
|
/**
|
||||||
* Extract the details of a user from a SAML response.
|
* Extract the details of a user from a SAML response.
|
||||||
*
|
*
|
||||||
* @return array{external_id: string, name: string, email: string, saml_id: string}
|
* @return array{external_id: string, name: string, email: string|null, saml_id: string}
|
||||||
*/
|
*/
|
||||||
protected function getUserDetails(string $samlID, $samlAttributes): array
|
protected function getUserDetails(string $samlID, $samlAttributes): array
|
||||||
{
|
{
|
||||||
@@ -357,7 +357,7 @@ class Saml2Service
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($userDetails['email'] === null) {
|
if (empty($userDetails['email'])) {
|
||||||
throw new SamlException(trans('errors.saml_no_email_address'));
|
throw new SamlException(trans('errors.saml_no_email_address'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,14 +117,14 @@ class SocialAuthService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// When a user is logged in and the social account exists and is already linked to the current user.
|
// When a user is logged in and the social account exists and is already linked to the current user.
|
||||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
|
if ($isLoggedIn && $socialAccount->user->id === $currentUser->id) {
|
||||||
session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
|
session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
|
||||||
|
|
||||||
return redirect('/my-account/auth#social_accounts');
|
return redirect('/my-account/auth#social_accounts');
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a user is logged in, A social account exists but the users do not match.
|
// When a user is logged in, A social account exists but the users do not match.
|
||||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
|
if ($isLoggedIn && $socialAccount->user->id != $currentUser->id) {
|
||||||
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));
|
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));
|
||||||
|
|
||||||
return redirect('/my-account/auth#social_accounts');
|
return redirect('/my-account/auth#social_accounts');
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ use BookStack\Users\Models\User;
|
|||||||
class NotificationManager
|
class NotificationManager
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var class-string<NotificationHandler>[]
|
* @var array<string, class-string<NotificationHandler>[]>
|
||||||
*/
|
*/
|
||||||
protected array $handlers = [];
|
protected array $handlersByActivity = [];
|
||||||
|
|
||||||
public function handle(Activity $activity, string|Loggable $detail, User $user): void
|
public function handle(Activity $activity, string|Loggable $detail, User $user): void
|
||||||
{
|
{
|
||||||
$activityType = $activity->type;
|
$activityType = $activity->type;
|
||||||
$handlersToRun = $this->handlers[$activityType] ?? [];
|
$handlersToRun = $this->handlersByActivity[$activityType] ?? [];
|
||||||
foreach ($handlersToRun as $handlerClass) {
|
foreach ($handlersToRun as $handlerClass) {
|
||||||
/** @var NotificationHandler $handler */
|
/** @var NotificationHandler $handler */
|
||||||
$handler = new $handlerClass();
|
$handler = new $handlerClass();
|
||||||
@@ -35,12 +35,12 @@ class NotificationManager
|
|||||||
*/
|
*/
|
||||||
public function registerHandler(string $activityType, string $handlerClass): void
|
public function registerHandler(string $activityType, string $handlerClass): void
|
||||||
{
|
{
|
||||||
if (!isset($this->handlers[$activityType])) {
|
if (!isset($this->handlersByActivity[$activityType])) {
|
||||||
$this->handlers[$activityType] = [];
|
$this->handlersByActivity[$activityType] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!in_array($handlerClass, $this->handlers[$activityType])) {
|
if (!in_array($handlerClass, $this->handlersByActivity[$activityType])) {
|
||||||
$this->handlers[$activityType][] = $handlerClass;
|
$this->handlersByActivity[$activityType][] = $handlerClass;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,7 +17,14 @@ use ReflectionMethod;
|
|||||||
|
|
||||||
class ApiDocsGenerator
|
class ApiDocsGenerator
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, ReflectionClass>
|
||||||
|
*/
|
||||||
protected array $reflectionClasses = [];
|
protected array $reflectionClasses = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, ApiController>
|
||||||
|
*/
|
||||||
protected array $controllerClasses = [];
|
protected array $controllerClasses = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,7 +114,6 @@ class ApiDocsGenerator
|
|||||||
*/
|
*/
|
||||||
protected function getBodyParamsFromClass(string $className, string $methodName): ?array
|
protected function getBodyParamsFromClass(string $className, string $methodName): ?array
|
||||||
{
|
{
|
||||||
/** @var ApiController $class */
|
|
||||||
$class = $this->controllerClasses[$className] ?? null;
|
$class = $this->controllerClasses[$className] ?? null;
|
||||||
if ($class === null) {
|
if ($class === null) {
|
||||||
$class = app()->make($className);
|
$class = app()->make($className);
|
||||||
@@ -153,7 +159,7 @@ class ApiDocsGenerator
|
|||||||
$matches = [];
|
$matches = [];
|
||||||
preg_match_all('/^\s*?\*\s?($|((?![\/@\s]).*?))$/m', $comment, $matches);
|
preg_match_all('/^\s*?\*\s?($|((?![\/@\s]).*?))$/m', $comment, $matches);
|
||||||
|
|
||||||
$text = implode(' ', $matches[1] ?? []);
|
$text = implode(' ', $matches[1]);
|
||||||
return str_replace(' ', "\n", $text);
|
return str_replace(' ', "\n", $text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,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,
|
||||||
@@ -201,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,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -74,18 +74,21 @@ class ApiEntityListFormatter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Include parent book/chapter info in the formatted data.
|
* Include parent book/chapter info in the formatted data.
|
||||||
|
* These functions are careful to not load the relation themselves, since they should
|
||||||
|
* have already been loaded in a more efficient manner, with permissions applied, by the time
|
||||||
|
* the parent fields are handled here.
|
||||||
*/
|
*/
|
||||||
public function withParents(): self
|
public function withParents(): self
|
||||||
{
|
{
|
||||||
$this->withField('book', function (Entity $entity) {
|
$this->withField('book', function (Entity $entity) {
|
||||||
if ($entity instanceof BookChild && $entity->book) {
|
if ($entity instanceof BookChild && $entity->relationLoaded('book') && $entity->getRelationValue('book')) {
|
||||||
return $entity->book->only(['id', 'name', 'slug']);
|
return $entity->book->only(['id', 'name', 'slug']);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->withField('chapter', function (Entity $entity) {
|
$this->withField('chapter', function (Entity $entity) {
|
||||||
if ($entity instanceof Page && $entity->chapter) {
|
if ($entity instanceof Page && $entity->relationLoaded('chapter') && $entity->getRelationValue('chapter')) {
|
||||||
return $entity->chapter->only(['id', 'name', 'slug']);
|
return $entity->chapter->only(['id', 'name', 'slug']);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -16,30 +16,15 @@ class ApiTokenGuard implements Guard
|
|||||||
{
|
{
|
||||||
use GuardHelpers;
|
use GuardHelpers;
|
||||||
|
|
||||||
/**
|
|
||||||
* The request instance.
|
|
||||||
*/
|
|
||||||
protected $request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var LoginService
|
|
||||||
*/
|
|
||||||
protected $loginService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The last auth exception thrown in this request.
|
* The last auth exception thrown in this request.
|
||||||
*
|
|
||||||
* @var ApiAuthException
|
|
||||||
*/
|
*/
|
||||||
protected $lastAuthException;
|
protected ApiAuthException|null $lastAuthException = null;
|
||||||
|
|
||||||
/**
|
public function __construct(
|
||||||
* ApiTokenGuard constructor.
|
protected Request $request,
|
||||||
*/
|
protected LoginService $loginService
|
||||||
public function __construct(Request $request, LoginService $loginService)
|
) {
|
||||||
{
|
|
||||||
$this->request = $request;
|
|
||||||
$this->loginService = $loginService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,7 +52,7 @@ class ApiTokenGuard implements Guard
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if current user is authenticated. If not, throw an exception.
|
* Determine if the current user is authenticated. If not, throw an exception.
|
||||||
*
|
*
|
||||||
* @throws ApiAuthException
|
* @throws ApiAuthException
|
||||||
*
|
*
|
||||||
@@ -121,7 +106,7 @@ class ApiTokenGuard implements Guard
|
|||||||
throw new ApiAuthException(trans('errors.api_no_authorization_found'));
|
throw new ApiAuthException(trans('errors.api_no_authorization_found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strpos($authToken, ':') === false || strpos($authToken, 'Token ') !== 0) {
|
if (!str_contains($authToken, ':') || !str_starts_with($authToken, 'Token ')) {
|
||||||
throw new ApiAuthException(trans('errors.api_bad_authorization_format'));
|
throw new ApiAuthException(trans('errors.api_bad_authorization_format'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +140,7 @@ class ApiTokenGuard implements Guard
|
|||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function validate(array $credentials = [])
|
public function validate(array $credentials = []): bool
|
||||||
{
|
{
|
||||||
if (empty($credentials['id']) || empty($credentials['secret'])) {
|
if (empty($credentials['id']) || empty($credentials['secret'])) {
|
||||||
return false;
|
return false;
|
||||||
@@ -175,7 +160,7 @@ class ApiTokenGuard implements Guard
|
|||||||
/**
|
/**
|
||||||
* "Log out" the currently authenticated user.
|
* "Log out" the currently authenticated user.
|
||||||
*/
|
*/
|
||||||
public function logout()
|
public function logout(): void
|
||||||
{
|
{
|
||||||
$this->user = null;
|
$this->user = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class AssignSortRuleCommand extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle(BookSorter $sorter): int
|
public function handle(BookSorter $sorter): int
|
||||||
{
|
{
|
||||||
$sortRuleId = intval($this->argument('sort-rule')) ?? 0;
|
$sortRuleId = intval($this->argument('sort-rule'));
|
||||||
if ($sortRuleId === 0) {
|
if ($sortRuleId === 0) {
|
||||||
return $this->listSortRules();
|
return $this->listSortRules();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class CopyShelfPermissionsCommand extends Command
|
|||||||
{
|
{
|
||||||
$shelfSlug = $this->option('slug');
|
$shelfSlug = $this->option('slug');
|
||||||
$cascadeAll = $this->option('all');
|
$cascadeAll = $this->option('all');
|
||||||
|
$noInteraction = boolval($this->option('no-interaction'));
|
||||||
$shelves = null;
|
$shelves = null;
|
||||||
|
|
||||||
if (!$cascadeAll && !$shelfSlug) {
|
if (!$cascadeAll && !$shelfSlug) {
|
||||||
@@ -41,15 +42,17 @@ class CopyShelfPermissionsCommand extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($cascadeAll) {
|
if ($cascadeAll) {
|
||||||
|
if (!$noInteraction) {
|
||||||
$continue = $this->confirm(
|
$continue = $this->confirm(
|
||||||
'Permission settings for all shelves will be cascaded. ' .
|
'Permission settings for all shelves will be cascaded. ' .
|
||||||
'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. ' .
|
'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. ' .
|
||||||
'Are you sure you want to proceed?'
|
'Are you sure you want to proceed?',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!$continue && !$this->hasOption('no-interaction')) {
|
if (!$continue) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$shelves = $queries->start()->get(['id']);
|
$shelves = $queries->start()->get(['id']);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,17 +213,25 @@ class InstallModuleCommand extends Command
|
|||||||
$redirectLocation = $resp->getHeaderLine('Location');
|
$redirectLocation = $resp->getHeaderLine('Location');
|
||||||
if ($redirectLocation) {
|
if ($redirectLocation) {
|
||||||
$redirectUrl = parse_url($redirectLocation);
|
$redirectUrl = parse_url($redirectLocation);
|
||||||
if (
|
$redirectOriginMatches = ($originalUrl['host'] ?? '') === ($redirectUrl['host'] ?? '')
|
||||||
($originalUrl['host'] ?? '') === ($redirectUrl['host'] ?? '')
|
|
||||||
&& ($originalUrl['scheme'] ?? '') === ($redirectUrl['scheme'] ?? '')
|
&& ($originalUrl['scheme'] ?? '') === ($redirectUrl['scheme'] ?? '')
|
||||||
&& ($originalUrl['port'] ?? '') === ($redirectUrl['port'] ?? '')
|
&& ($originalUrl['port'] ?? '') === ($redirectUrl['port'] ?? '');
|
||||||
) {
|
|
||||||
|
if (!$redirectOriginMatches) {
|
||||||
|
$redirectOrigin = ($redirectUrl['scheme'] ?? '') . '://' . ($redirectUrl['host'] ?? '') . (isset($redirectUrl['port']) ? ':' . $redirectUrl['port'] : '');
|
||||||
|
$this->info("The download URL is redirecting to a different site: {$redirectOrigin}");
|
||||||
|
$shouldContinue = $this->confirm("Do you trust downloading the module from this site?");
|
||||||
|
if (!$shouldContinue) {
|
||||||
|
$this->error("Stopping module installation");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$currentLocation = $redirectLocation;
|
$currentLocation = $redirectLocation;
|
||||||
$redirectCount++;
|
$redirectCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
} while (true);
|
} while (true);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use Illuminate\Support\Collection;
|
|||||||
*
|
*
|
||||||
* @property string $description
|
* @property string $description
|
||||||
* @property string $description_html
|
* @property string $description_html
|
||||||
* @property int $image_id
|
* @property ?int $image_id
|
||||||
* @property ?int $default_template_id
|
* @property ?int $default_template_id
|
||||||
* @property ?int $sort_rule_id
|
* @property ?int $sort_rule_id
|
||||||
* @property \Illuminate\Database\Eloquent\Collection $chapters
|
* @property \Illuminate\Database\Eloquent\Collection $chapters
|
||||||
|
|||||||
@@ -479,6 +479,7 @@ abstract class Entity extends Model implements
|
|||||||
'chapter' => new Chapter(),
|
'chapter' => new Chapter(),
|
||||||
'book' => new Book(),
|
'book' => new Book(),
|
||||||
'bookshelf' => new Bookshelf(),
|
'bookshelf' => new Bookshelf(),
|
||||||
|
default => throw new \InvalidArgumentException("Invalid entity type: {$type}"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
|
|||||||
* @property bool $draft
|
* @property bool $draft
|
||||||
* @property int $revision_count
|
* @property int $revision_count
|
||||||
* @property string $editor
|
* @property string $editor
|
||||||
* @property Chapter $chapter
|
* @property Chapter|null $chapter
|
||||||
* @property Collection $attachments
|
* @property Collection $attachments
|
||||||
* @property Collection $revisions
|
* @property Collection $revisions
|
||||||
* @property PageRevision $currentRevision
|
* @property PageRevision $currentRevision
|
||||||
|
|||||||
@@ -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']);
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class PageRepo
|
|||||||
$page->book_id = $parent->id;
|
$page->book_id = $parent->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
$defaultTemplate = $page->chapter?->defaultTemplate()->get() ?? $page->book?->defaultTemplate()->get();
|
$defaultTemplate = $page->chapter?->defaultTemplate()->get() ?? $page->book->defaultTemplate()->get();
|
||||||
if ($defaultTemplate) {
|
if ($defaultTemplate) {
|
||||||
$page->forceFill([
|
$page->forceFill([
|
||||||
'html' => $defaultTemplate->html,
|
'html' => $defaultTemplate->html,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -359,7 +360,7 @@ class PageContent
|
|||||||
{
|
{
|
||||||
$contentHash = md5($html);
|
$contentHash = md5($html);
|
||||||
$contentId = $this->page->id;
|
$contentId = $this->page->id;
|
||||||
$contentTime = $this->page->updated_at?->timestamp ?? time();
|
$contentTime = $this->page->updated_at->timestamp ?? time();
|
||||||
$appVersion = AppVersion::get();
|
$appVersion = AppVersion::get();
|
||||||
$filterConfig = config('app.content_filtering') ?? '';
|
$filterConfig = config('app.content_filtering') ?? '';
|
||||||
return "page-content-cache::{$filterConfig}::{$appVersion}::{$contentId}::{$contentTime}::{$contentHash}";
|
return "page-content-cache::{$filterConfig}::{$appVersion}::{$contentId}::{$contentTime}::{$contentHash}";
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ class PermissionsUpdater
|
|||||||
{
|
{
|
||||||
if (isset($data['role_permissions'])) {
|
if (isset($data['role_permissions'])) {
|
||||||
$entity->permissions()->where('role_id', '!=', 0)->delete();
|
$entity->permissions()->where('role_id', '!=', 0)->delete();
|
||||||
$rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['role_permissions'] ?? [], false);
|
$rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['role_permissions'], false);
|
||||||
$entity->permissions()->createMany($rolePermissionData);
|
$entity->permissions()->createMany($rolePermissionData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -208,7 +209,7 @@ class ExportFormatter
|
|||||||
preg_match_all("/\<img.*?src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
|
preg_match_all("/\<img.*?src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
|
||||||
|
|
||||||
// Replace image src with base64 encoded image strings
|
// Replace image src with base64 encoded image strings
|
||||||
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
|
if (count($imageTagsOutput[0]) > 0) {
|
||||||
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
||||||
$oldImgTagString = $imgMatch;
|
$oldImgTagString = $imgMatch;
|
||||||
$srcString = $imageTagsOutput[2][$index];
|
$srcString = $imageTagsOutput[2][$index];
|
||||||
@@ -225,7 +226,7 @@ class ExportFormatter
|
|||||||
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
|
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
|
||||||
|
|
||||||
// Update relative links to be absolute, with instance url
|
// Update relative links to be absolute, with instance url
|
||||||
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
|
if (count($linksOutput[0]) > 0) {
|
||||||
foreach ($linksOutput[0] as $index => $linkMatch) {
|
foreach ($linksOutput[0] as $index => $linkMatch) {
|
||||||
$oldLinkString = $linkMatch;
|
$oldLinkString = $linkMatch;
|
||||||
$srcString = $linksOutput[2][$index];
|
$srcString = $linksOutput[2][$index];
|
||||||
@@ -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()],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -82,10 +82,8 @@ class ZipImportRunner
|
|||||||
$entity = $this->importBook($exportModel, $reader);
|
$entity = $this->importBook($exportModel, $reader);
|
||||||
} else if ($exportModel instanceof ZipExportChapter) {
|
} else if ($exportModel instanceof ZipExportChapter) {
|
||||||
$entity = $this->importChapter($exportModel, $parent, $reader);
|
$entity = $this->importChapter($exportModel, $parent, $reader);
|
||||||
} else if ($exportModel instanceof ZipExportPage) {
|
|
||||||
$entity = $this->importPage($exportModel, $parent, $reader);
|
|
||||||
} else {
|
} else {
|
||||||
throw new ZipImportException(['No importable data found in import data.']);
|
$entity = $this->importPage($exportModel, $parent, $reader);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->references->replaceReferences();
|
$this->references->replaceReferences();
|
||||||
@@ -132,7 +130,7 @@ class ZipImportRunner
|
|||||||
'name' => $exportBook->name,
|
'name' => $exportBook->name,
|
||||||
'description_html' => $exportBook->description_html ?? '',
|
'description_html' => $exportBook->description_html ?? '',
|
||||||
'image' => $exportBook->cover ? $this->zipFileToUploadedFile($exportBook->cover, $reader) : null,
|
'image' => $exportBook->cover ? $this->zipFileToUploadedFile($exportBook->cover, $reader) : null,
|
||||||
'tags' => $this->exportTagsToInputArray($exportBook->tags ?? []),
|
'tags' => $this->exportTagsToInputArray($exportBook->tags),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($book->coverInfo()->getImage()) {
|
if ($book->coverInfo()->getImage()) {
|
||||||
@@ -151,7 +149,7 @@ class ZipImportRunner
|
|||||||
foreach ($children as $child) {
|
foreach ($children as $child) {
|
||||||
if ($child instanceof ZipExportChapter) {
|
if ($child instanceof ZipExportChapter) {
|
||||||
$this->importChapter($child, $book, $reader);
|
$this->importChapter($child, $book, $reader);
|
||||||
} else if ($child instanceof ZipExportPage) {
|
} else {
|
||||||
$this->importPage($child, $book, $reader);
|
$this->importPage($child, $book, $reader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,7 +164,7 @@ class ZipImportRunner
|
|||||||
$chapter = $this->chapterRepo->create([
|
$chapter = $this->chapterRepo->create([
|
||||||
'name' => $exportChapter->name,
|
'name' => $exportChapter->name,
|
||||||
'description_html' => $exportChapter->description_html ?? '',
|
'description_html' => $exportChapter->description_html ?? '',
|
||||||
'tags' => $this->exportTagsToInputArray($exportChapter->tags ?? []),
|
'tags' => $this->exportTagsToInputArray($exportChapter->tags),
|
||||||
], $parent);
|
], $parent);
|
||||||
|
|
||||||
$exportPages = $exportChapter->pages;
|
$exportPages = $exportChapter->pages;
|
||||||
@@ -199,7 +197,7 @@ class ZipImportRunner
|
|||||||
'name' => $exportPage->name,
|
'name' => $exportPage->name,
|
||||||
'markdown' => $exportPage->markdown ?? '',
|
'markdown' => $exportPage->markdown ?? '',
|
||||||
'html' => $exportPage->html ?? '',
|
'html' => $exportPage->html ?? '',
|
||||||
'tags' => $this->exportTagsToInputArray($exportPage->tags ?? []),
|
'tags' => $this->exportTagsToInputArray($exportPage->tags),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->references->addPage($page, $exportPage);
|
$this->references->addPage($page, $exportPage);
|
||||||
@@ -302,7 +300,7 @@ class ZipImportRunner
|
|||||||
array_push($chapters, ...$exportModel->chapters);
|
array_push($chapters, ...$exportModel->chapters);
|
||||||
} else if ($exportModel instanceof ZipExportChapter) {
|
} else if ($exportModel instanceof ZipExportChapter) {
|
||||||
$chapters[] = $exportModel;
|
$chapters[] = $exportModel;
|
||||||
} else if ($exportModel instanceof ZipExportPage) {
|
} else {
|
||||||
$pages[] = $exportModel;
|
$pages[] = $exportModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,10 +68,6 @@ class ZipReferenceParser
|
|||||||
$matches = [];
|
$matches = [];
|
||||||
preg_match_all($referenceRegex, $content, $matches);
|
preg_match_all($referenceRegex, $content, $matches);
|
||||||
|
|
||||||
if (count($matches) < 3) {
|
|
||||||
return $content;
|
|
||||||
}
|
|
||||||
|
|
||||||
for ($i = 0; $i < count($matches[0]); $i++) {
|
for ($i = 0; $i < count($matches[0]); $i++) {
|
||||||
$referenceText = $matches[0][$i];
|
$referenceText = $matches[0][$i];
|
||||||
$type = strtolower($matches[1][$i]);
|
$type = strtolower($matches[1][$i]);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ abstract class Controller extends BaseController
|
|||||||
*/
|
*/
|
||||||
protected function checkPermission(string|Permission $permission): void
|
protected function checkPermission(string|Permission $permission): void
|
||||||
{
|
{
|
||||||
if (!user() || !user()->can($permission)) {
|
if (!user()->can($permission)) {
|
||||||
$this->showPermissionError();
|
$this->showPermissionError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,7 @@ class JointPermissionBuilder
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var BookChild $entity */
|
if ($entity instanceof BookChild) {
|
||||||
if ($entity->book) {
|
|
||||||
$entities[] = $entity->book;
|
$entities[] = $entity->book;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -121,14 +121,12 @@ class SearchOptions
|
|||||||
foreach ($patterns as $termType => $pattern) {
|
foreach ($patterns as $termType => $pattern) {
|
||||||
$matches = [];
|
$matches = [];
|
||||||
preg_match_all($pattern, $searchString, $matches);
|
preg_match_all($pattern, $searchString, $matches);
|
||||||
if (count($matches) > 0) {
|
|
||||||
foreach ($matches[1] as $index => $value) {
|
foreach ($matches[1] as $index => $value) {
|
||||||
$negated = str_starts_with($matches[0][$index], '-');
|
$negated = str_starts_with($matches[0][$index], '-');
|
||||||
$terms[$termType][] = $constructors[$termType]($value, $negated);
|
$terms[$termType][] = $constructors[$termType]($value, $negated);
|
||||||
}
|
}
|
||||||
$searchString = preg_replace($pattern, '', $searchString);
|
$searchString = preg_replace($pattern, '', $searchString);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Unescape exacts and backslash escapes
|
// Unescape exacts and backslash escapes
|
||||||
foreach ($terms['exacts'] as $exact) {
|
foreach ($terms['exacts'] as $exact) {
|
||||||
@@ -261,7 +259,7 @@ class SearchOptions
|
|||||||
$userFilters = ['updated_by', 'created_by', 'owned_by'];
|
$userFilters = ['updated_by', 'created_by', 'owned_by'];
|
||||||
$unsupportedFilters = ['is_template', 'sort_by'];
|
$unsupportedFilters = ['is_template', 'sort_by'];
|
||||||
foreach ($this->filters->all() as $filter) {
|
foreach ($this->filters->all() as $filter) {
|
||||||
if (in_array($filter->getKey(), $userFilters, true) && $filter->value !== null && $filter->value !== 'me') {
|
if (in_array($filter->getKey(), $userFilters, true) && $filter->value && $filter->value !== 'me') {
|
||||||
$options[] = $filter;
|
$options[] = $filter;
|
||||||
} else if (in_array($filter->getKey(), $unsupportedFilters, true)) {
|
} else if (in_array($filter->getKey(), $unsupportedFilters, true)) {
|
||||||
$options[] = $filter;
|
$options[] = $filter;
|
||||||
|
|||||||
@@ -120,14 +120,8 @@ class SearchRunner
|
|||||||
$filter = function (EloquentBuilder $query) use ($exact) {
|
$filter = function (EloquentBuilder $query) use ($exact) {
|
||||||
$inputTerm = str_replace('\\', '\\\\', $exact->value);
|
$inputTerm = str_replace('\\', '\\\\', $exact->value);
|
||||||
$query->where('name', 'like', '%' . $inputTerm . '%')
|
$query->where('name', 'like', '%' . $inputTerm . '%')
|
||||||
->orWhere(function (EloquentBuilder $query) use ($inputTerm) {
|
->orWhere('description', 'like', '%' . $inputTerm . '%')
|
||||||
$query->whereNotNull('description')
|
->orWhere('text', 'like', '%' . $inputTerm . '%');
|
||||||
->where('description', 'like', '%' . $inputTerm . '%');
|
|
||||||
})
|
|
||||||
->orWhere(function (EloquentBuilder $query) use ($inputTerm) {
|
|
||||||
$query->whereNotNull('text')
|
|
||||||
->where('text', 'like', '%' . $inputTerm . '%');
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$exact->negated ? $entityQuery->whereNot($filter) : $entityQuery->where($filter);
|
$exact->negated ? $entityQuery->whereNot($filter) : $entityQuery->where($filter);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,9 +125,8 @@ class BookSorter
|
|||||||
*/
|
*/
|
||||||
protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
|
protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
|
||||||
{
|
{
|
||||||
/** @var BookChild $model */
|
|
||||||
$model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;
|
$model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;
|
||||||
if (!$model) {
|
if (!($model instanceof BookChild)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,14 @@ class ThemeModuleManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
$folderPath = $this->modulesFolderPath . DIRECTORY_SEPARATOR . $folderName;
|
$folderPath = $this->modulesFolderPath . DIRECTORY_SEPARATOR . $folderName;
|
||||||
|
try {
|
||||||
$zip->extractTo($folderPath);
|
$zip->extractTo($folderPath);
|
||||||
|
} catch (ThemeModuleException $exception) {
|
||||||
|
if (is_dir($folderPath)) {
|
||||||
|
$this->deleteDirectoryRecursively($folderPath);
|
||||||
|
}
|
||||||
|
throw new ThemeModuleException("Failed to load extract files from module ZIP with error: {$exception->getMessage()}");
|
||||||
|
}
|
||||||
|
|
||||||
$module = $this->loadFromFolder($folderName);
|
$module = $this->loadFromFolder($folderName);
|
||||||
if (!$module) {
|
if (!$module) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Theming;
|
namespace BookStack\Theming;
|
||||||
|
|
||||||
|
use BookStack\Util\FilePathNormalizer;
|
||||||
use ZipArchive;
|
use ZipArchive;
|
||||||
|
|
||||||
readonly class ThemeModuleZip
|
readonly class ThemeModuleZip
|
||||||
@@ -15,7 +16,46 @@ readonly class ThemeModuleZip
|
|||||||
{
|
{
|
||||||
$zip = new ZipArchive();
|
$zip = new ZipArchive();
|
||||||
$zip->open($this->path);
|
$zip->open($this->path);
|
||||||
$zip->extractTo($destinationPath);
|
$prefix = $this->getZipContentPrefix($zip);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||||
|
$name = $zip->getNameIndex($i);
|
||||||
|
$entryIsDir = str_ends_with($name, "/");
|
||||||
|
if ($entryIsDir) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stream = $zip->getStreamIndex($i);
|
||||||
|
|
||||||
|
if ($prefix) {
|
||||||
|
if (!str_starts_with($name, $prefix) || $name === $prefix) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$name = str_replace($prefix, '', $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$targetPath = $destinationPath . DIRECTORY_SEPARATOR . FilePathNormalizer::normalize($name);
|
||||||
|
} catch (\Exception $exception) {
|
||||||
|
throw new ThemeModuleException("Bad file path found in module ZIP file: {$name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetPathDir = dirname($targetPath);
|
||||||
|
if (!is_dir($targetPathDir)) {
|
||||||
|
$dirCreated = mkdir($targetPathDir, 0777, true);
|
||||||
|
if (!$dirCreated) {
|
||||||
|
throw new ThemeModuleException("Failed to create directory {$targetPathDir} when extracting module files");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetFile = fopen($targetPath, 'w');
|
||||||
|
$written = stream_copy_to_stream($stream, $targetFile);
|
||||||
|
if (!$written) {
|
||||||
|
throw new ThemeModuleException("Failed to write to {$targetPath} when extracting module files");
|
||||||
|
}
|
||||||
|
fclose($targetFile);
|
||||||
|
}
|
||||||
|
|
||||||
$zip->close();
|
$zip->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +71,8 @@ readonly class ThemeModuleZip
|
|||||||
throw new ThemeModuleException("Unable to open zip file at {$this->path}");
|
throw new ThemeModuleException("Unable to open zip file at {$this->path}");
|
||||||
}
|
}
|
||||||
|
|
||||||
$moduleJsonText = $zip->getFromName('bookstack-module.json');
|
$prefix = $this->getZipContentPrefix($zip);
|
||||||
|
$moduleJsonText = $zip->getFromName("{$prefix}bookstack-module.json");
|
||||||
$zip->close();
|
$zip->close();
|
||||||
|
|
||||||
if ($moduleJsonText === false) {
|
if ($moduleJsonText === false) {
|
||||||
@@ -95,4 +136,20 @@ readonly class ThemeModuleZip
|
|||||||
|
|
||||||
return $totalSize;
|
return $totalSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getZipContentPrefix(ZipArchive $zip): string
|
||||||
|
{
|
||||||
|
$index = $zip->locateName('bookstack-module.json', ZipArchive::FL_NODIR);
|
||||||
|
if ($index === false) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$location = $zip->getNameIndex($index);
|
||||||
|
$pathParts = explode('/', $location);
|
||||||
|
if (count($pathParts) !== 2) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pathParts[0] . '/';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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', [
|
||||||
@@ -195,11 +195,10 @@ class AttachmentController extends Controller
|
|||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'order' => ['required', 'array'],
|
'order' => ['required', 'array'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||||
|
|
||||||
$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')]);
|
||||||
@@ -222,6 +221,8 @@ class AttachmentController extends Controller
|
|||||||
throw new NotFoundException(trans('errors.attachment_not_found'));
|
throw new NotFoundException(trans('errors.attachment_not_found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->checkOwnablePermission(Permission::PageView, $page);
|
||||||
|
|
||||||
if ($attachment->external) {
|
if ($attachment->external) {
|
||||||
return redirect($attachment->path);
|
return redirect($attachment->path);
|
||||||
}
|
}
|
||||||
@@ -230,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,13 +247,6 @@ class AttachmentController extends Controller
|
|||||||
{
|
{
|
||||||
/** @var Attachment $attachment */
|
/** @var Attachment $attachment */
|
||||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||||
|
|
||||||
try {
|
|
||||||
$this->pageQueries->findVisibleByIdOrFail($attachment->uploaded_to);
|
|
||||||
} catch (NotFoundException $exception) {
|
|
||||||
throw new NotFoundException(trans('errors.attachment_not_found'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->checkOwnablePermission(Permission::AttachmentDelete, $attachment);
|
$this->checkOwnablePermission(Permission::AttachmentDelete, $attachment);
|
||||||
$this->attachmentService->deleteFile($attachment);
|
$this->attachmentService->deleteFile($attachment);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class ImageRepo
|
|||||||
$parentFilter = function (Builder $query) use ($filterType, $contextPage) {
|
$parentFilter = function (Builder $query) use ($filterType, $contextPage) {
|
||||||
if ($filterType === 'page') {
|
if ($filterType === 'page') {
|
||||||
$query->where('uploaded_to', '=', $contextPage->id);
|
$query->where('uploaded_to', '=', $contextPage->id);
|
||||||
} else if ($filterType === 'book') {
|
} else {
|
||||||
$validPageIds = $contextPage->book->pages()
|
$validPageIds = $contextPage->book->pages()
|
||||||
->scopes('visible')
|
->scopes('visible')
|
||||||
->pluck('id')
|
->pluck('id')
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class UserAvatars
|
|||||||
$responseCount++;
|
$responseCount++;
|
||||||
$isRedirect = ($response->getStatusCode() === 301 || $response->getStatusCode() === 302);
|
$isRedirect = ($response->getStatusCode() === 301 || $response->getStatusCode() === 302);
|
||||||
$url = $response->getHeader('Location')[0] ?? '';
|
$url = $response->getHeader('Location')[0] ?? '';
|
||||||
} while ($responseCount < 3 && $isRedirect && is_string($url) && str_starts_with($url, 'http'));
|
} while ($responseCount < 3 && $isRedirect && str_starts_with($url, 'http'));
|
||||||
|
|
||||||
if ($responseCount === 3) {
|
if ($responseCount === 3) {
|
||||||
throw new HttpFetchException("Failed to fetch image, max redirect limit of 3 tries reached. Last fetched URL: {$url}");
|
throw new HttpFetchException("Failed to fetch image, max redirect limit of 3 tries reached. Last fetched URL: {$url}");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -222,8 +222,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||||||
public function getAvatar(int $size = 50): string
|
public function getAvatar(int $size = 50): string
|
||||||
{
|
{
|
||||||
$default = url('/user_avatar.png');
|
$default = url('/user_avatar.png');
|
||||||
$imageId = $this->image_id;
|
if ($this->image_id === 0) {
|
||||||
if ($imageId === 0 || $imageId === '0' || $imageId === null) {
|
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ use BookStack\Exceptions\HttpFetchException;
|
|||||||
* Validate the host we're connecting to when making a server-side-request.
|
* Validate the host we're connecting to when making a server-side-request.
|
||||||
* Will use the given hosts config if given during construction otherwise
|
* Will use the given hosts config if given during construction otherwise
|
||||||
* will look to the app configured config.
|
* will look to the app configured config.
|
||||||
*
|
|
||||||
* The config format is a space-seperated list of URL prefixes which should contain the
|
|
||||||
* protocol and host. It can optionally define a path prefix as part of the URL.
|
|
||||||
* Wildcards, via a '*', can be used within these elements to match anything but a '/'.
|
|
||||||
*/
|
*/
|
||||||
class SsrUrlValidator
|
class SsrUrlValidator
|
||||||
{
|
{
|
||||||
@@ -52,34 +48,15 @@ class SsrUrlValidator
|
|||||||
{
|
{
|
||||||
$pattern = rtrim(trim($pattern), '/');
|
$pattern = rtrim(trim($pattern), '/');
|
||||||
$url = trim($url);
|
$url = trim($url);
|
||||||
$urlParts = parse_url($url);
|
|
||||||
|
|
||||||
if (empty($pattern) || empty($url) || $urlParts === false) {
|
if (empty($pattern) || empty($url)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent potential tricks using percent encoded slashes
|
|
||||||
if (str_contains(strtolower($urlParts['host'] ?? ''), '%2f')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disregard query and fragment
|
|
||||||
$url = explode('?', $url, 2)[0];
|
|
||||||
$url = explode('#', $url, 2)[0];
|
|
||||||
|
|
||||||
// Disregard userinfo if existing
|
|
||||||
if (!empty($urlParts['user']) || !empty($urlParts['pass'])) {
|
|
||||||
[$start, $postUserinfo] = explode('@', $url, 2);
|
|
||||||
$preUserinfo = explode('//', $start, 2)[0];
|
|
||||||
$url = ($preUserinfo ? $preUserinfo . '//' : '') . $postUserinfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare pattern
|
|
||||||
$quoted = preg_quote($pattern, '/');
|
$quoted = preg_quote($pattern, '/');
|
||||||
$regexPattern = str_replace('\*', '[^\/]*', $quoted);
|
$regexPattern = str_replace('\*', '.*', $quoted);
|
||||||
|
|
||||||
// Check against our URL
|
return preg_match('/^' . $regexPattern . '($|\/.*$|#.*$)/i', $url);
|
||||||
return preg_match('/^' . $regexPattern . '($|\/.*$)/i', $url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
254
composer.lock
generated
254
composer.lock
generated
@@ -62,16 +62,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "aws/aws-sdk-php",
|
"name": "aws/aws-sdk-php",
|
||||||
"version": "3.379.8",
|
"version": "3.376.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||||
"reference": "856ddf3d241c29132fe1eb946e112351ab043542"
|
"reference": "2081f8db174df4bb8842aed3b7b513590ee9d219"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/856ddf3d241c29132fe1eb946e112351ab043542",
|
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2081f8db174df4bb8842aed3b7b513590ee9d219",
|
||||||
"reference": "856ddf3d241c29132fe1eb946e112351ab043542",
|
"reference": "2081f8db174df4bb8842aed3b7b513590ee9d219",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -153,9 +153,9 @@
|
|||||||
"support": {
|
"support": {
|
||||||
"forum": "https://github.com/aws/aws-sdk-php/discussions",
|
"forum": "https://github.com/aws/aws-sdk-php/discussions",
|
||||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.379.8"
|
"source": "https://github.com/aws/aws-sdk-php/tree/3.376.3"
|
||||||
},
|
},
|
||||||
"time": "2026-04-27T19:13:21+00:00"
|
"time": "2026-04-03T18:07:33+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@@ -985,12 +985,12 @@
|
|||||||
"version": "v7.0.5",
|
"version": "v7.0.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/googleapis/php-jwt.git",
|
"url": "https://github.com/firebase/php-jwt.git",
|
||||||
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380"
|
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380",
|
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380",
|
||||||
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380",
|
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
@@ -1039,8 +1039,8 @@
|
|||||||
"php"
|
"php"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/googleapis/php-jwt/issues",
|
"issues": "https://github.com/firebase/php-jwt/issues",
|
||||||
"source": "https://github.com/googleapis/php-jwt/tree/v7.0.5"
|
"source": "https://github.com/firebase/php-jwt/tree/v7.0.5"
|
||||||
},
|
},
|
||||||
"time": "2026-04-01T20:38:03+00:00"
|
"time": "2026-04-01T20:38:03+00:00"
|
||||||
},
|
},
|
||||||
@@ -1802,16 +1802,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/framework",
|
"name": "laravel/framework",
|
||||||
"version": "v12.58.0",
|
"version": "v12.56.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/framework.git",
|
"url": "https://github.com/laravel/framework.git",
|
||||||
"reference": "6172ae1f44ba5d89e111057ee4a4e7c27f5a610d"
|
"reference": "dac16d424b59debb2273910dde88eb7050a2a709"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/framework/zipball/6172ae1f44ba5d89e111057ee4a4e7c27f5a610d",
|
"url": "https://api.github.com/repos/laravel/framework/zipball/dac16d424b59debb2273910dde88eb7050a2a709",
|
||||||
"reference": "6172ae1f44ba5d89e111057ee4a4e7c27f5a610d",
|
"reference": "dac16d424b59debb2273910dde88eb7050a2a709",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1852,8 +1852,8 @@
|
|||||||
"symfony/mailer": "^7.2.0",
|
"symfony/mailer": "^7.2.0",
|
||||||
"symfony/mime": "^7.2.0",
|
"symfony/mime": "^7.2.0",
|
||||||
"symfony/polyfill-php83": "^1.33",
|
"symfony/polyfill-php83": "^1.33",
|
||||||
"symfony/polyfill-php84": "^1.34",
|
"symfony/polyfill-php84": "^1.33",
|
||||||
"symfony/polyfill-php85": "^1.34",
|
"symfony/polyfill-php85": "^1.33",
|
||||||
"symfony/process": "^7.2.0",
|
"symfony/process": "^7.2.0",
|
||||||
"symfony/routing": "^7.2.0",
|
"symfony/routing": "^7.2.0",
|
||||||
"symfony/uid": "^7.2.0",
|
"symfony/uid": "^7.2.0",
|
||||||
@@ -2020,20 +2020,20 @@
|
|||||||
"issues": "https://github.com/laravel/framework/issues",
|
"issues": "https://github.com/laravel/framework/issues",
|
||||||
"source": "https://github.com/laravel/framework"
|
"source": "https://github.com/laravel/framework"
|
||||||
},
|
},
|
||||||
"time": "2026-04-26T16:42:04+00:00"
|
"time": "2026-03-26T14:51:54+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/prompts",
|
"name": "laravel/prompts",
|
||||||
"version": "v0.3.17",
|
"version": "v0.3.16",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/prompts.git",
|
"url": "https://github.com/laravel/prompts.git",
|
||||||
"reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818"
|
"reference": "11e7d5f93803a2190b00e145142cb00a33d17ad2"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/prompts/zipball/6a82ac19a28b916ae0885828795dbd4c59d9a818",
|
"url": "https://api.github.com/repos/laravel/prompts/zipball/11e7d5f93803a2190b00e145142cb00a33d17ad2",
|
||||||
"reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818",
|
"reference": "11e7d5f93803a2190b00e145142cb00a33d17ad2",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2077,22 +2077,22 @@
|
|||||||
"description": "Add beautiful and user-friendly forms to your command-line applications.",
|
"description": "Add beautiful and user-friendly forms to your command-line applications.",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/laravel/prompts/issues",
|
"issues": "https://github.com/laravel/prompts/issues",
|
||||||
"source": "https://github.com/laravel/prompts/tree/v0.3.17"
|
"source": "https://github.com/laravel/prompts/tree/v0.3.16"
|
||||||
},
|
},
|
||||||
"time": "2026-04-20T16:07:33+00:00"
|
"time": "2026-03-23T14:35:33+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/serializable-closure",
|
"name": "laravel/serializable-closure",
|
||||||
"version": "v2.0.13",
|
"version": "v2.0.10",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/serializable-closure.git",
|
"url": "https://github.com/laravel/serializable-closure.git",
|
||||||
"reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce"
|
"reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce",
|
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669",
|
||||||
"reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce",
|
"reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2140,20 +2140,20 @@
|
|||||||
"issues": "https://github.com/laravel/serializable-closure/issues",
|
"issues": "https://github.com/laravel/serializable-closure/issues",
|
||||||
"source": "https://github.com/laravel/serializable-closure"
|
"source": "https://github.com/laravel/serializable-closure"
|
||||||
},
|
},
|
||||||
"time": "2026-04-16T14:03:50+00:00"
|
"time": "2026-02-20T19:59:49+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/socialite",
|
"name": "laravel/socialite",
|
||||||
"version": "v5.27.0",
|
"version": "v5.26.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/socialite.git",
|
"url": "https://github.com/laravel/socialite.git",
|
||||||
"reference": "40e0757a75637c7b2dff05d3286b0d8fc25e5c0e"
|
"reference": "db6ec2ee967b7f06412c3a0cf1daaf072f4752a4"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/socialite/zipball/40e0757a75637c7b2dff05d3286b0d8fc25e5c0e",
|
"url": "https://api.github.com/repos/laravel/socialite/zipball/db6ec2ee967b7f06412c3a0cf1daaf072f4752a4",
|
||||||
"reference": "40e0757a75637c7b2dff05d3286b0d8fc25e5c0e",
|
"reference": "db6ec2ee967b7f06412c3a0cf1daaf072f4752a4",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2212,7 +2212,7 @@
|
|||||||
"issues": "https://github.com/laravel/socialite/issues",
|
"issues": "https://github.com/laravel/socialite/issues",
|
||||||
"source": "https://github.com/laravel/socialite"
|
"source": "https://github.com/laravel/socialite"
|
||||||
},
|
},
|
||||||
"time": "2026-04-24T14:05:47+00:00"
|
"time": "2026-03-29T14:50:53+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/tinker",
|
"name": "laravel/tinker",
|
||||||
@@ -3362,16 +3362,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nesbot/carbon",
|
"name": "nesbot/carbon",
|
||||||
"version": "3.11.4",
|
"version": "3.11.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/CarbonPHP/carbon.git",
|
"url": "https://github.com/CarbonPHP/carbon.git",
|
||||||
"reference": "e890471a3494740f7d9326d72ce6a8c559ffee60"
|
"reference": "6a7e652845bb018c668220c2a545aded8594fbbf"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60",
|
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6a7e652845bb018c668220c2a545aded8594fbbf",
|
||||||
"reference": "e890471a3494740f7d9326d72ce6a8c559ffee60",
|
"reference": "6a7e652845bb018c668220c2a545aded8594fbbf",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -3463,7 +3463,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-07T09:57:54+00:00"
|
"time": "2026-03-11T17:23:39+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nette/schema",
|
"name": "nette/schema",
|
||||||
@@ -4028,16 +4028,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpseclib/phpseclib",
|
"name": "phpseclib/phpseclib",
|
||||||
"version": "3.0.52",
|
"version": "3.0.50",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/phpseclib/phpseclib.git",
|
"url": "https://github.com/phpseclib/phpseclib.git",
|
||||||
"reference": "2adaefc83df2ec548558307690f376dd7d4f4fce"
|
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce",
|
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
|
||||||
"reference": "2adaefc83df2ec548558307690f376dd7d4f4fce",
|
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -4118,7 +4118,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
||||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.52"
|
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.50"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -4134,7 +4134,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-27T07:02:15+00:00"
|
"time": "2026-03-19T02:57:58+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pragmarx/google2fa",
|
"name": "pragmarx/google2fa",
|
||||||
@@ -6499,16 +6499,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-ctype",
|
"name": "symfony/polyfill-ctype",
|
||||||
"version": "v1.37.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||||
"reference": "141046a8f9477948ff284fa65be2095baafb94f2"
|
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
|
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||||
"reference": "141046a8f9477948ff284fa65be2095baafb94f2",
|
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -6558,7 +6558,7 @@
|
|||||||
"portable"
|
"portable"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
|
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -6578,20 +6578,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-10T16:19:22+00:00"
|
"time": "2024-09-09T11:45:10+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-grapheme",
|
"name": "symfony/polyfill-intl-grapheme",
|
||||||
"version": "v1.37.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||||
"reference": "4864388bfbd3001ce88e234fab652acd91fdc57e"
|
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e",
|
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
|
||||||
"reference": "4864388bfbd3001ce88e234fab652acd91fdc57e",
|
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -6640,7 +6640,7 @@
|
|||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0"
|
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -6660,11 +6660,11 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-26T13:13:48+00:00"
|
"time": "2025-06-27T09:58:17+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-idn",
|
"name": "symfony/polyfill-intl-idn",
|
||||||
"version": "v1.37.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||||
@@ -6727,7 +6727,7 @@
|
|||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0"
|
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -6751,7 +6751,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-normalizer",
|
"name": "symfony/polyfill-intl-normalizer",
|
||||||
"version": "v1.37.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||||
@@ -6812,7 +6812,7 @@
|
|||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0"
|
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -6836,16 +6836,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-mbstring",
|
"name": "symfony/polyfill-mbstring",
|
||||||
"version": "v1.37.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||||
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
|
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
|
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
|
||||||
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
|
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -6897,7 +6897,7 @@
|
|||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0"
|
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -6917,20 +6917,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-10T17:25:58+00:00"
|
"time": "2024-12-23T08:48:59+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php80",
|
"name": "symfony/polyfill-php80",
|
||||||
"version": "v1.37.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||||
"reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
|
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
|
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
|
||||||
"reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
|
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -6981,7 +6981,7 @@
|
|||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
|
"source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -7001,20 +7001,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-10T16:19:22+00:00"
|
"time": "2025-01-02T08:10:11+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php83",
|
"name": "symfony/polyfill-php83",
|
||||||
"version": "v1.37.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php83.git",
|
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||||
"reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149"
|
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149",
|
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
|
||||||
"reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149",
|
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -7061,7 +7061,7 @@
|
|||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0"
|
"source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -7081,20 +7081,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-10T17:25:58+00:00"
|
"time": "2025-07-08T02:45:35+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php84",
|
"name": "symfony/polyfill-php84",
|
||||||
"version": "v1.37.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php84.git",
|
"url": "https://github.com/symfony/polyfill-php84.git",
|
||||||
"reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06"
|
"reference": "d8ced4d875142b6a7426000426b8abc631d6b191"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06",
|
"url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191",
|
||||||
"reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06",
|
"reference": "d8ced4d875142b6a7426000426b8abc631d6b191",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -7141,7 +7141,7 @@
|
|||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0"
|
"source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -7161,20 +7161,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-10T18:47:49+00:00"
|
"time": "2025-06-24T13:30:11+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php85",
|
"name": "symfony/polyfill-php85",
|
||||||
"version": "v1.37.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php85.git",
|
"url": "https://github.com/symfony/polyfill-php85.git",
|
||||||
"reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee"
|
"reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee",
|
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
|
||||||
"reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee",
|
"reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -7221,7 +7221,7 @@
|
|||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0"
|
"source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -7241,20 +7241,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-26T13:10:57+00:00"
|
"time": "2025-06-23T16:12:55+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-uuid",
|
"name": "symfony/polyfill-uuid",
|
||||||
"version": "v1.37.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-uuid.git",
|
"url": "https://github.com/symfony/polyfill-uuid.git",
|
||||||
"reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94"
|
"reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94",
|
"url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
|
||||||
"reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94",
|
"reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -7304,7 +7304,7 @@
|
|||||||
"uuid"
|
"uuid"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0"
|
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -7324,7 +7324,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-10T16:19:22+00:00"
|
"time": "2024-09-09T11:45:10+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/process",
|
"name": "symfony/process",
|
||||||
@@ -8285,23 +8285,23 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "voku/portable-ascii",
|
"name": "voku/portable-ascii",
|
||||||
"version": "2.1.1",
|
"version": "2.0.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/voku/portable-ascii.git",
|
"url": "https://github.com/voku/portable-ascii.git",
|
||||||
"reference": "8e1051fe39379367aecf014f41744ce7539a856f"
|
"reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f",
|
"url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d",
|
||||||
"reference": "8e1051fe39379367aecf014f41744ce7539a856f",
|
"reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.1.0"
|
"php": ">=7.0.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5"
|
"phpunit/phpunit": "~6.0 || ~7.0 || ~9.0"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-intl": "Use Intl for transliterator_transliterate() support"
|
"ext-intl": "Use Intl for transliterator_transliterate() support"
|
||||||
@@ -8331,7 +8331,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/voku/portable-ascii/issues",
|
"issues": "https://github.com/voku/portable-ascii/issues",
|
||||||
"source": "https://github.com/voku/portable-ascii/tree/2.1.1"
|
"source": "https://github.com/voku/portable-ascii/tree/2.0.3"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -8355,7 +8355,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-26T05:33:54+00:00"
|
"time": "2024-11-21T01:49:47+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "xemlock/htmlpurifier-html5",
|
"name": "xemlock/htmlpurifier-html5",
|
||||||
@@ -8723,16 +8723,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "larastan/larastan",
|
"name": "larastan/larastan",
|
||||||
"version": "v3.9.6",
|
"version": "v3.9.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/larastan/larastan.git",
|
"url": "https://github.com/larastan/larastan.git",
|
||||||
"reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636"
|
"reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/larastan/larastan/zipball/9ad17e83e96b63536cb6ac39c3d40d29ff9cf636",
|
"url": "https://api.github.com/repos/larastan/larastan/zipball/64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65",
|
||||||
"reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636",
|
"reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -8746,7 +8746,7 @@
|
|||||||
"illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13",
|
"illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13",
|
||||||
"illuminate/support": "^11.44.2 || ^12.4.1 || ^13",
|
"illuminate/support": "^11.44.2 || ^12.4.1 || ^13",
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"phpstan/phpstan": "^2.1.44"
|
"phpstan/phpstan": "^2.1.32"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"doctrine/coding-standard": "^13",
|
"doctrine/coding-standard": "^13",
|
||||||
@@ -8801,7 +8801,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/larastan/larastan/issues",
|
"issues": "https://github.com/larastan/larastan/issues",
|
||||||
"source": "https://github.com/larastan/larastan/tree/v3.9.6"
|
"source": "https://github.com/larastan/larastan/tree/v3.9.3"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -8809,7 +8809,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-16T10:02:43+00:00"
|
"time": "2026-02-20T12:07:12+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mockery/mockery",
|
"name": "mockery/mockery",
|
||||||
@@ -8956,23 +8956,23 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nunomaduro/collision",
|
"name": "nunomaduro/collision",
|
||||||
"version": "v8.9.4",
|
"version": "v8.9.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/nunomaduro/collision.git",
|
"url": "https://github.com/nunomaduro/collision.git",
|
||||||
"reference": "716af8f95a470e9094cfca09ed897b023be191a5"
|
"reference": "6eb16883e74fd725ac64dbe81544c961ab448ba5"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5",
|
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/6eb16883e74fd725ac64dbe81544c961ab448ba5",
|
||||||
"reference": "716af8f95a470e9094cfca09ed897b023be191a5",
|
"reference": "6eb16883e74fd725ac64dbe81544c961ab448ba5",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"filp/whoops": "^2.18.4",
|
"filp/whoops": "^2.18.4",
|
||||||
"nunomaduro/termwind": "^2.4.0",
|
"nunomaduro/termwind": "^2.4.0",
|
||||||
"php": "^8.2.0",
|
"php": "^8.2.0",
|
||||||
"symfony/console": "^7.4.8 || ^8.0.8"
|
"symfony/console": "^7.4.8 || ^8.0.4"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"laravel/framework": "<11.48.0 || >=14.0.0",
|
"laravel/framework": "<11.48.0 || >=14.0.0",
|
||||||
@@ -8980,12 +8980,12 @@
|
|||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"brianium/paratest": "^7.8.5",
|
"brianium/paratest": "^7.8.5",
|
||||||
"larastan/larastan": "^3.9.6",
|
"larastan/larastan": "^3.9.3",
|
||||||
"laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0",
|
"laravel/framework": "^11.48.0 || ^12.56.0 || ^13.2.0",
|
||||||
"laravel/pint": "^1.29.1",
|
"laravel/pint": "^1.29.0",
|
||||||
"orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1",
|
"orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.0.0",
|
||||||
"pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0",
|
"pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0",
|
||||||
"sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0"
|
"sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.0.0"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
@@ -9048,7 +9048,7 @@
|
|||||||
"type": "patreon"
|
"type": "patreon"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-21T14:04:20+00:00"
|
"time": "2026-03-31T21:51:27+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phar-io/manifest",
|
"name": "phar-io/manifest",
|
||||||
@@ -9170,11 +9170,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpstan",
|
"name": "phpstan/phpstan",
|
||||||
"version": "2.1.54",
|
"version": "2.1.46",
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
|
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||||
"reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
|
"reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -9219,7 +9219,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-29T13:31:09+00:00"
|
"time": "2026-04-01T09:25:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/php-code-coverage",
|
"name": "phpunit/php-code-coverage",
|
||||||
@@ -10983,5 +10983,5 @@
|
|||||||
"platform-overrides": {
|
"platform-overrides": {
|
||||||
"php": "8.2.0"
|
"php": "8.2.0"
|
||||||
},
|
},
|
||||||
"plugin-api-version": "2.9.0"
|
"plugin-api-version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user