mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-05-04 18:08:46 +03:00
Compare commits
35 Commits
Zhey-on/fe
...
MilnerMart
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
844c79ae72 | ||
|
|
f3c1fad50a | ||
|
|
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 | ||
|
|
e033578fea | ||
|
|
a7dd998ac9 | ||
|
|
b9d650785a | ||
|
|
abed4eae0c | ||
|
|
c7d3775bb9 | ||
|
|
0b659671fe |
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.
|
||||
5
.github/translators.txt
vendored
5
.github/translators.txt
vendored
@@ -444,7 +444,7 @@ Irjan Olsen (Irch) :: Norwegian Bokmal
|
||||
Aleksandar Jovanovic (jovanoviczaleksandar) :: Serbian (Cyrillic)
|
||||
Red (RedVortex) :: Hebrew
|
||||
xgrug :: Chinese Simplified
|
||||
HrCalmar :: Danish
|
||||
Calle Calmar (HrCalmar) :: Danish
|
||||
Avishay Rapp (AvishayRapp) :: Hebrew
|
||||
matthias4217 :: French
|
||||
Berke BOYLU2 (berkeboylu2) :: Turkish
|
||||
@@ -534,3 +534,6 @@ Charllys Fernandes (CharllysFernandes) :: Portuguese, Brazilian
|
||||
Ilgiz Zigangirov (inov8) :: Russian
|
||||
Max Israelsson (Blezie) :: Swedish
|
||||
Skiddybison5924 (chris-devel0per) :: German
|
||||
Veyilla Nightwhisper (Veyilla) :: German
|
||||
João Barbosa (hypeedd) :: Portuguese
|
||||
Abcdefg Hijklmn (collatek) :: Korean
|
||||
|
||||
2
.github/workflows/test-js.yml
vendored
2
.github/workflows/test-js.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install NPM deps
|
||||
run: npm ci
|
||||
|
||||
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;
|
||||
|
||||
@@ -55,6 +55,7 @@ class OidcController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$this->throwIfAuthorizationError($request);
|
||||
$this->oidcService->processAuthorizeResponse($request->query('code'));
|
||||
} catch (OidcException $oidcException) {
|
||||
$this->showErrorNotification($oidcException->getMessage());
|
||||
@@ -72,4 +73,23 @@ class OidcController extends Controller
|
||||
{
|
||||
return redirect($this->oidcService->logout());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @throws OidcException
|
||||
*/
|
||||
private function throwIfAuthorizationError(Request $request): void
|
||||
{
|
||||
$errorCode = $request->query('error');
|
||||
if (!$errorCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$errorDescription = $request->query('error_description');
|
||||
if ($errorDescription) {
|
||||
throw new OidcException($errorDescription);
|
||||
}
|
||||
|
||||
throw new OidcException(trans('errors.oidc_fail_authed', ['system' => config('oidc.name')]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
27
app/Access/Oidc/OidcHttpBasicWithClientIdOptionProvider.php
Normal file
27
app/Access/Oidc/OidcHttpBasicWithClientIdOptionProvider.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Access\Oidc;
|
||||
|
||||
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
|
||||
|
||||
/**
|
||||
* Option provider that sends credentials via HTTP Basic Auth header
|
||||
* and also includes client_id in the request body.
|
||||
*/
|
||||
class OidcHttpBasicWithClientIdOptionProvider extends HttpBasicAuthOptionProvider
|
||||
{
|
||||
public function getAccessTokenOptions($method, array $params)
|
||||
{
|
||||
$clientId = $params['client_id'] ?? null;
|
||||
|
||||
$options = parent::getAccessTokenOptions($method, $params);
|
||||
|
||||
if ($clientId) {
|
||||
parse_str($options['body'] ?? '', $body);
|
||||
$body['client_id'] = $clientId;
|
||||
$options['body'] = http_build_query($body);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,10 @@ class OidcIdToken extends OidcJwtWithClaims implements ProvidesClaims
|
||||
*
|
||||
* @throws OidcInvalidTokenException
|
||||
*/
|
||||
public function validate(string $clientId): bool
|
||||
public function validate(OidcProviderSettings $settings): bool
|
||||
{
|
||||
parent::validateCommonTokenDetails($clientId);
|
||||
$this->validateTokenClaims($clientId);
|
||||
parent::validateCommonTokenDetails($settings);
|
||||
$this->validateTokenClaims($settings->clientId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -9,7 +9,9 @@ class OidcJwtWithClaims implements ProvidesClaims
|
||||
protected string $signature;
|
||||
protected string $issuer;
|
||||
protected array $tokenParts = [];
|
||||
|
||||
protected array $acceptedSignatures = [self::hs256Signature, self::rs256Signature];
|
||||
private const hs256Signature = 'HS256'
|
||||
, rs256Signature = 'RS256';
|
||||
/**
|
||||
* @var array[]|string[]
|
||||
*/
|
||||
@@ -59,11 +61,11 @@ class OidcJwtWithClaims implements ProvidesClaims
|
||||
*
|
||||
* @throws OidcInvalidTokenException
|
||||
*/
|
||||
public function validateCommonTokenDetails(string $clientId): bool
|
||||
public function validateCommonTokenDetails(OidcProviderSettings $settings): bool
|
||||
{
|
||||
$this->validateTokenStructure();
|
||||
$this->validateTokenSignature();
|
||||
$this->validateCommonClaims($clientId);
|
||||
$this->validateTokenSignature($settings);
|
||||
$this->validateCommonClaims($settings->clientId);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -117,31 +119,42 @@ class OidcJwtWithClaims implements ProvidesClaims
|
||||
*
|
||||
* @throws OidcInvalidTokenException
|
||||
*/
|
||||
protected function validateTokenSignature(): void
|
||||
{
|
||||
if ($this->header['alg'] !== 'RS256') {
|
||||
throw new OidcInvalidTokenException("Only RS256 signature validation is supported. Token reports using {$this->header['alg']}");
|
||||
protected function validateTokenSignature(OidcProviderSettings $settings): void {
|
||||
$validSignatures = implode(', ',$this->acceptedSignatures);
|
||||
switch ($this->header['alg']) {
|
||||
case self::rs256Signature:
|
||||
$parsedKeys = array_map(function ($key) {
|
||||
try {
|
||||
return new OidcJwtSigningKey($key);
|
||||
} catch (OidcInvalidKeyException $e) {
|
||||
throw new OidcInvalidTokenException('Failed to read signing key with error: ' . $e->getMessage());
|
||||
}
|
||||
}, $this->keys);
|
||||
|
||||
$parsedKeys = array_filter($parsedKeys);
|
||||
|
||||
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
|
||||
/** @var OidcJwtSigningKey $parsedKey */
|
||||
foreach ($parsedKeys as $parsedKey) {
|
||||
if ($parsedKey->verify($contentToSign, $this->signature)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new OidcInvalidTokenException('Token signature could not be validated using the provided keys');
|
||||
case self::hs256Signature:
|
||||
$secret = $settings->clientSecret;
|
||||
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
|
||||
$expectedSignature = hash_hmac('sha256', $contentToSign, $secret, true);
|
||||
|
||||
if (hash_equals($expectedSignature, $this->signature)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new OidcInvalidTokenException('Token signature could not be validated using the provided secret');
|
||||
default:
|
||||
throw new OidcInvalidTokenException("Only $validSignatures signatures validation are supported. Token reports using {$this->header['alg']}");
|
||||
}
|
||||
|
||||
$parsedKeys = array_map(function ($key) {
|
||||
try {
|
||||
return new OidcJwtSigningKey($key);
|
||||
} catch (OidcInvalidKeyException $e) {
|
||||
throw new OidcInvalidTokenException('Failed to read signing key with error: ' . $e->getMessage());
|
||||
}
|
||||
}, $this->keys);
|
||||
|
||||
$parsedKeys = array_filter($parsedKeys);
|
||||
|
||||
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
|
||||
/** @var OidcJwtSigningKey $parsedKey */
|
||||
foreach ($parsedKeys as $parsedKey) {
|
||||
if ($parsedKey->verify($contentToSign, $this->signature)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new OidcInvalidTokenException('Token signature could not be validated using the provided keys');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -74,7 +74,7 @@ class OidcProviderSettings
|
||||
{
|
||||
$this->validateInitial();
|
||||
|
||||
$required = ['keys', 'tokenEndpoint', 'authorizationEndpoint'];
|
||||
$required = ['tokenEndpoint', 'authorizationEndpoint'];
|
||||
foreach ($required as $prop) {
|
||||
if (empty($this->$prop)) {
|
||||
throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value");
|
||||
|
||||
@@ -14,7 +14,6 @@ use BookStack\Theming\ThemeEvents;
|
||||
use BookStack\Uploads\UserAvatars;
|
||||
use BookStack\Users\Models\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
|
||||
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
|
||||
|
||||
/**
|
||||
@@ -140,7 +139,7 @@ class OidcService
|
||||
'redirectUri' => url('/oidc/callback'),
|
||||
], [
|
||||
'httpClient' => $this->http->buildClient(5),
|
||||
'optionProvider' => new HttpBasicAuthOptionProvider(),
|
||||
'optionProvider' => new OidcHttpBasicWithClientIdOptionProvider(),
|
||||
]);
|
||||
|
||||
foreach ($this->getAdditionalScopes() as $scope) {
|
||||
@@ -199,7 +198,7 @@ class OidcService
|
||||
}
|
||||
|
||||
try {
|
||||
$idToken->validate($settings->clientId);
|
||||
$idToken->validate($settings);
|
||||
} catch (OidcInvalidTokenException $exception) {
|
||||
throw new OidcException("ID token validation failed with error: {$exception->getMessage()}");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
341
composer.lock
generated
341
composer.lock
generated
@@ -62,16 +62,16 @@
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.373.7",
|
||||
"version": "3.376.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "4402bd10f913e66b7271f44466be8d5ba6c9146e"
|
||||
"reference": "2081f8db174df4bb8842aed3b7b513590ee9d219"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/4402bd10f913e66b7271f44466be8d5ba6c9146e",
|
||||
"reference": "4402bd10f913e66b7271f44466be8d5ba6c9146e",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2081f8db174df4bb8842aed3b7b513590ee9d219",
|
||||
"reference": "2081f8db174df4bb8842aed3b7b513590ee9d219",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -153,22 +153,22 @@
|
||||
"support": {
|
||||
"forum": "https://github.com/aws/aws-sdk-php/discussions",
|
||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.373.7"
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.376.3"
|
||||
},
|
||||
"time": "2026-03-20T18:14:19+00:00"
|
||||
"time": "2026-04-03T18:07:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
"version": "v3.0.4",
|
||||
"version": "v3.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Bacon/BaconQrCode.git",
|
||||
"reference": "3feed0e212b8412cc5d2612706744789b0615824"
|
||||
"reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/3feed0e212b8412cc5d2612706744789b0615824",
|
||||
"reference": "3feed0e212b8412cc5d2612706744789b0615824",
|
||||
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2",
|
||||
"reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -208,9 +208,9 @@
|
||||
"homepage": "https://github.com/Bacon/BaconQrCode",
|
||||
"support": {
|
||||
"issues": "https://github.com/Bacon/BaconQrCode/issues",
|
||||
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.4"
|
||||
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.1.1"
|
||||
},
|
||||
"time": "2026-03-16T01:01:30+00:00"
|
||||
"time": "2026-04-05T21:06:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -982,16 +982,16 @@
|
||||
},
|
||||
{
|
||||
"name": "firebase/php-jwt",
|
||||
"version": "v7.0.3",
|
||||
"version": "v7.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/firebase/php-jwt.git",
|
||||
"reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e"
|
||||
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/28aa0694bcfdfa5e2959c394d5a1ee7a5083629e",
|
||||
"reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e",
|
||||
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380",
|
||||
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -999,6 +999,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/guzzle": "^7.4",
|
||||
"phpfastcache/phpfastcache": "^9.2",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"psr/cache": "^2.0||^3.0",
|
||||
@@ -1039,9 +1040,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/firebase/php-jwt/issues",
|
||||
"source": "https://github.com/firebase/php-jwt/tree/v7.0.3"
|
||||
"source": "https://github.com/firebase/php-jwt/tree/v7.0.5"
|
||||
},
|
||||
"time": "2026-02-25T22:16:40+00:00"
|
||||
"time": "2026-04-01T20:38:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fruitcake/php-cors",
|
||||
@@ -1801,16 +1802,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v12.55.1",
|
||||
"version": "v12.56.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "6d9185a248d101b07eecaf8fd60b18129545fd33"
|
||||
"reference": "dac16d424b59debb2273910dde88eb7050a2a709"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/6d9185a248d101b07eecaf8fd60b18129545fd33",
|
||||
"reference": "6d9185a248d101b07eecaf8fd60b18129545fd33",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/dac16d424b59debb2273910dde88eb7050a2a709",
|
||||
"reference": "dac16d424b59debb2273910dde88eb7050a2a709",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2019,20 +2020,20 @@
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2026-03-18T14:28:59+00:00"
|
||||
"time": "2026-03-26T14:51:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
"version": "v0.3.15",
|
||||
"version": "v0.3.16",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/prompts.git",
|
||||
"reference": "4bb8107ec97651fd3f17f897d6489dbc4d8fb999"
|
||||
"reference": "11e7d5f93803a2190b00e145142cb00a33d17ad2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/prompts/zipball/4bb8107ec97651fd3f17f897d6489dbc4d8fb999",
|
||||
"reference": "4bb8107ec97651fd3f17f897d6489dbc4d8fb999",
|
||||
"url": "https://api.github.com/repos/laravel/prompts/zipball/11e7d5f93803a2190b00e145142cb00a33d17ad2",
|
||||
"reference": "11e7d5f93803a2190b00e145142cb00a33d17ad2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2076,9 +2077,9 @@
|
||||
"description": "Add beautiful and user-friendly forms to your command-line applications.",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/prompts/issues",
|
||||
"source": "https://github.com/laravel/prompts/tree/v0.3.15"
|
||||
"source": "https://github.com/laravel/prompts/tree/v0.3.16"
|
||||
},
|
||||
"time": "2026-03-17T13:45:17+00:00"
|
||||
"time": "2026-03-23T14:35:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/serializable-closure",
|
||||
@@ -2143,16 +2144,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/socialite",
|
||||
"version": "v5.25.0",
|
||||
"version": "v5.26.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/socialite.git",
|
||||
"reference": "231f572e1a37c9ca1fb8085e9fb8608285beafb3"
|
||||
"reference": "db6ec2ee967b7f06412c3a0cf1daaf072f4752a4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/socialite/zipball/231f572e1a37c9ca1fb8085e9fb8608285beafb3",
|
||||
"reference": "231f572e1a37c9ca1fb8085e9fb8608285beafb3",
|
||||
"url": "https://api.github.com/repos/laravel/socialite/zipball/db6ec2ee967b7f06412c3a0cf1daaf072f4752a4",
|
||||
"reference": "db6ec2ee967b7f06412c3a0cf1daaf072f4752a4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2211,7 +2212,7 @@
|
||||
"issues": "https://github.com/laravel/socialite/issues",
|
||||
"source": "https://github.com/laravel/socialite"
|
||||
},
|
||||
"time": "2026-02-27T13:56:35+00:00"
|
||||
"time": "2026-03-29T14:50:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/tinker",
|
||||
@@ -2470,16 +2471,16 @@
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem",
|
||||
"version": "3.32.0",
|
||||
"version": "3.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem.git",
|
||||
"reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725"
|
||||
"reference": "570b8871e0ce693764434b29154c54b434905350"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/254b1595b16b22dbddaaef9ed6ca9fdac4956725",
|
||||
"reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/570b8871e0ce693764434b29154c54b434905350",
|
||||
"reference": "570b8871e0ce693764434b29154c54b434905350",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2547,9 +2548,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/flysystem/issues",
|
||||
"source": "https://github.com/thephpleague/flysystem/tree/3.32.0"
|
||||
"source": "https://github.com/thephpleague/flysystem/tree/3.33.0"
|
||||
},
|
||||
"time": "2026-02-25T17:01:41+00:00"
|
||||
"time": "2026-03-25T07:59:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-aws-s3-v3",
|
||||
@@ -4664,16 +4665,16 @@
|
||||
},
|
||||
{
|
||||
"name": "psy/psysh",
|
||||
"version": "v0.12.21",
|
||||
"version": "v0.12.22",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bobthecow/psysh.git",
|
||||
"reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97"
|
||||
"reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/4821fab5b7cd8c49a673a9fd5754dc9162bb9e97",
|
||||
"reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97",
|
||||
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/3be75d5b9244936dd4ac62ade2bfb004d13acf0f",
|
||||
"reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4737,9 +4738,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/bobthecow/psysh/issues",
|
||||
"source": "https://github.com/bobthecow/psysh/tree/v0.12.21"
|
||||
"source": "https://github.com/bobthecow/psysh/tree/v0.12.22"
|
||||
},
|
||||
"time": "2026-03-06T21:21:28+00:00"
|
||||
"time": "2026-03-22T23:03:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ralouphie/getallheaders",
|
||||
@@ -5431,16 +5432,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/clock",
|
||||
"version": "v7.4.0",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/clock.git",
|
||||
"reference": "9169f24776edde469914c1e7a1442a50f7a4e110"
|
||||
"reference": "674fa3b98e21531dd040e613479f5f6fa8f32111"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110",
|
||||
"reference": "9169f24776edde469914c1e7a1442a50f7a4e110",
|
||||
"url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111",
|
||||
"reference": "674fa3b98e21531dd040e613479f5f6fa8f32111",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5485,7 +5486,7 @@
|
||||
"time"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/clock/tree/v7.4.0"
|
||||
"source": "https://github.com/symfony/clock/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5505,20 +5506,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-12T15:39:26+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.4.7",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d"
|
||||
"reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d",
|
||||
"reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707",
|
||||
"reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5583,7 +5584,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v7.4.7"
|
||||
"source": "https://github.com/symfony/console/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5603,20 +5604,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-06T14:06:20+00:00"
|
||||
"time": "2026-03-30T13:54:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/css-selector",
|
||||
"version": "v7.4.6",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/css-selector.git",
|
||||
"reference": "2e7c52c647b406e2107dd867db424a4dbac91864"
|
||||
"reference": "b055f228a4178a1d6774909903905e3475f3eac8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/css-selector/zipball/2e7c52c647b406e2107dd867db424a4dbac91864",
|
||||
"reference": "2e7c52c647b406e2107dd867db424a4dbac91864",
|
||||
"url": "https://api.github.com/repos/symfony/css-selector/zipball/b055f228a4178a1d6774909903905e3475f3eac8",
|
||||
"reference": "b055f228a4178a1d6774909903905e3475f3eac8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5652,7 +5653,7 @@
|
||||
"description": "Converts CSS selectors to XPath expressions",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/css-selector/tree/v7.4.6"
|
||||
"source": "https://github.com/symfony/css-selector/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5672,7 +5673,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-17T07:53:42+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
@@ -5743,16 +5744,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/error-handler",
|
||||
"version": "v7.4.4",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/error-handler.git",
|
||||
"reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8"
|
||||
"reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8",
|
||||
"reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8",
|
||||
"url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa",
|
||||
"reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5801,7 +5802,7 @@
|
||||
"description": "Provides tools to manage errors and ease debugging PHP code",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/error-handler/tree/v7.4.4"
|
||||
"source": "https://github.com/symfony/error-handler/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5821,20 +5822,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-20T16:42:42+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v7.4.4",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
"reference": "dc2c0eba1af673e736bb851d747d266108aea746"
|
||||
"reference": "f57b899fa736fd71121168ef268f23c206083f0a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746",
|
||||
"reference": "dc2c0eba1af673e736bb851d747d266108aea746",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f57b899fa736fd71121168ef268f23c206083f0a",
|
||||
"reference": "f57b899fa736fd71121168ef268f23c206083f0a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5886,7 +5887,7 @@
|
||||
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4"
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5906,7 +5907,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-05T11:45:34+00:00"
|
||||
"time": "2026-03-30T13:54:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher-contracts",
|
||||
@@ -5986,16 +5987,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v7.4.6",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "3ebc794fa5315e59fd122561623c2e2e4280538e"
|
||||
"reference": "58b9790d12f9670b7f53a1c1738febd3108970a5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e",
|
||||
"reference": "3ebc794fa5315e59fd122561623c2e2e4280538e",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5",
|
||||
"reference": "58b9790d12f9670b7f53a1c1738febd3108970a5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6032,7 +6033,7 @@
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.4.6"
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6052,20 +6053,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-25T16:50:00+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.4.6",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf"
|
||||
"reference": "e0be088d22278583a82da281886e8c3592fbf149"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf",
|
||||
"reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149",
|
||||
"reference": "e0be088d22278583a82da281886e8c3592fbf149",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6100,7 +6101,7 @@
|
||||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v7.4.6"
|
||||
"source": "https://github.com/symfony/finder/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6120,20 +6121,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-29T09:40:50+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-foundation",
|
||||
"version": "v7.4.7",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-foundation.git",
|
||||
"reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81"
|
||||
"reference": "9381209597ec66c25be154cbf2289076e64d1eab"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/f94b3e7b7dafd40e666f0c9ff2084133bae41e81",
|
||||
"reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81",
|
||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab",
|
||||
"reference": "9381209597ec66c25be154cbf2289076e64d1eab",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6182,7 +6183,7 @@
|
||||
"description": "Defines an object-oriented layer for the HTTP specification",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-foundation/tree/v7.4.7"
|
||||
"source": "https://github.com/symfony/http-foundation/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6202,20 +6203,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-06T13:15:18+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-kernel",
|
||||
"version": "v7.4.7",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-kernel.git",
|
||||
"reference": "3b3fcf386c809be990c922e10e4c620d6367cab1"
|
||||
"reference": "017e76ad089bac281553389269e259e155935e1a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/3b3fcf386c809be990c922e10e4c620d6367cab1",
|
||||
"reference": "3b3fcf386c809be990c922e10e4c620d6367cab1",
|
||||
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/017e76ad089bac281553389269e259e155935e1a",
|
||||
"reference": "017e76ad089bac281553389269e259e155935e1a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6301,7 +6302,7 @@
|
||||
"description": "Provides a structured process for converting a Request into a Response",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-kernel/tree/v7.4.7"
|
||||
"source": "https://github.com/symfony/http-kernel/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6321,20 +6322,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-06T16:33:18+00:00"
|
||||
"time": "2026-03-31T20:57:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mailer",
|
||||
"version": "v7.4.6",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/mailer.git",
|
||||
"reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9"
|
||||
"reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9",
|
||||
"reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9",
|
||||
"url": "https://api.github.com/repos/symfony/mailer/zipball/f6ea532250b476bfc1b56699b388a1bdbf168f62",
|
||||
"reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6385,7 +6386,7 @@
|
||||
"description": "Helps sending emails",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/mailer/tree/v7.4.6"
|
||||
"source": "https://github.com/symfony/mailer/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6405,20 +6406,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-25T16:50:00+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mime",
|
||||
"version": "v7.4.7",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/mime.git",
|
||||
"reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1"
|
||||
"reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/mime/zipball/da5ab4fde3f6c88ab06e96185b9922f48b677cd1",
|
||||
"reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1",
|
||||
"url": "https://api.github.com/repos/symfony/mime/zipball/6df02f99998081032da3407a8d6c4e1dcb5d4379",
|
||||
"reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6474,7 +6475,7 @@
|
||||
"mime-type"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/mime/tree/v7.4.7"
|
||||
"source": "https://github.com/symfony/mime/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6494,7 +6495,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-05T15:24:09+00:00"
|
||||
"time": "2026-03-30T14:11:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
@@ -7327,16 +7328,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v7.4.5",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "608476f4604102976d687c483ac63a79ba18cc97"
|
||||
"reference": "60f19cd3badc8de688421e21e4305eba50f8089a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97",
|
||||
"reference": "608476f4604102976d687c483ac63a79ba18cc97",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a",
|
||||
"reference": "60f19cd3badc8de688421e21e4305eba50f8089a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7368,7 +7369,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v7.4.5"
|
||||
"source": "https://github.com/symfony/process/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7388,20 +7389,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-26T15:07:59+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/routing",
|
||||
"version": "v7.4.6",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/routing.git",
|
||||
"reference": "238d749c56b804b31a9bf3e26519d93b65a60938"
|
||||
"reference": "9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938",
|
||||
"reference": "238d749c56b804b31a9bf3e26519d93b65a60938",
|
||||
"url": "https://api.github.com/repos/symfony/routing/zipball/9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b",
|
||||
"reference": "9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7453,7 +7454,7 @@
|
||||
"url"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/routing/tree/v7.4.6"
|
||||
"source": "https://github.com/symfony/routing/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7473,7 +7474,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-25T16:50:00+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
@@ -7564,16 +7565,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v7.4.6",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "9f209231affa85aa930a5e46e6eb03381424b30b"
|
||||
"reference": "114ac57257d75df748eda23dd003878080b8e688"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/9f209231affa85aa930a5e46e6eb03381424b30b",
|
||||
"reference": "9f209231affa85aa930a5e46e6eb03381424b30b",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/114ac57257d75df748eda23dd003878080b8e688",
|
||||
"reference": "114ac57257d75df748eda23dd003878080b8e688",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7631,7 +7632,7 @@
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v7.4.6"
|
||||
"source": "https://github.com/symfony/string/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7651,20 +7652,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-09T09:33:46+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/translation",
|
||||
"version": "v7.4.6",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/translation.git",
|
||||
"reference": "1888cf064399868af3784b9e043240f1d89d25ce"
|
||||
"reference": "33600f8489485425bfcddd0d983391038d3422e7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/translation/zipball/1888cf064399868af3784b9e043240f1d89d25ce",
|
||||
"reference": "1888cf064399868af3784b9e043240f1d89d25ce",
|
||||
"url": "https://api.github.com/repos/symfony/translation/zipball/33600f8489485425bfcddd0d983391038d3422e7",
|
||||
"reference": "33600f8489485425bfcddd0d983391038d3422e7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7731,7 +7732,7 @@
|
||||
"description": "Provides tools to internationalize your application",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/translation/tree/v7.4.6"
|
||||
"source": "https://github.com/symfony/translation/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7751,7 +7752,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-17T07:53:42+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/translation-contracts",
|
||||
@@ -7837,16 +7838,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/uid",
|
||||
"version": "v7.4.4",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/uid.git",
|
||||
"reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36"
|
||||
"reference": "6883ebdf7bf6a12b37519dbc0df62b0222401b56"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36",
|
||||
"reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36",
|
||||
"url": "https://api.github.com/repos/symfony/uid/zipball/6883ebdf7bf6a12b37519dbc0df62b0222401b56",
|
||||
"reference": "6883ebdf7bf6a12b37519dbc0df62b0222401b56",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7891,7 +7892,7 @@
|
||||
"uuid"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/uid/tree/v7.4.4"
|
||||
"source": "https://github.com/symfony/uid/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7911,20 +7912,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-03T23:30:35+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-dumper",
|
||||
"version": "v7.4.6",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/var-dumper.git",
|
||||
"reference": "045321c440ac18347b136c63d2e9bf28a2dc0291"
|
||||
"reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291",
|
||||
"reference": "045321c440ac18347b136c63d2e9bf28a2dc0291",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd",
|
||||
"reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7978,7 +7979,7 @@
|
||||
"dump"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v7.4.6"
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7998,7 +7999,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-15T10:53:20+00:00"
|
||||
"time": "2026-03-30T13:44:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "thecodingmachine/safe",
|
||||
@@ -8955,23 +8956,23 @@
|
||||
},
|
||||
{
|
||||
"name": "nunomaduro/collision",
|
||||
"version": "v8.9.1",
|
||||
"version": "v8.9.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nunomaduro/collision.git",
|
||||
"reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935"
|
||||
"reference": "6eb16883e74fd725ac64dbe81544c961ab448ba5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935",
|
||||
"reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935",
|
||||
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/6eb16883e74fd725ac64dbe81544c961ab448ba5",
|
||||
"reference": "6eb16883e74fd725ac64dbe81544c961ab448ba5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"filp/whoops": "^2.18.4",
|
||||
"nunomaduro/termwind": "^2.4.0",
|
||||
"php": "^8.2.0",
|
||||
"symfony/console": "^7.4.4 || ^8.0.4"
|
||||
"symfony/console": "^7.4.8 || ^8.0.4"
|
||||
},
|
||||
"conflict": {
|
||||
"laravel/framework": "<11.48.0 || >=14.0.0",
|
||||
@@ -8979,12 +8980,12 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^7.8.5",
|
||||
"larastan/larastan": "^3.9.2",
|
||||
"laravel/framework": "^11.48.0 || ^12.52.0",
|
||||
"laravel/pint": "^1.27.1",
|
||||
"orchestra/testbench-core": "^9.12.0 || ^10.9.0",
|
||||
"pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0",
|
||||
"sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0"
|
||||
"larastan/larastan": "^3.9.3",
|
||||
"laravel/framework": "^11.48.0 || ^12.56.0 || ^13.2.0",
|
||||
"laravel/pint": "^1.29.0",
|
||||
"orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.0.0",
|
||||
"pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0",
|
||||
"sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -9047,7 +9048,7 @@
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-17T17:33:08+00:00"
|
||||
"time": "2026-03-31T21:51:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phar-io/manifest",
|
||||
@@ -9169,11 +9170,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.42",
|
||||
"version": "2.1.46",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0",
|
||||
"reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||
"reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -9218,7 +9219,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-17T14:58:32+00:00"
|
||||
"time": "2026-04-01T09:25:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@@ -10841,16 +10842,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/dom-crawler",
|
||||
"version": "v7.4.6",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/dom-crawler.git",
|
||||
"reference": "487ba8fa43da9a8e6503fe939b45ecd96875410e"
|
||||
"reference": "2918e7c2ba964defca1f5b69c6f74886529e2dc8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/487ba8fa43da9a8e6503fe939b45ecd96875410e",
|
||||
"reference": "487ba8fa43da9a8e6503fe939b45ecd96875410e",
|
||||
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2918e7c2ba964defca1f5b69c6f74886529e2dc8",
|
||||
"reference": "2918e7c2ba964defca1f5b69c6f74886529e2dc8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -10889,7 +10890,7 @@
|
||||
"description": "Eases DOM navigation for HTML and XML documents",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/dom-crawler/tree/v7.4.6"
|
||||
"source": "https://github.com/symfony/dom-crawler/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -10909,7 +10910,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-17T07:53:42+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "theseer/tokenizer",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -66,6 +66,7 @@ Here are some general best practices when it comes to creating modules:
|
||||
### Distribution Format
|
||||
|
||||
Modules are expected to be distributed as a compressed ZIP file, where the ZIP contents follow that of a module folder.
|
||||
Contents may optionally be placed within a nested folder inside the ZIP.
|
||||
BookStack provides a `php artisan bookstack:install-module` command which allows modules to be installed from these ZIP files, either from a local path or from a web URL.
|
||||
Currently, there's a hardcoded total filesize limit of 50MB for module contents installed via this method.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user