mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-05-04 18:08:46 +03:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e2f59fa0f | ||
|
|
cc6e9e0546 | ||
|
|
0f59981932 | ||
|
|
a37f903dc7 | ||
|
|
74aa897626 | ||
|
|
4b624596c8 | ||
|
|
00239bb6c8 | ||
|
|
241563e8fc | ||
|
|
e91747785b | ||
|
|
4f370ccddb | ||
|
|
743a21a02f | ||
|
|
0c9fabb6de | ||
|
|
426f9ac493 | ||
|
|
ec0b0384a2 | ||
|
|
e7e019d3d4 | ||
|
|
1339f668eb | ||
|
|
befa3a8fbb | ||
|
|
083fb1a600 | ||
|
|
a2bb5bdf10 | ||
|
|
e274a5fa4e | ||
|
|
18364d1e6e | ||
|
|
0760e677b2 | ||
|
|
208629ee1f | ||
|
|
346dc27979 | ||
|
|
1c1ad1d1b7 | ||
|
|
f14fc68b66 | ||
|
|
93f84a81b2 | ||
|
|
4feb50e7ee | ||
|
|
c7e2b487c1 | ||
|
|
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
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- '**.php'
|
||||
@@ -11,14 +12,16 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker.io/library/node:24-trixie
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
uses: https://github.com/shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
php-version: 8.5
|
||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
@@ -27,14 +30,16 @@ jobs:
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache composer packages
|
||||
uses: actions/cache@v4
|
||||
uses: https://code.forgejo.org/actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-8.3
|
||||
key: ${{ runner.os }}-composer-8.5
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
|
||||
- name: Install composer dependencies
|
||||
run: composer install --prefer-dist --no-interaction --ansi
|
||||
env:
|
||||
COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GH_TOKEN }}"}}'
|
||||
|
||||
- name: Run static analysis check
|
||||
run: composer check-static
|
||||
@@ -1,6 +1,7 @@
|
||||
name: lint-js
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- '**.js'
|
||||
@@ -13,9 +14,11 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker.io/library/node:24-trixie
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||
|
||||
- name: Install NPM deps
|
||||
run: npm ci
|
||||
@@ -1,6 +1,7 @@
|
||||
name: lint-php
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- '**.php'
|
||||
@@ -11,14 +12,16 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker.io/library/node:24-trixie
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
uses: https://github.com/shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
php-version: 8.5
|
||||
tools: phpcs
|
||||
|
||||
- name: Run formatting check
|
||||
@@ -1,6 +1,7 @@
|
||||
name: test-js
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- '**.js'
|
||||
@@ -15,9 +16,11 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker.io/library/node:24-trixie
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||
|
||||
- name: Install NPM deps
|
||||
run: npm ci
|
||||
@@ -1,6 +1,7 @@
|
||||
name: test-migrations
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- '**.php'
|
||||
@@ -13,15 +14,25 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker.io/library/node:24-trixie
|
||||
strategy:
|
||||
matrix:
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
uses: https://github.com/shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap
|
||||
@@ -32,34 +43,31 @@ jobs:
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache composer packages
|
||||
uses: actions/cache@v4
|
||||
uses: https://code.forgejo.org/actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}
|
||||
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
|
||||
run: composer install --prefer-dist --no-interaction --ansi
|
||||
env:
|
||||
COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GH_TOKEN }}"}}'
|
||||
|
||||
- name: Start migration test
|
||||
env:
|
||||
TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test'
|
||||
run: |
|
||||
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
|
||||
|
||||
- name: Start migration:rollback test
|
||||
env:
|
||||
TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test'
|
||||
run: |
|
||||
php${{ matrix.php }} artisan migrate:rollback --force -n --database=mysql_testing
|
||||
|
||||
- name: Start migration rerun test
|
||||
env:
|
||||
TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test'
|
||||
run: |
|
||||
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
|
||||
@@ -1,6 +1,7 @@
|
||||
name: test-php
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- '**.php'
|
||||
@@ -13,15 +14,25 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker.io/library/node:24-trixie
|
||||
strategy:
|
||||
matrix:
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
uses: https://github.com/shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp
|
||||
@@ -32,30 +43,25 @@ jobs:
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache composer packages
|
||||
uses: actions/cache@v4
|
||||
uses: https://code.forgejo.org/actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}
|
||||
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
|
||||
run: composer install --prefer-dist --no-interaction --ansi
|
||||
env:
|
||||
COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GH_TOKEN }}"}}'
|
||||
|
||||
- name: Migrate and seed the database
|
||||
env:
|
||||
TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test'
|
||||
run: |
|
||||
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
|
||||
php${{ matrix.php }} artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
|
||||
|
||||
- name: Run PHP tests
|
||||
env:
|
||||
TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test'
|
||||
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
|
||||
|
||||
## 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
|
||||
Please find our community rules on our website here:
|
||||
https://www.bookstackapp.com/about/community-rules/
|
||||
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.
|
||||
validations:
|
||||
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.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
/node_modules
|
||||
/.vscode
|
||||
/composer
|
||||
/composer.phar
|
||||
/coverage
|
||||
Homestead.yaml
|
||||
.env
|
||||
|
||||
@@ -45,11 +45,11 @@ class ForgotPasswordController extends Controller
|
||||
);
|
||||
|
||||
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])) {
|
||||
$message = trans('auth.reset_password_sent', ['email' => $request->get('email')]);
|
||||
$message = trans('auth.reset_password_sent', ['email' => $request->input('email')]);
|
||||
$this->showSuccessNotification($message);
|
||||
|
||||
return redirect('/password/email')->with('status', trans($response));
|
||||
|
||||
@@ -32,12 +32,12 @@ class LoginController extends Controller
|
||||
{
|
||||
$socialDrivers = $this->socialDriverManager->getActive();
|
||||
$authMethod = config('auth.method');
|
||||
$preventInitiation = $request->get('prevent_auto_init') === 'true';
|
||||
$preventInitiation = $request->input('prevent_auto_init') === 'true';
|
||||
|
||||
if ($request->has('email')) {
|
||||
session()->flashInput([
|
||||
'email' => $request->get('email'),
|
||||
'password' => (config('app.env') === 'demo') ? $request->get('password', '') : '',
|
||||
'email' => $request->input('email'),
|
||||
'password' => (config('app.env') === 'demo') ? $request->input('password', '') : '',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class LoginController extends Controller
|
||||
public function login(Request $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
|
||||
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);
|
||||
|
||||
$mfaSession->markVerifiedForUser($user);
|
||||
|
||||
@@ -51,14 +51,14 @@ class MfaController extends Controller
|
||||
*/
|
||||
public function verify(Request $request)
|
||||
{
|
||||
$desiredMethod = $request->get('method');
|
||||
$desiredMethod = $request->input('method');
|
||||
$userMethods = $this->currentOrLastAttemptedUser()
|
||||
->mfaValues()
|
||||
->get(['id', 'method'])
|
||||
->groupBy('method');
|
||||
|
||||
// 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();
|
||||
$otherMethods = $userMethods->keys()->filter(function ($userMethod) use ($method) {
|
||||
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
|
||||
// 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');
|
||||
$response = Password::broker()->reset($credentials, function (User $user, string $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.
|
||||
return $response === Password::PASSWORD_RESET
|
||||
? $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)
|
||||
{
|
||||
$samlResponse = $request->get('SAMLResponse', null);
|
||||
$samlResponse = $request->input('SAMLResponse', null);
|
||||
|
||||
if (empty($samlResponse)) {
|
||||
$this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')]));
|
||||
@@ -100,7 +100,7 @@ class Saml2Controller extends Controller
|
||||
*/
|
||||
public function processAcs(Request $request)
|
||||
{
|
||||
$acsId = $request->get('id', null);
|
||||
$acsId = $request->input('id', null);
|
||||
$cacheKey = 'saml2_acs:' . $acsId;
|
||||
$samlResponse = null;
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ class SocialController extends Controller
|
||||
if ($request->has('error') && $request->has('error_description')) {
|
||||
throw new SocialSignInException(trans('errors.social_login_bad_response', [
|
||||
'socialAccount' => $socialDriver,
|
||||
'error' => $request->get('error_description'),
|
||||
'error' => $request->input('error_description'),
|
||||
]), '/login');
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ class UserInviteController extends Controller
|
||||
}
|
||||
|
||||
$user = $this->userRepo->getById($userId);
|
||||
$user->password = Hash::make($request->get('password'));
|
||||
$user->password = Hash::make($request->input('password'));
|
||||
$user->email_confirmed = true;
|
||||
$user->save();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace BookStack\Access;
|
||||
use BookStack\Access\Notifications\ConfirmEmailNotification;
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
use BookStack\Users\Models\User;
|
||||
use Exception;
|
||||
|
||||
class EmailConfirmationService extends UserTokenService
|
||||
{
|
||||
@@ -16,6 +17,7 @@ class EmailConfirmationService extends UserTokenService
|
||||
* Also removes any existing old ones.
|
||||
*
|
||||
* @throws ConfirmationEmailException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function sendConfirmation(User $user): void
|
||||
{
|
||||
|
||||
@@ -71,7 +71,7 @@ class LoginService
|
||||
}
|
||||
|
||||
$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
|
||||
{
|
||||
/** @var MfaValue $mfaVal */
|
||||
$mfaVal = static::query()
|
||||
->where('user_id', '=', $user->id)
|
||||
->where('method', '=', $method)
|
||||
->first();
|
||||
|
||||
return $mfaVal ? $mfaVal->getValue() : null;
|
||||
return $mfaVal?->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,10 +9,7 @@ use phpseclib3\Math\BigInteger;
|
||||
|
||||
class OidcJwtSigningKey
|
||||
{
|
||||
/**
|
||||
* @var PublicKey
|
||||
*/
|
||||
protected $key;
|
||||
protected PublicKey $key;
|
||||
|
||||
/**
|
||||
* 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'
|
||||
* ['kty' => 'RSA', 'alg' => 'RS256', 'n' => 'abc123...'].
|
||||
*
|
||||
* @param array|string $jwkOrKeyPath
|
||||
*
|
||||
* @throws OidcInvalidKeyException
|
||||
*/
|
||||
public function __construct($jwkOrKeyPath)
|
||||
public function __construct(array|string $jwkOrKeyPath)
|
||||
{
|
||||
if (is_array($jwkOrKeyPath)) {
|
||||
$this->loadFromJwkArray($jwkOrKeyPath);
|
||||
} elseif (is_string($jwkOrKeyPath) && strpos($jwkOrKeyPath, 'file://') === 0) {
|
||||
} elseif (str_starts_with($jwkOrKeyPath, 'file://')) {
|
||||
$this->loadFromPath($jwkOrKeyPath);
|
||||
} else {
|
||||
throw new OidcInvalidKeyException('Unexpected type of key value provided');
|
||||
@@ -38,7 +33,7 @@ class OidcJwtSigningKey
|
||||
/**
|
||||
* @throws OidcInvalidKeyException
|
||||
*/
|
||||
protected function loadFromPath(string $path)
|
||||
protected function loadFromPath(string $path): void
|
||||
{
|
||||
try {
|
||||
$key = PublicKeyLoader::load(
|
||||
@@ -58,7 +53,7 @@ class OidcJwtSigningKey
|
||||
/**
|
||||
* @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
|
||||
// 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');
|
||||
}
|
||||
|
||||
$n = strtr($jwk['n'] ?? '', '-_', '+/');
|
||||
$n = strtr($jwk['n'], '-_', '+/');
|
||||
|
||||
try {
|
||||
$key = PublicKeyLoader::load([
|
||||
|
||||
@@ -102,12 +102,12 @@ class OidcJwtWithClaims implements ProvidesClaims
|
||||
protected function validateTokenStructure(): void
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class OidcUserDetails
|
||||
): void {
|
||||
$this->externalId = $claims->getClaim($idClaim) ?? $this->externalId;
|
||||
$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->picture = static::getPicture($claims) ?: $this->picture;
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ class Saml2Service
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
@@ -357,7 +357,7 @@ class Saml2Service
|
||||
]);
|
||||
}
|
||||
|
||||
if ($userDetails['email'] === null) {
|
||||
if (empty($userDetails['email'])) {
|
||||
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.
|
||||
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]));
|
||||
|
||||
return redirect('/my-account/auth#social_accounts');
|
||||
}
|
||||
|
||||
// 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]));
|
||||
|
||||
return redirect('/my-account/auth#social_accounts');
|
||||
|
||||
@@ -17,19 +17,19 @@ class AuditLogController extends Controller
|
||||
$this->checkPermission(Permission::SettingsManage);
|
||||
$this->checkPermission(Permission::UsersManage);
|
||||
|
||||
$sort = $request->get('sort', 'activity_date');
|
||||
$order = $request->get('order', 'desc');
|
||||
$sort = $request->input('sort', 'activity_date');
|
||||
$order = $request->input('order', 'desc');
|
||||
$listOptions = (new SimpleListOptions('', $sort, $order))->withSortOptions([
|
||||
'created_at' => trans('settings.audit_table_date'),
|
||||
'type' => trans('settings.audit_table_event'),
|
||||
]);
|
||||
|
||||
$filters = [
|
||||
'event' => $request->get('event', ''),
|
||||
'date_from' => $request->get('date_from', ''),
|
||||
'date_to' => $request->get('date_to', ''),
|
||||
'user' => $request->get('user', ''),
|
||||
'ip' => $request->get('ip', ''),
|
||||
'event' => $request->input('event', ''),
|
||||
'date_from' => $request->input('date_from', ''),
|
||||
'date_to' => $request->input('date_to', ''),
|
||||
'user' => $request->input('user', ''),
|
||||
'ip' => $request->input('ip', ''),
|
||||
];
|
||||
|
||||
$query = Activity::query()
|
||||
|
||||
@@ -20,7 +20,7 @@ class FavouriteController extends Controller
|
||||
public function index(Request $request, QueryTopFavourites $topFavourites)
|
||||
{
|
||||
$viewCount = 20;
|
||||
$page = intval($request->get('page', 1));
|
||||
$page = intval($request->input('page', 1));
|
||||
$favourites = $topFavourites->run($viewCount + 1, (($page - 1) * $viewCount));
|
||||
|
||||
$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'),
|
||||
]);
|
||||
|
||||
$nameFilter = $request->get('name', '');
|
||||
$nameFilter = $request->input('name', '');
|
||||
$tags = $this->tagRepo
|
||||
->queryWithTotals($listOptions, $nameFilter)
|
||||
->queryWithTotalsForList($listOptions, $nameFilter)
|
||||
->paginate(50)
|
||||
->appends(array_filter(array_merge($listOptions->getPaginationAppends(), [
|
||||
'name' => $nameFilter,
|
||||
@@ -46,7 +46,7 @@ class TagController extends Controller
|
||||
*/
|
||||
public function getNameSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->get('search', '');
|
||||
$searchTerm = $request->input('search', '');
|
||||
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
|
||||
|
||||
return response()->json($suggestions);
|
||||
@@ -57,8 +57,8 @@ class TagController extends Controller
|
||||
*/
|
||||
public function getValueSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->get('search', '');
|
||||
$tagName = $request->get('name', '');
|
||||
$searchTerm = $request->input('search', '');
|
||||
$tagName = $request->input('name', '');
|
||||
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
|
||||
|
||||
return response()->json($suggestions);
|
||||
|
||||
@@ -9,6 +9,7 @@ use BookStack\Users\Models\HasCreatorAndUpdater;
|
||||
use BookStack\Users\Models\OwnableInterface;
|
||||
use BookStack\Util\HtmlContentFilter;
|
||||
use BookStack\Util\HtmlContentFilterConfig;
|
||||
use BookStack\Util\HtmlToPlainText;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -87,6 +88,12 @@ class Comment extends Model implements Loggable, OwnableInterface
|
||||
return $filter->filterString($this->html ?? '');
|
||||
}
|
||||
|
||||
public function getPlainText(): string
|
||||
{
|
||||
$converter = new HtmlToPlainText();
|
||||
return $converter->convert($this->html ?? '');
|
||||
}
|
||||
|
||||
public function jointPermissions(): HasMany
|
||||
{
|
||||
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_path') => $this->buildPagePathLine($page, $notifiable),
|
||||
$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)
|
||||
|
||||
@@ -24,7 +24,7 @@ class CommentMentionNotification extends BaseActivityNotification
|
||||
$locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),
|
||||
$locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),
|
||||
$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)
|
||||
|
||||
@@ -15,14 +15,14 @@ use BookStack\Users\Models\User;
|
||||
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
|
||||
{
|
||||
$activityType = $activity->type;
|
||||
$handlersToRun = $this->handlers[$activityType] ?? [];
|
||||
$handlersToRun = $this->handlersByActivity[$activityType] ?? [];
|
||||
foreach ($handlersToRun as $handlerClass) {
|
||||
/** @var NotificationHandler $handler */
|
||||
$handler = new $handlerClass();
|
||||
@@ -35,12 +35,12 @@ class NotificationManager
|
||||
*/
|
||||
public function registerHandler(string $activityType, string $handlerClass): void
|
||||
{
|
||||
if (!isset($this->handlers[$activityType])) {
|
||||
$this->handlers[$activityType] = [];
|
||||
if (!isset($this->handlersByActivity[$activityType])) {
|
||||
$this->handlersByActivity[$activityType] = [];
|
||||
}
|
||||
|
||||
if (!in_array($handlerClass, $this->handlers[$activityType])) {
|
||||
$this->handlers[$activityType][] = $handlerClass;
|
||||
if (!in_array($handlerClass, $this->handlersByActivity[$activityType])) {
|
||||
$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();
|
||||
$sort = $listOptions->getSort();
|
||||
@@ -28,17 +29,34 @@ class TagRepo
|
||||
$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()
|
||||
->select([
|
||||
'name',
|
||||
($searchTerm || $nameFilter) ? 'value' : DB::raw('COUNT(distinct value) as `values`'),
|
||||
DB::raw('COUNT(id) as usages'),
|
||||
DB::raw('SUM(IF(entity_type = \'page\', 1, 0)) as page_count'),
|
||||
DB::raw('SUM(IF(entity_type = \'chapter\', 1, 0)) as chapter_count'),
|
||||
DB::raw('SUM(IF(entity_type = \'book\', 1, 0)) as book_count'),
|
||||
DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'),
|
||||
DB::raw('CAST(SUM(IF(entity_type = \'page\', 1, 0)) as UNSIGNED) as page_count'),
|
||||
DB::raw('CAST(SUM(IF(entity_type = \'chapter\', 1, 0)) as UNSIGNED) as chapter_count'),
|
||||
DB::raw('CAST(SUM(IF(entity_type = \'book\', 1, 0)) as UNSIGNED) as book_count'),
|
||||
DB::raw('CAST(SUM(IF(entity_type = \'bookshelf\', 1, 0)) as UNSIGNED) as shelf_count'),
|
||||
])
|
||||
->orderBy($sort, $listOptions->getOrder())
|
||||
->whereHas('entity');
|
||||
|
||||
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
|
||||
{
|
||||
/**
|
||||
* @var array<string, ReflectionClass>
|
||||
*/
|
||||
protected array $reflectionClasses = [];
|
||||
|
||||
/**
|
||||
* @var array<string, ApiController>
|
||||
*/
|
||||
protected array $controllerClasses = [];
|
||||
|
||||
/**
|
||||
@@ -107,7 +114,6 @@ class ApiDocsGenerator
|
||||
*/
|
||||
protected function getBodyParamsFromClass(string $className, string $methodName): ?array
|
||||
{
|
||||
/** @var ApiController $class */
|
||||
$class = $this->controllerClasses[$className] ?? null;
|
||||
if ($class === null) {
|
||||
$class = app()->make($className);
|
||||
@@ -153,7 +159,7 @@ class ApiDocsGenerator
|
||||
$matches = [];
|
||||
preg_match_all('/^\s*?\*\s?($|((?![\/@\s]).*?))$/m', $comment, $matches);
|
||||
|
||||
$text = implode(' ', $matches[1] ?? []);
|
||||
$text = implode(' ', $matches[1]);
|
||||
return str_replace(' ', "\n", $text);
|
||||
}
|
||||
|
||||
@@ -189,11 +195,12 @@ class ApiDocsGenerator
|
||||
protected function getFlatApiRoutes(): Collection
|
||||
{
|
||||
return collect(Route::getRoutes()->getRoutes())->filter(function ($route) {
|
||||
return strpos($route->uri, 'api/') === 0;
|
||||
return str_starts_with($route->uri, 'api/');
|
||||
})->map(function ($route) {
|
||||
[$controller, $controllerMethod] = explode('@', $route->action['uses']);
|
||||
$baseModelName = explode('.', explode('/', $route->uri)[1])[0];
|
||||
$shortName = $baseModelName . '-' . $controllerMethod;
|
||||
$controllerMethodKebab = Str::kebab($controllerMethod);
|
||||
$shortName = $baseModelName . '-' . $controllerMethodKebab;
|
||||
|
||||
return [
|
||||
'name' => $shortName,
|
||||
@@ -201,7 +208,7 @@ class ApiDocsGenerator
|
||||
'method' => $route->methods[0],
|
||||
'controller' => $controller,
|
||||
'controller_method' => $controllerMethod,
|
||||
'controller_method_kebab' => Str::kebab($controllerMethod),
|
||||
'controller_method_kebab' => $controllerMethodKebab,
|
||||
'base_model' => $baseModelName,
|
||||
];
|
||||
});
|
||||
|
||||
@@ -74,18 +74,21 @@ class ApiEntityListFormatter
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$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 null;
|
||||
});
|
||||
|
||||
$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 null;
|
||||
|
||||
@@ -16,30 +16,15 @@ class ApiTokenGuard implements Guard
|
||||
{
|
||||
use GuardHelpers;
|
||||
|
||||
/**
|
||||
* The request instance.
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* @var LoginService
|
||||
*/
|
||||
protected $loginService;
|
||||
|
||||
/**
|
||||
* The last auth exception thrown in this request.
|
||||
*
|
||||
* @var ApiAuthException
|
||||
*/
|
||||
protected $lastAuthException;
|
||||
protected ApiAuthException|null $lastAuthException = null;
|
||||
|
||||
/**
|
||||
* ApiTokenGuard constructor.
|
||||
*/
|
||||
public function __construct(Request $request, LoginService $loginService)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->loginService = $loginService;
|
||||
public function __construct(
|
||||
protected Request $request,
|
||||
protected 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
|
||||
*
|
||||
@@ -121,7 +106,7 @@ class ApiTokenGuard implements Guard
|
||||
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'));
|
||||
}
|
||||
}
|
||||
@@ -155,7 +140,7 @@ class ApiTokenGuard implements Guard
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate(array $credentials = [])
|
||||
public function validate(array $credentials = []): bool
|
||||
{
|
||||
if (empty($credentials['id']) || empty($credentials['secret'])) {
|
||||
return false;
|
||||
@@ -175,7 +160,7 @@ class ApiTokenGuard implements Guard
|
||||
/**
|
||||
* "Log out" the currently authenticated user.
|
||||
*/
|
||||
public function logout()
|
||||
public function logout(): void
|
||||
{
|
||||
$this->user = null;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,13 @@ class ListingResponseBuilder
|
||||
*/
|
||||
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>
|
||||
*/
|
||||
@@ -54,7 +61,7 @@ class ListingResponseBuilder
|
||||
{
|
||||
$filteredQuery = $this->filterQuery($this->query);
|
||||
|
||||
$total = $filteredQuery->count();
|
||||
$total = $filteredQuery->getCountForPagination();
|
||||
$data = $this->fetchData($filteredQuery)->each(function ($model) {
|
||||
foreach ($this->resultModifiers as $modifier) {
|
||||
$modifier($model);
|
||||
@@ -77,6 +84,14 @@ class ListingResponseBuilder
|
||||
$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.
|
||||
*/
|
||||
@@ -94,7 +109,7 @@ class ListingResponseBuilder
|
||||
protected function filterQuery(Builder $query): Builder
|
||||
{
|
||||
$query = clone $query;
|
||||
$requestFilters = $this->request->get('filter', []);
|
||||
$requestFilters = $this->request->input('filter', []);
|
||||
if (!is_array($requestFilters)) {
|
||||
return $query;
|
||||
}
|
||||
@@ -114,10 +129,11 @@ class ListingResponseBuilder
|
||||
protected function requestFilterToQueryFilter($fieldKey, $value): ?array
|
||||
{
|
||||
$splitKey = explode(':', $fieldKey);
|
||||
$field = $splitKey[0];
|
||||
$field = strtolower($splitKey[0]);
|
||||
$filterOperator = $splitKey[1] ?? 'eq';
|
||||
|
||||
if (!in_array($field, $this->fields)) {
|
||||
$filterFields = $this->filterableFields ?? $this->fields;
|
||||
if (!in_array($field, $filterFields)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -140,8 +156,8 @@ class ListingResponseBuilder
|
||||
$defaultSortName = $this->fields[0];
|
||||
$direction = 'asc';
|
||||
|
||||
$sort = $this->request->get('sort', '');
|
||||
if (strpos($sort, '-') === 0) {
|
||||
$sort = $this->request->input('sort', '');
|
||||
if (str_starts_with($sort, '-')) {
|
||||
$direction = 'desc';
|
||||
}
|
||||
|
||||
@@ -160,9 +176,9 @@ class ListingResponseBuilder
|
||||
protected function countAndOffsetQuery(Builder $query): Builder
|
||||
{
|
||||
$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');
|
||||
$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);
|
||||
|
||||
return $query->skip($offset)->take($count);
|
||||
|
||||
@@ -48,11 +48,11 @@ class UserApiTokenController extends Controller
|
||||
$secret = Str::random(32);
|
||||
|
||||
$token = (new ApiToken())->forceFill([
|
||||
'name' => $request->get('name'),
|
||||
'name' => $request->input('name'),
|
||||
'token_id' => Str::random(32),
|
||||
'secret' => Hash::make($secret),
|
||||
'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()) {
|
||||
@@ -100,8 +100,8 @@ class UserApiTokenController extends Controller
|
||||
|
||||
[$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
|
||||
$token->fill([
|
||||
'name' => $request->get('name'),
|
||||
'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(),
|
||||
'name' => $request->input('name'),
|
||||
'expires_at' => $request->input('expires_at') ?: ApiToken::defaultExpiry(),
|
||||
])->save();
|
||||
|
||||
$this->logActivity(ActivityType::API_TOKEN_UPDATE, $token);
|
||||
|
||||
@@ -68,7 +68,7 @@ return [
|
||||
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
|
||||
* 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.
|
||||
@@ -78,7 +78,7 @@ return [
|
||||
*
|
||||
* 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.
|
||||
|
||||
@@ -32,7 +32,7 @@ class AssignSortRuleCommand extends Command
|
||||
*/
|
||||
public function handle(BookSorter $sorter): int
|
||||
{
|
||||
$sortRuleId = intval($this->argument('sort-rule')) ?? 0;
|
||||
$sortRuleId = intval($this->argument('sort-rule'));
|
||||
if ($sortRuleId === 0) {
|
||||
return $this->listSortRules();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ class CopyShelfPermissionsCommand extends Command
|
||||
{
|
||||
$shelfSlug = $this->option('slug');
|
||||
$cascadeAll = $this->option('all');
|
||||
$noInteraction = boolval($this->option('no-interaction'));
|
||||
$shelves = null;
|
||||
|
||||
if (!$cascadeAll && !$shelfSlug) {
|
||||
@@ -41,14 +42,16 @@ class CopyShelfPermissionsCommand extends Command
|
||||
}
|
||||
|
||||
if ($cascadeAll) {
|
||||
$continue = $this->confirm(
|
||||
'Permission settings for all shelves will be cascaded. ' .
|
||||
'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. ' .
|
||||
'Are you sure you want to proceed?'
|
||||
);
|
||||
if (!$noInteraction) {
|
||||
$continue = $this->confirm(
|
||||
'Permission settings for all shelves will be cascaded. ' .
|
||||
'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. ' .
|
||||
'Are you sure you want to proceed?',
|
||||
);
|
||||
|
||||
if (!$continue && !$this->hasOption('no-interaction')) {
|
||||
return 0;
|
||||
if (!$continue) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
$shelves = $queries->start()->get(['id']);
|
||||
|
||||
@@ -213,15 +213,23 @@ class InstallModuleCommand extends Command
|
||||
$redirectLocation = $resp->getHeaderLine('Location');
|
||||
if ($redirectLocation) {
|
||||
$redirectUrl = parse_url($redirectLocation);
|
||||
if (
|
||||
($originalUrl['host'] ?? '') === ($redirectUrl['host'] ?? '')
|
||||
$redirectOriginMatches = ($originalUrl['host'] ?? '') === ($redirectUrl['host'] ?? '')
|
||||
&& ($originalUrl['scheme'] ?? '') === ($redirectUrl['scheme'] ?? '')
|
||||
&& ($originalUrl['port'] ?? '') === ($redirectUrl['port'] ?? '')
|
||||
) {
|
||||
$currentLocation = $redirectLocation;
|
||||
$redirectCount++;
|
||||
continue;
|
||||
&& ($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;
|
||||
$redirectCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ class BookController extends Controller
|
||||
|
||||
View::incrementFor($book);
|
||||
if ($request->has('shelf')) {
|
||||
$this->shelfContext->setShelfContext(intval($request->get('shelf')));
|
||||
$this->shelfContext->setShelfContext(intval($request->input('shelf')));
|
||||
}
|
||||
|
||||
$this->setPageTitle($book->getShortName());
|
||||
@@ -263,7 +263,7 @@ class BookController extends Controller
|
||||
$this->checkOwnablePermission(Permission::BookView, $book);
|
||||
$this->checkPermission(Permission::BookCreateAll);
|
||||
|
||||
$newName = $request->get('name') ?: $book->name;
|
||||
$newName = $request->input('name') ?: $book->name;
|
||||
$bookCopy = $cloner->cloneBook($book, $newName);
|
||||
$this->showSuccessNotification(trans('entities.books_copy_success'));
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class BookshelfApiController extends ApiController
|
||||
$this->checkPermission(Permission::BookshelfCreateAll);
|
||||
$requestData = $this->validate($request, $this->rules()['create']);
|
||||
|
||||
$bookIds = $request->get('books', []);
|
||||
$bookIds = $request->input('books', []);
|
||||
$shelf = $this->bookshelfRepo->create($requestData, $bookIds);
|
||||
|
||||
return response()->json($this->forJsonDisplay($shelf));
|
||||
@@ -88,7 +88,7 @@ class BookshelfApiController extends ApiController
|
||||
$this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);
|
||||
|
||||
$requestData = $this->validate($request, $this->rules()['update']);
|
||||
$bookIds = $request->get('books', null);
|
||||
$bookIds = $request->input('books', null);
|
||||
|
||||
$shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ class BookshelfController extends Controller
|
||||
'tags' => ['array'],
|
||||
]);
|
||||
|
||||
$bookIds = explode(',', $request->get('books', ''));
|
||||
$bookIds = explode(',', $request->input('books', ''));
|
||||
$shelf = $this->shelfRepo->create($validated, $bookIds);
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
@@ -196,7 +196,7 @@ class BookshelfController extends Controller
|
||||
unset($validated['image']);
|
||||
}
|
||||
|
||||
$bookIds = explode(',', $request->get('books', ''));
|
||||
$bookIds = explode(',', $request->input('books', ''));
|
||||
$shelf = $this->shelfRepo->update($shelf, $validated, $bookIds);
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
|
||||
@@ -64,7 +64,7 @@ class ChapterApiController extends ApiController
|
||||
{
|
||||
$requestData = $this->validate($request, $this->rules['create']);
|
||||
|
||||
$bookId = $request->get('book_id');
|
||||
$bookId = $request->input('book_id');
|
||||
$book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId));
|
||||
$this->checkOwnablePermission(Permission::ChapterCreate, $book);
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ class ChapterController extends Controller
|
||||
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
|
||||
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
|
||||
|
||||
$entitySelection = $request->get('entity_selection', null);
|
||||
$entitySelection = $request->input('entity_selection', null);
|
||||
if ($entitySelection === null || $entitySelection === '') {
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
@@ -248,7 +248,7 @@ class ChapterController extends Controller
|
||||
{
|
||||
$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();
|
||||
|
||||
if (!$newParentBook instanceof Book) {
|
||||
@@ -259,7 +259,7 @@ class ChapterController extends Controller
|
||||
|
||||
$this->checkOwnablePermission(Permission::ChapterCreate, $newParentBook);
|
||||
|
||||
$newName = $request->get('name') ?: $chapter->name;
|
||||
$newName = $request->input('name') ?: $chapter->name;
|
||||
$chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName);
|
||||
$this->showSuccessNotification(trans('entities.chapters_copy_success'));
|
||||
|
||||
|
||||
@@ -74,9 +74,9 @@ class PageApiController extends ApiController
|
||||
$this->validate($request, $this->rules['create']);
|
||||
|
||||
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 {
|
||||
$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);
|
||||
|
||||
@@ -133,9 +133,9 @@ class PageApiController extends ApiController
|
||||
|
||||
$parent = null;
|
||||
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')) {
|
||||
$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())) {
|
||||
|
||||
@@ -88,7 +88,7 @@ class PageController extends Controller
|
||||
|
||||
$page = $this->pageRepo->getNewDraftPage($parent);
|
||||
$this->pageRepo->publishDraft($page, [
|
||||
'name' => $request->get('name'),
|
||||
'name' => $request->input('name'),
|
||||
]);
|
||||
|
||||
return redirect($page->getUrl('/edit'));
|
||||
@@ -408,7 +408,7 @@ class PageController extends Controller
|
||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
||||
|
||||
$entitySelection = $request->get('entity_selection', null);
|
||||
$entitySelection = $request->input('entity_selection', null);
|
||||
if ($entitySelection === null || $entitySelection === '') {
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
@@ -453,7 +453,7 @@ class PageController extends Controller
|
||||
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||
$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();
|
||||
|
||||
if (!$newParent instanceof Book && !$newParent instanceof Chapter) {
|
||||
@@ -464,7 +464,7 @@ class PageController extends Controller
|
||||
|
||||
$this->checkOwnablePermission(Permission::PageCreate, $newParent);
|
||||
|
||||
$newName = $request->get('name') ?: $page->name;
|
||||
$newName = $request->input('name') ?: $page->name;
|
||||
$pageCopy = $cloner->clonePage($page, $newParent, $newName);
|
||||
$this->showSuccessNotification(trans('entities.pages_copy_success'));
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class PageRevisionController extends Controller
|
||||
*/
|
||||
public function index(Request $request, string $bookSlug, string $pageSlug)
|
||||
{
|
||||
$this->checkPermission(Permission::RevisionViewAll);
|
||||
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||
$listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([
|
||||
'id' => trans('entities.pages_revisions_sort_number')
|
||||
@@ -65,6 +66,8 @@ class PageRevisionController extends Controller
|
||||
*/
|
||||
public function show(string $bookSlug, string $pageSlug, int $revisionId)
|
||||
{
|
||||
$this->checkPermission(Permission::RevisionViewAll);
|
||||
|
||||
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||
/** @var ?PageRevision $revision */
|
||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||
@@ -94,6 +97,8 @@ class PageRevisionController extends Controller
|
||||
*/
|
||||
public function changes(string $bookSlug, string $pageSlug, int $revisionId)
|
||||
{
|
||||
$this->checkPermission(Permission::RevisionViewAll);
|
||||
|
||||
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||
/** @var ?PageRevision $revision */
|
||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||
@@ -129,6 +134,7 @@ class PageRevisionController extends Controller
|
||||
*/
|
||||
public function restore(string $bookSlug, string $pageSlug, int $revisionId)
|
||||
{
|
||||
$this->checkPermission(Permission::RevisionViewAll);
|
||||
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||
|
||||
@@ -144,6 +150,7 @@ class PageRevisionController extends Controller
|
||||
*/
|
||||
public function destroy(string $bookSlug, string $pageSlug, int $revId)
|
||||
{
|
||||
$this->checkPermission(Permission::RevisionViewAll);
|
||||
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ class PageTemplateController extends Controller
|
||||
*/
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = $request->get('page', 1);
|
||||
$search = $request->get('search', '');
|
||||
$page = $request->input('page', 1);
|
||||
$search = $request->input('search', '');
|
||||
$count = 10;
|
||||
|
||||
$query = $this->pageQueries->visibleTemplates()
|
||||
|
||||
@@ -17,7 +17,7 @@ use Illuminate\Support\Collection;
|
||||
*
|
||||
* @property string $description
|
||||
* @property string $description_html
|
||||
* @property int $image_id
|
||||
* @property ?int $image_id
|
||||
* @property ?int $default_template_id
|
||||
* @property ?int $sort_rule_id
|
||||
* @property \Illuminate\Database\Eloquent\Collection $chapters
|
||||
|
||||
@@ -479,6 +479,7 @@ abstract class Entity extends Model implements
|
||||
'chapter' => new Chapter(),
|
||||
'book' => new Book(),
|
||||
'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 int $revision_count
|
||||
* @property string $editor
|
||||
* @property Chapter $chapter
|
||||
* @property Chapter|null $chapter
|
||||
* @property Collection $attachments
|
||||
* @property Collection $revisions
|
||||
* @property PageRevision $currentRevision
|
||||
|
||||
@@ -16,6 +16,7 @@ use BookStack\References\ReferenceUpdater;
|
||||
use BookStack\Sorting\BookSorter;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use BookStack\Util\HtmlDescriptionFilter;
|
||||
use BookStack\Util\HtmlToPlainText;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class BaseRepo
|
||||
@@ -151,9 +152,10 @@ class BaseRepo
|
||||
}
|
||||
|
||||
if (isset($input['description_html'])) {
|
||||
$plainTextConverter = new HtmlToPlainText();
|
||||
$entity->descriptionInfo()->set(
|
||||
HtmlDescriptionFilter::filterFromString($input['description_html']),
|
||||
html_entity_decode(strip_tags($input['description_html']))
|
||||
$plainTextConverter->convert($input['description_html']),
|
||||
);
|
||||
} else if (isset($input['description'])) {
|
||||
$entity->descriptionInfo()->set('', $input['description']);
|
||||
|
||||
@@ -60,7 +60,7 @@ class PageRepo
|
||||
$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) {
|
||||
$page->forceFill([
|
||||
'html' => $defaultTemplate->html,
|
||||
|
||||
@@ -16,6 +16,7 @@ use BookStack\Users\Models\User;
|
||||
use BookStack\Util\HtmlContentFilter;
|
||||
use BookStack\Util\HtmlContentFilterConfig;
|
||||
use BookStack\Util\HtmlDocument;
|
||||
use BookStack\Util\HtmlToPlainText;
|
||||
use BookStack\Util\WebSafeMimeSniffer;
|
||||
use Closure;
|
||||
use DOMElement;
|
||||
@@ -303,8 +304,8 @@ class PageContent
|
||||
public function toPlainText(): string
|
||||
{
|
||||
$html = $this->render(true);
|
||||
|
||||
return html_entity_decode(strip_tags($html));
|
||||
$converter = new HtmlToPlainText();
|
||||
return $converter->convert($html);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -359,7 +360,7 @@ class PageContent
|
||||
{
|
||||
$contentHash = md5($html);
|
||||
$contentId = $this->page->id;
|
||||
$contentTime = $this->page->updated_at?->timestamp ?? time();
|
||||
$contentTime = $this->page->updated_at->timestamp ?? time();
|
||||
$appVersion = AppVersion::get();
|
||||
$filterConfig = config('app.content_filtering') ?? '';
|
||||
return "page-content-cache::{$filterConfig}::{$appVersion}::{$contentId}::{$contentTime}::{$contentHash}";
|
||||
|
||||
@@ -20,8 +20,8 @@ class PermissionsUpdater
|
||||
*/
|
||||
public function updateFromPermissionsForm(Entity $entity, Request $request): void
|
||||
{
|
||||
$permissions = $request->get('permissions', null);
|
||||
$ownerId = $request->get('owned_by', null);
|
||||
$permissions = $request->input('permissions', null);
|
||||
$ownerId = $request->input('owned_by', null);
|
||||
|
||||
$entity->permissions()->delete();
|
||||
|
||||
@@ -47,7 +47,7 @@ class PermissionsUpdater
|
||||
{
|
||||
if (isset($data['role_permissions'])) {
|
||||
$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);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use BookStack\Util\CspService;
|
||||
use BookStack\Util\HtmlDocument;
|
||||
use BookStack\Util\HtmlToPlainText;
|
||||
use DOMElement;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
@@ -208,7 +209,7 @@ class ExportFormatter
|
||||
preg_match_all("/\<img.*?src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
|
||||
|
||||
// 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) {
|
||||
$oldImgTagString = $imgMatch;
|
||||
$srcString = $imageTagsOutput[2][$index];
|
||||
@@ -225,7 +226,7 @@ class ExportFormatter
|
||||
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
|
||||
|
||||
// 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) {
|
||||
$oldLinkString = $linkMatch;
|
||||
$srcString = $linksOutput[2][$index];
|
||||
@@ -242,24 +243,13 @@ class ExportFormatter
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$html = $pageRendered ? $page->html : (new PageContent($page))->render();
|
||||
// Add proceeding spaces before tags so spaces remain between
|
||||
// text within elements after stripping tags.
|
||||
$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;
|
||||
$contentText = (new HtmlToPlainText())->convert($html);
|
||||
return $page->name . ($fromParent ? "\n" : "\n\n") . $contentText;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -267,7 +257,7 @@ class ExportFormatter
|
||||
*/
|
||||
public function chapterToPlainText(Chapter $chapter): string
|
||||
{
|
||||
$text = $chapter->name . "\n" . $chapter->description;
|
||||
$text = $chapter->name . "\n" . $chapter->descriptionInfo()->getPlain();
|
||||
$text = trim($text) . "\n\n";
|
||||
|
||||
$parts = [];
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace BookStack\Exports;
|
||||
|
||||
use BookStack\Exceptions\PdfExportException;
|
||||
use Dompdf\Dompdf;
|
||||
use FontLib\Font;
|
||||
use Illuminate\Support\Str;
|
||||
use Knp\Snappy\Pdf as SnappyPdf;
|
||||
use Symfony\Component\Process\Exception\ProcessTimedOutException;
|
||||
use Symfony\Component\Process\Process;
|
||||
@@ -60,12 +62,65 @@ class PdfGenerator
|
||||
$domPdf = new Dompdf($options);
|
||||
$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->render();
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -45,7 +45,7 @@ final class ZipExportAttachment extends ZipExportModel
|
||||
$rules = [
|
||||
'id' => ['nullable', 'int', $context->uniqueIdRule('attachment')],
|
||||
'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()],
|
||||
];
|
||||
|
||||
|
||||
@@ -82,10 +82,8 @@ class ZipImportRunner
|
||||
$entity = $this->importBook($exportModel, $reader);
|
||||
} else if ($exportModel instanceof ZipExportChapter) {
|
||||
$entity = $this->importChapter($exportModel, $parent, $reader);
|
||||
} else if ($exportModel instanceof ZipExportPage) {
|
||||
$entity = $this->importPage($exportModel, $parent, $reader);
|
||||
} else {
|
||||
throw new ZipImportException(['No importable data found in import data.']);
|
||||
$entity = $this->importPage($exportModel, $parent, $reader);
|
||||
}
|
||||
|
||||
$this->references->replaceReferences();
|
||||
@@ -132,7 +130,7 @@ class ZipImportRunner
|
||||
'name' => $exportBook->name,
|
||||
'description_html' => $exportBook->description_html ?? '',
|
||||
'image' => $exportBook->cover ? $this->zipFileToUploadedFile($exportBook->cover, $reader) : null,
|
||||
'tags' => $this->exportTagsToInputArray($exportBook->tags ?? []),
|
||||
'tags' => $this->exportTagsToInputArray($exportBook->tags),
|
||||
]);
|
||||
|
||||
if ($book->coverInfo()->getImage()) {
|
||||
@@ -151,7 +149,7 @@ class ZipImportRunner
|
||||
foreach ($children as $child) {
|
||||
if ($child instanceof ZipExportChapter) {
|
||||
$this->importChapter($child, $book, $reader);
|
||||
} else if ($child instanceof ZipExportPage) {
|
||||
} else {
|
||||
$this->importPage($child, $book, $reader);
|
||||
}
|
||||
}
|
||||
@@ -166,7 +164,7 @@ class ZipImportRunner
|
||||
$chapter = $this->chapterRepo->create([
|
||||
'name' => $exportChapter->name,
|
||||
'description_html' => $exportChapter->description_html ?? '',
|
||||
'tags' => $this->exportTagsToInputArray($exportChapter->tags ?? []),
|
||||
'tags' => $this->exportTagsToInputArray($exportChapter->tags),
|
||||
], $parent);
|
||||
|
||||
$exportPages = $exportChapter->pages;
|
||||
@@ -199,7 +197,7 @@ class ZipImportRunner
|
||||
'name' => $exportPage->name,
|
||||
'markdown' => $exportPage->markdown ?? '',
|
||||
'html' => $exportPage->html ?? '',
|
||||
'tags' => $this->exportTagsToInputArray($exportPage->tags ?? []),
|
||||
'tags' => $this->exportTagsToInputArray($exportPage->tags),
|
||||
]);
|
||||
|
||||
$this->references->addPage($page, $exportPage);
|
||||
@@ -302,7 +300,7 @@ class ZipImportRunner
|
||||
array_push($chapters, ...$exportModel->chapters);
|
||||
} else if ($exportModel instanceof ZipExportChapter) {
|
||||
$chapters[] = $exportModel;
|
||||
} else if ($exportModel instanceof ZipExportPage) {
|
||||
} else {
|
||||
$pages[] = $exportModel;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,10 +68,6 @@ class ZipReferenceParser
|
||||
$matches = [];
|
||||
preg_match_all($referenceRegex, $content, $matches);
|
||||
|
||||
if (count($matches) < 3) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < count($matches[0]); $i++) {
|
||||
$referenceText = $matches[0][$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
|
||||
* 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);
|
||||
|
||||
if (count($filterableFields) > 0) {
|
||||
$listing->setFilterableFields($filterableFields);
|
||||
}
|
||||
|
||||
foreach ($modifiers as $modifier) {
|
||||
$listing->modifyResults($modifier);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ abstract class Controller extends BaseController
|
||||
*/
|
||||
protected function checkPermission(string|Permission $permission): void
|
||||
{
|
||||
if (!user() || !user()->can($permission)) {
|
||||
if (!user()->can($permission)) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,8 +61,7 @@ class JointPermissionBuilder
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var BookChild $entity */
|
||||
if ($entity->book) {
|
||||
if ($entity instanceof BookChild) {
|
||||
$entities[] = $entity->book;
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,8 @@ enum Permission: string
|
||||
case PageViewAll = 'page-view-all';
|
||||
case PageViewOwn = 'page-view-own';
|
||||
|
||||
case RevisionViewAll = 'revision-view-all';
|
||||
|
||||
/**
|
||||
* Get the generic permissions which may be queried for entities.
|
||||
*/
|
||||
|
||||
@@ -40,9 +40,9 @@ class SearchApiController extends ApiController
|
||||
{
|
||||
$this->validate($request, $this->rules['all']);
|
||||
|
||||
$options = SearchOptions::fromString($request->get('query') ?? '');
|
||||
$page = intval($request->get('page', '0')) ?: 1;
|
||||
$count = min(intval($request->get('count', '0')) ?: 20, 100);
|
||||
$options = SearchOptions::fromString($request->input('query') ?? '');
|
||||
$page = intval($request->input('page', '0')) ?: 1;
|
||||
$count = min(intval($request->input('count', '0')) ?: 20, 100);
|
||||
|
||||
$results = $this->searchRunner->searchEntities($options, 'all', $page, $count);
|
||||
$this->resultsFormatter->format($results['results']->all(), $options);
|
||||
|
||||
@@ -24,7 +24,7 @@ class SearchController extends Controller
|
||||
{
|
||||
$searchOpts = SearchOptions::fromRequest($request);
|
||||
$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);
|
||||
|
||||
$results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, $count);
|
||||
@@ -49,7 +49,7 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function searchBook(Request $request, int $bookId)
|
||||
{
|
||||
$term = $request->get('term', '');
|
||||
$term = $request->input('term', '');
|
||||
$results = $this->searchRunner->searchBook($bookId, $term);
|
||||
|
||||
return view('entities.list', ['entities' => $results]);
|
||||
@@ -60,7 +60,7 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function searchChapter(Request $request, int $chapterId)
|
||||
{
|
||||
$term = $request->get('term', '');
|
||||
$term = $request->input('term', '');
|
||||
$results = $this->searchRunner->searchChapter($chapterId, $term);
|
||||
|
||||
return view('entities.list', ['entities' => $results]);
|
||||
@@ -72,9 +72,9 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function searchForSelector(Request $request, QueryPopular $queryPopular)
|
||||
{
|
||||
$entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
|
||||
$searchTerm = $request->get('term', false);
|
||||
$permission = $request->get('permission', 'view');
|
||||
$entityTypes = $request->filled('types') ? explode(',', $request->input('types')) : ['page', 'chapter', 'book'];
|
||||
$searchTerm = $request->input('term', false);
|
||||
$permission = $request->input('permission', 'view');
|
||||
|
||||
// Search for entities otherwise show most popular
|
||||
if ($searchTerm !== false) {
|
||||
@@ -93,7 +93,7 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function templatesForSelector(Request $request)
|
||||
{
|
||||
$searchTerm = $request->get('term', false);
|
||||
$searchTerm = $request->input('term', false);
|
||||
|
||||
if ($searchTerm !== false) {
|
||||
$searchOptions = SearchOptions::fromString($searchTerm);
|
||||
@@ -119,7 +119,7 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function searchSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->get('term', '');
|
||||
$searchTerm = $request->input('term', '');
|
||||
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 5)['results'];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
@@ -136,8 +136,8 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function searchSiblings(Request $request, SiblingFetcher $siblingFetcher)
|
||||
{
|
||||
$type = $request->get('entity_type', null);
|
||||
$id = $request->get('entity_id', null);
|
||||
$type = $request->input('entity_type', null);
|
||||
$id = $request->input('entity_id', null);
|
||||
|
||||
$entities = $siblingFetcher->fetch($type, $id);
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class SearchOptions
|
||||
}
|
||||
|
||||
if ($request->has('term')) {
|
||||
return static::fromString($request->get('term'));
|
||||
return static::fromString($request->input('term'));
|
||||
}
|
||||
|
||||
$instance = new SearchOptions();
|
||||
@@ -121,13 +121,11 @@ class SearchOptions
|
||||
foreach ($patterns as $termType => $pattern) {
|
||||
$matches = [];
|
||||
preg_match_all($pattern, $searchString, $matches);
|
||||
if (count($matches) > 0) {
|
||||
foreach ($matches[1] as $index => $value) {
|
||||
$negated = str_starts_with($matches[0][$index], '-');
|
||||
$terms[$termType][] = $constructors[$termType]($value, $negated);
|
||||
}
|
||||
$searchString = preg_replace($pattern, '', $searchString);
|
||||
foreach ($matches[1] as $index => $value) {
|
||||
$negated = str_starts_with($matches[0][$index], '-');
|
||||
$terms[$termType][] = $constructors[$termType]($value, $negated);
|
||||
}
|
||||
$searchString = preg_replace($pattern, '', $searchString);
|
||||
}
|
||||
|
||||
// Unescape exacts and backslash escapes
|
||||
@@ -261,7 +259,7 @@ class SearchOptions
|
||||
$userFilters = ['updated_by', 'created_by', 'owned_by'];
|
||||
$unsupportedFilters = ['is_template', 'sort_by'];
|
||||
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;
|
||||
} else if (in_array($filter->getKey(), $unsupportedFilters, true)) {
|
||||
$options[] = $filter;
|
||||
|
||||
@@ -44,7 +44,7 @@ class AppSettingsStore
|
||||
}
|
||||
|
||||
// Clear icon image if requested
|
||||
if ($request->get('app_icon_reset')) {
|
||||
if ($request->input('app_icon_reset')) {
|
||||
$this->destroyExistingSettingImage('app-icon');
|
||||
setting()->remove('app-icon');
|
||||
foreach ($sizes as $size) {
|
||||
@@ -67,7 +67,7 @@ class AppSettingsStore
|
||||
}
|
||||
|
||||
// Clear logo image if requested
|
||||
if ($request->get('app_logo_reset')) {
|
||||
if ($request->input('app_logo_reset')) {
|
||||
$this->destroyExistingSettingImage('app-logo');
|
||||
setting()->remove('app-logo');
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class MaintenanceController extends Controller
|
||||
$this->checkPermission(Permission::SettingsManage);
|
||||
$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'));
|
||||
|
||||
$imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
|
||||
|
||||
@@ -58,7 +58,7 @@ class BookSortController extends Controller
|
||||
// Sort via map
|
||||
if ($request->filled('sort-tree')) {
|
||||
(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);
|
||||
|
||||
// Add activity for involved books.
|
||||
@@ -72,7 +72,7 @@ class BookSortController extends Controller
|
||||
}
|
||||
|
||||
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) {
|
||||
$sortSetId = null;
|
||||
}
|
||||
|
||||
@@ -125,9 +125,8 @@ class BookSorter
|
||||
*/
|
||||
protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
|
||||
{
|
||||
/** @var BookChild $model */
|
||||
$model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;
|
||||
if (!$model) {
|
||||
if (!($model instanceof BookChild)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,14 @@ class ThemeModuleManager
|
||||
}
|
||||
|
||||
$folderPath = $this->modulesFolderPath . DIRECTORY_SEPARATOR . $folderName;
|
||||
$zip->extractTo($folderPath);
|
||||
try {
|
||||
$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);
|
||||
if (!$module) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Theming;
|
||||
|
||||
use BookStack\Util\FilePathNormalizer;
|
||||
use ZipArchive;
|
||||
|
||||
readonly class ThemeModuleZip
|
||||
@@ -15,7 +16,46 @@ readonly class ThemeModuleZip
|
||||
{
|
||||
$zip = new ZipArchive();
|
||||
$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();
|
||||
}
|
||||
|
||||
@@ -31,7 +71,8 @@ readonly class ThemeModuleZip
|
||||
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();
|
||||
|
||||
if ($moduleJsonText === false) {
|
||||
@@ -95,4 +136,20 @@ readonly class ThemeModuleZip
|
||||
|
||||
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);
|
||||
$requestData = $this->validate($request, $this->rules()['create']);
|
||||
|
||||
$pageId = $request->get('uploaded_to');
|
||||
$pageId = $request->input('uploaded_to');
|
||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||
|
||||
@@ -134,7 +134,7 @@ class AttachmentApiController extends ApiController
|
||||
|
||||
$page = $attachment->page;
|
||||
if ($requestData['uploaded_to'] ?? false) {
|
||||
$pageId = $request->get('uploaded_to');
|
||||
$pageId = $request->input('uploaded_to');
|
||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||
$attachment->uploaded_to = $requestData['uploaded_to'];
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class AttachmentController extends Controller
|
||||
'file' => array_merge(['required'], $this->attachmentService->getFileValidationRules()),
|
||||
]);
|
||||
|
||||
$pageId = $request->get('uploaded_to');
|
||||
$pageId = $request->input('uploaded_to');
|
||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||
|
||||
$this->checkPermission(Permission::AttachmentCreateAll);
|
||||
@@ -125,8 +125,8 @@ class AttachmentController extends Controller
|
||||
$this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment);
|
||||
|
||||
$attachment = $this->attachmentService->updateFile($attachment, [
|
||||
'name' => $request->get('attachment_edit_name'),
|
||||
'link' => $request->get('attachment_edit_url'),
|
||||
'name' => $request->input('attachment_edit_name'),
|
||||
'link' => $request->input('attachment_edit_url'),
|
||||
]);
|
||||
|
||||
return view('attachments.manager-edit-form', [
|
||||
@@ -141,7 +141,7 @@ class AttachmentController extends Controller
|
||||
*/
|
||||
public function attachLink(Request $request)
|
||||
{
|
||||
$pageId = $request->get('attachment_link_uploaded_to');
|
||||
$pageId = $request->input('attachment_link_uploaded_to');
|
||||
|
||||
try {
|
||||
$this->validate($request, [
|
||||
@@ -161,8 +161,8 @@ class AttachmentController extends Controller
|
||||
$this->checkPermission(Permission::AttachmentCreateAll);
|
||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||
|
||||
$attachmentName = $request->get('attachment_link_name');
|
||||
$link = $request->get('attachment_link_url');
|
||||
$attachmentName = $request->input('attachment_link_name');
|
||||
$link = $request->input('attachment_link_url');
|
||||
$this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
|
||||
|
||||
return view('attachments.manager-link-form', [
|
||||
@@ -198,7 +198,7 @@ class AttachmentController extends Controller
|
||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||
|
||||
$attachmentOrder = $request->get('order');
|
||||
$attachmentOrder = $request->input('order');
|
||||
$this->attachmentService->updateFileOrderWithinPage($attachmentOrder, $pageId);
|
||||
|
||||
return response()->json(['message' => trans('entities.attachments_order_updated')]);
|
||||
@@ -231,7 +231,7 @@ class AttachmentController extends Controller
|
||||
$attachmentStream = $this->attachmentService->streamAttachmentFromStorage($attachment);
|
||||
$attachmentSize = $this->attachmentService->getAttachmentFileSize($attachment);
|
||||
|
||||
if ($request->get('open') === 'true') {
|
||||
if ($request->input('open') === 'true') {
|
||||
return $this->download()->streamedInline($attachmentStream, $fileName, $attachmentSize);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ class DrawioImageController extends Controller
|
||||
*/
|
||||
public function list(Request $request, ImageResizer $resizer)
|
||||
{
|
||||
$page = $request->get('page', 1);
|
||||
$searchTerm = $request->get('search', null);
|
||||
$uploadedToFilter = $request->get('uploaded_to', null);
|
||||
$parentTypeFilter = $request->get('filter_type', null);
|
||||
$page = $request->input('page', 1);
|
||||
$searchTerm = $request->input('search', null);
|
||||
$uploadedToFilter = $request->input('uploaded_to', null);
|
||||
$parentTypeFilter = $request->input('filter_type', null);
|
||||
|
||||
$imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
|
||||
$viewData = [
|
||||
@@ -59,10 +59,10 @@ class DrawioImageController extends Controller
|
||||
]);
|
||||
|
||||
$this->checkPermission(Permission::ImageCreateAll);
|
||||
$imageBase64Data = $request->get('image');
|
||||
$imageBase64Data = $request->input('image');
|
||||
|
||||
try {
|
||||
$uploadedTo = $request->get('uploaded_to', 0);
|
||||
$uploadedTo = $request->input('uploaded_to', 0);
|
||||
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
|
||||
} catch (ImageUploadException $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
|
||||
@@ -24,10 +24,10 @@ class GalleryImageController extends Controller
|
||||
*/
|
||||
public function list(Request $request, ImageResizer $resizer)
|
||||
{
|
||||
$page = $request->get('page', 1);
|
||||
$searchTerm = $request->get('search', null);
|
||||
$uploadedToFilter = $request->get('uploaded_to', null);
|
||||
$parentTypeFilter = $request->get('filter_type', null);
|
||||
$page = $request->input('page', 1);
|
||||
$searchTerm = $request->input('search', null);
|
||||
$uploadedToFilter = $request->input('uploaded_to', null);
|
||||
$parentTypeFilter = $request->input('filter_type', null);
|
||||
|
||||
$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 30, $uploadedToFilter, $searchTerm);
|
||||
$viewData = [
|
||||
@@ -69,7 +69,7 @@ class GalleryImageController extends Controller
|
||||
|
||||
try {
|
||||
$imageUpload = $request->file('file');
|
||||
$uploadedTo = $request->get('uploaded_to', 0);
|
||||
$uploadedTo = $request->input('uploaded_to', 0);
|
||||
$image = $this->imageRepo->saveNew($imageUpload, 'gallery', $uploadedTo);
|
||||
} catch (ImageUploadException $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
|
||||
@@ -91,7 +91,7 @@ class ImageRepo
|
||||
$parentFilter = function (Builder $query) use ($filterType, $contextPage) {
|
||||
if ($filterType === 'page') {
|
||||
$query->where('uploaded_to', '=', $contextPage->id);
|
||||
} else if ($filterType === 'book') {
|
||||
} else {
|
||||
$validPageIds = $contextPage->book->pages()
|
||||
->scopes('visible')
|
||||
->pluck('id')
|
||||
|
||||
@@ -148,7 +148,7 @@ class UserAvatars
|
||||
$responseCount++;
|
||||
$isRedirect = ($response->getStatusCode() === 301 || $response->getStatusCode() === 302);
|
||||
$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) {
|
||||
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 */
|
||||
$role = null;
|
||||
if ($request->has('copy_from')) {
|
||||
$role = Role::query()->find($request->get('copy_from'));
|
||||
$role = Role::query()->find($request->input('copy_from'));
|
||||
}
|
||||
|
||||
if ($role) {
|
||||
@@ -150,7 +150,7 @@ class RoleController extends Controller
|
||||
$this->checkPermission(Permission::UserRolesManage);
|
||||
|
||||
try {
|
||||
$migrateRoleId = intval($request->get('migrate_role_id') ?: "0");
|
||||
$migrateRoleId = intval($request->input('migrate_role_id') ?: "0");
|
||||
$this->permissionsRepo->deleteRole($id, $migrateRoleId);
|
||||
} catch (PermissionsException $e) {
|
||||
$this->showErrorNotification($e->getMessage());
|
||||
|
||||
@@ -106,8 +106,8 @@ class UserAccountController extends Controller
|
||||
*/
|
||||
public function updateShortcuts(Request $request)
|
||||
{
|
||||
$enabled = $request->get('enabled') === 'true';
|
||||
$providedShortcuts = $request->get('shortcut', []);
|
||||
$enabled = $request->input('enabled') === 'true';
|
||||
$providedShortcuts = $request->input('shortcut', []);
|
||||
$shortcuts = new UserShortcutMap($providedShortcuts);
|
||||
|
||||
setting()->putForCurrentUser('ui-shortcuts', $shortcuts->toJson());
|
||||
@@ -218,7 +218,7 @@ class UserAccountController extends Controller
|
||||
{
|
||||
$this->preventAccessInDemoMode();
|
||||
|
||||
$requestNewOwnerId = intval($request->get('new_owner_id')) ?: null;
|
||||
$requestNewOwnerId = intval($request->input('new_owner_id')) ?: null;
|
||||
$newOwnerId = userCan(Permission::UsersManage) ? $requestNewOwnerId : null;
|
||||
|
||||
$this->userRepo->destroy(user(), $newOwnerId);
|
||||
|
||||
@@ -141,7 +141,7 @@ class UserApiController extends ApiController
|
||||
public function delete(Request $request, string $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);
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class UserController extends Controller
|
||||
$this->checkPermission(Permission::UsersManage);
|
||||
|
||||
$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';
|
||||
$passwordRequired = ($authMethod === 'standard' && !$sendInvite);
|
||||
|
||||
@@ -202,7 +202,7 @@ class UserController extends Controller
|
||||
$this->checkPermission(Permission::UsersManage);
|
||||
|
||||
$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);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class UserPreferencesController extends Controller
|
||||
return $this->redirectToRequest($request);
|
||||
}
|
||||
|
||||
$view = $request->get('view');
|
||||
$view = $request->input('view');
|
||||
if (!in_array($view, ['grid', 'list'])) {
|
||||
$view = 'list';
|
||||
}
|
||||
@@ -44,8 +44,8 @@ class UserPreferencesController extends Controller
|
||||
return $this->redirectToRequest($request);
|
||||
}
|
||||
|
||||
$sort = substr($request->get('sort') ?: 'name', 0, 50);
|
||||
$order = $request->get('order') === 'desc' ? 'desc' : 'asc';
|
||||
$sort = substr($request->input('sort') ?: 'name', 0, 50);
|
||||
$order = $request->input('order') === 'desc' ? 'desc' : 'asc';
|
||||
|
||||
$sortKey = $type . '_sort';
|
||||
$orderKey = $type . '_sort_order';
|
||||
@@ -76,7 +76,7 @@ class UserPreferencesController extends Controller
|
||||
return response('Invalid key', 500);
|
||||
}
|
||||
|
||||
$newState = $request->get('expand', 'false');
|
||||
$newState = $request->input('expand', 'false');
|
||||
setting()->putForCurrentUser('section_expansion#' . $type, $newState);
|
||||
|
||||
return response('', 204);
|
||||
|
||||
@@ -26,7 +26,7 @@ class UserSearchController extends Controller
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
$search = $request->get('search', '');
|
||||
$search = $request->input('search', '');
|
||||
$query = User::query()
|
||||
->orderBy('name', 'asc')
|
||||
->take(20);
|
||||
@@ -58,7 +58,7 @@ class UserSearchController extends Controller
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
$search = $request->get('search', '');
|
||||
$search = $request->input('search', '');
|
||||
$query = User::query()
|
||||
->orderBy('name', 'asc')
|
||||
->take(20);
|
||||
|
||||
@@ -222,8 +222,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
public function getAvatar(int $size = 50): string
|
||||
{
|
||||
$default = url('/user_avatar.png');
|
||||
$imageId = $this->image_id;
|
||||
if ($imageId === 0 || $imageId === '0' || $imageId === null) {
|
||||
if ($this->image_id === 0) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ class HtmlDescriptionFilter
|
||||
'span' => [],
|
||||
'em' => [],
|
||||
'br' => [],
|
||||
'code' => [],
|
||||
];
|
||||
|
||||
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
|
||||
{
|
||||
$search = $request->get('search', '');
|
||||
$search = $request->input('search', '');
|
||||
$sort = setting()->getForCurrentUser($typeKey . '_sort', '');
|
||||
$order = setting()->getForCurrentUser($typeKey . '_sort_order', $sortDescDefault ? 'desc' : 'asc');
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Create new revision-view-all permission
|
||||
$permissionId = DB::table('role_permissions')->insertGetId([
|
||||
'name' => 'revision-view-all',
|
||||
'created_at' => Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => Carbon::now()->toDateTimeString(),
|
||||
]);
|
||||
|
||||
// Get ids of page view permissions
|
||||
$pageViewPermissions = DB::table('role_permissions')
|
||||
->whereIn('name', [
|
||||
'page-view-own',
|
||||
'page-view-all',
|
||||
])->get();
|
||||
|
||||
if ($pageViewPermissions->count() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get role ids which have page view permission
|
||||
$applicableRoleIds = DB::table('permission_role')
|
||||
->whereIn('permission_id', $pageViewPermissions->pluck('id'))
|
||||
->pluck('role_id')
|
||||
->unique()
|
||||
->all();
|
||||
|
||||
// Assign the new permission to relevant roles
|
||||
$newPermissionRoles = array_values(array_map(function (int $roleId) use ($permissionId) {
|
||||
return [
|
||||
'role_id' => $roleId,
|
||||
'permission_id' => $permissionId,
|
||||
];
|
||||
}, $applicableRoleIds));
|
||||
|
||||
DB::table('permission_role')->insert($newPermissionRoles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Get the permission to remove
|
||||
$revisionViewPermission = DB::table('role_permissions')
|
||||
->where('name', '=', 'revision-view-all')
|
||||
->first();
|
||||
|
||||
if (!$revisionViewPermission) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the permission, and its use on roles, from the database
|
||||
DB::table('permission_role')->where('permission_id', '=', $revisionViewPermission->id)->delete();
|
||||
DB::table('role_permissions')->where('id', '=', $revisionViewPermission->id)->delete();
|
||||
}
|
||||
};
|
||||
1
dev/api/requests/tags-list-values.http
Normal file
1
dev/api/requests/tags-list-values.http
Normal file
@@ -0,0 +1 @@
|
||||
GET /api/tags/values-for-name?name=Category
|
||||
32
dev/api/responses/tags-list-names.json
Normal file
32
dev/api/responses/tags-list-names.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "Category",
|
||||
"values": 8,
|
||||
"usages": 184,
|
||||
"page_count": 3,
|
||||
"chapter_count": 8,
|
||||
"book_count": 171,
|
||||
"shelf_count": 2
|
||||
},
|
||||
{
|
||||
"name": "Review Due",
|
||||
"values": 2,
|
||||
"usages": 2,
|
||||
"page_count": 1,
|
||||
"chapter_count": 0,
|
||||
"book_count": 1,
|
||||
"shelf_count": 0
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
"values": 2,
|
||||
"usages": 2,
|
||||
"page_count": 0,
|
||||
"chapter_count": 1,
|
||||
"book_count": 1,
|
||||
"shelf_count": 0
|
||||
}
|
||||
],
|
||||
"total": 3
|
||||
}
|
||||
32
dev/api/responses/tags-list-values.json
Normal file
32
dev/api/responses/tags-list-values.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "Category",
|
||||
"value": "Cool Stuff",
|
||||
"usages": 3,
|
||||
"page_count": 1,
|
||||
"chapter_count": 0,
|
||||
"book_count": 2,
|
||||
"shelf_count": 0
|
||||
},
|
||||
{
|
||||
"name": "Category",
|
||||
"value": "Top Content",
|
||||
"usages": 168,
|
||||
"page_count": 0,
|
||||
"chapter_count": 3,
|
||||
"book_count": 165,
|
||||
"shelf_count": 0
|
||||
},
|
||||
{
|
||||
"name": "Category",
|
||||
"value": "Learning",
|
||||
"usages": 2,
|
||||
"page_count": 0,
|
||||
"chapter_count": 0,
|
||||
"book_count": 0,
|
||||
"shelf_count": 2
|
||||
}
|
||||
],
|
||||
"total": 3
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user