Merge branch 'development' into release

This commit is contained in:
Dan Brown 2022-02-01 11:51:48 +00:00
commit b62dab32e0
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
66 changed files with 342 additions and 105 deletions

View File

@ -297,6 +297,11 @@ RECYCLE_BIN_LIFETIME=30
# Maximum file size, in megabytes, that can be uploaded to the system.
FILE_UPLOAD_SIZE_LIMIT=50
# Export Page Size
# Primarily used to determine page size of PDF exports.
# Can be 'a4' or 'letter'.
EXPORT_PAGE_SIZE=a4
# Allow <script> tags in page content
# Note, if set to 'true' the page editor may still escape scripts.
ALLOW_CONTENT_SCRIPTS=false

View File

@ -3,10 +3,10 @@ name: phpstan
on:
push:
branches-ignore:
- l10n_master
- l10n_development
pull_request:
branches-ignore:
- l10n_master
- l10n_development
jobs:
build:

View File

@ -3,10 +3,10 @@ name: phpunit
on:
push:
branches-ignore:
- l10n_master
- l10n_development
pull_request:
branches-ignore:
- l10n_master
- l10n_development
jobs:
build:

View File

@ -3,10 +3,10 @@ name: test-migrations
on:
push:
branches-ignore:
- l10n_master
- l10n_development
pull_request:
branches-ignore:
- l10n_master
- l10n_development
jobs:
build:

View File

@ -60,8 +60,11 @@ class OidcJwtSigningKey
*/
protected function loadFromJwkArray(array $jwk)
{
if ($jwk['alg'] !== 'RS256') {
throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$jwk['alg']}");
// 'alg' is optional for a JWK, but we will still attempt to validate if
// it exists otherwise presume it will be compatible.
$alg = $jwk['alg'] ?? null;
if ($jwk['kty'] !== 'RSA' || !(is_null($alg) || $alg === 'RS256')) {
throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$alg}");
}
if (empty($jwk['use'])) {

View File

@ -164,7 +164,9 @@ class OidcProviderSettings
protected function filterKeys(array $keys): array
{
return array_filter($keys, function (array $key) {
return $key['kty'] === 'RSA' && $key['use'] === 'sig' && $key['alg'] === 'RS256';
$alg = $key['alg'] ?? null;
return $key['kty'] === 'RSA' && $key['use'] === 'sig' && (is_null($alg) || $alg === 'RS256');
});
}

View File

@ -146,7 +146,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function attachDefaultRole(): void
{
$roleId = setting('registration-role');
$roleId = intval(setting('registration-role'));
if ($roleId && $this->roles()->where('id', '=', $roleId)->count() === 0) {
$this->roles()->attach($roleId);
}

View File

@ -7,6 +7,10 @@
* Configuration should be altered via the `.env` file or environment variables.
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
$dompdfPaperSizeMap = [
'a4' => 'a4',
'letter' => 'letter',
];
return [
@ -150,7 +154,7 @@ return [
*
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
*/
'default_paper_size' => 'a4',
'default_paper_size' => $dompdfPaperSizeMap[env('EXPORT_PAGE_SIZE', 'a4')] ?? 'a4',
/**
* The default font family.

View File

@ -7,6 +7,10 @@
* Configuration should be altered via the `.env` file or environment variables.
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
$snappyPaperSizeMap = [
'a4' => 'A4',
'letter' => 'Letter',
];
return [
'pdf' => [
@ -14,7 +18,8 @@ return [
'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false),
'timeout' => false,
'options' => [
'outline' => true,
'outline' => true,
'page-size' => $snappyPaperSizeMap[env('EXPORT_PAGE_SIZE', 'a4')] ?? 'A4',
],
'env' => [],
],

View File

@ -3,8 +3,10 @@
namespace BookStack\Console\Commands;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\Rules\Unique;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
@ -19,7 +21,8 @@ class CreateAdmin extends Command
protected $signature = 'bookstack:create-admin
{--email= : The email address for the new admin user}
{--name= : The name of the new admin user}
{--password= : The password to assign to the new admin user}';
{--password= : The password to assign to the new admin user}
{--external-auth-id= : The external authentication system id for the new admin user (SAML2/LDAP/OIDC)}';
/**
* The console command description.
@ -42,28 +45,35 @@ class CreateAdmin extends Command
/**
* Execute the console command.
*
* @throws \BookStack\Exceptions\NotFoundException
* @throws NotFoundException
*
* @return mixed
*/
public function handle()
{
$details = $this->options();
$details = $this->snakeCaseOptions();
if (empty($details['email'])) {
$details['email'] = $this->ask('Please specify an email address for the new admin user');
}
if (empty($details['name'])) {
$details['name'] = $this->ask('Please specify a name for the new admin user');
}
if (empty($details['password'])) {
$details['password'] = $this->ask('Please specify a password for the new admin user (8 characters min)');
if (empty($details['external_auth_id'])) {
$details['password'] = $this->ask('Please specify a password for the new admin user (8 characters min)');
} else {
$details['password'] = Str::random(32);
}
}
$validator = Validator::make($details, [
'email' => ['required', 'email', 'min:5', new Unique('users', 'email')],
'name' => ['required', 'min:2'],
'password' => ['required', Password::default()],
'email' => ['required', 'email', 'min:5', new Unique('users', 'email')],
'name' => ['required', 'min:2'],
'password' => ['required_without:external_auth_id', Password::default()],
'external_auth_id' => ['required_without:password'],
]);
if ($validator->fails()) {
@ -84,4 +94,14 @@ class CreateAdmin extends Command
return SymfonyCommand::SUCCESS;
}
protected function snakeCaseOptions(): array
{
$returnOpts = [];
foreach ($this->options() as $key => $value) {
$returnOpts[str_replace('-', '_', $key)] = $value;
}
return $returnOpts;
}
}

View File

@ -62,6 +62,7 @@ class UserController extends Controller
$this->checkPermission('users-manage');
$authMethod = config('auth.method');
$roles = $this->userRepo->getAllRoles();
$this->setPageTitle(trans('settings.users_add_new'));
return view('users.create', ['authMethod' => $authMethod, 'roles' => $roles]);
}
@ -76,8 +77,9 @@ class UserController extends Controller
{
$this->checkPermission('users-manage');
$validationRules = [
'name' => ['required'],
'email' => ['required', 'email', 'unique:users,email'],
'name' => ['required'],
'email' => ['required', 'email', 'unique:users,email'],
'setting' => ['array'],
];
$authMethod = config('auth.method');
@ -104,6 +106,13 @@ class UserController extends Controller
DB::transaction(function () use ($user, $sendInvite, $request) {
$user->save();
// Save user-specific settings
if ($request->filled('setting')) {
foreach ($request->get('setting') as $key => $value) {
setting()->putUser($user, $key, $value);
}
}
if ($sendInvite) {
$this->inviteService->sendInvitation($user);
}
@ -198,7 +207,7 @@ class UserController extends Controller
$user->external_auth_id = $request->get('external_auth_id');
}
// Save an user-specific settings
// Save user-specific settings
if ($request->filled('setting')) {
foreach ($request->get('setting') as $key => $value) {
setting()->putUser($user, $key, $value);

View File

@ -2,35 +2,33 @@
namespace BookStack\Notifications;
use BookStack\Auth\User;
use Illuminate\Notifications\Messages\MailMessage;
class UserInvite extends MailNotification
{
public $token;
/**
* Create a new notification instance.
*
* @param string $token
*/
public function __construct($token)
public function __construct(string $token)
{
$this->token = $token;
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
*
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
public function toMail(User $notifiable): MailMessage
{
$appName = ['appName' => setting('app-name')];
$language = setting()->getUser($notifiable, 'language');
return $this->newMailMessage()
->subject(trans('auth.user_invite_email_subject', $appName))
->greeting(trans('auth.user_invite_email_greeting', $appName))
->line(trans('auth.user_invite_email_text'))
->action(trans('auth.user_invite_email_action'), url('/register/invite/' . $this->token));
->subject(trans('auth.user_invite_email_subject', $appName, $language))
->greeting(trans('auth.user_invite_email_greeting', $appName, $language))
->line(trans('auth.user_invite_email_text', [], $language))
->action(trans('auth.user_invite_email_action', [], $language), url('/register/invite/' . $this->token));
}
}

View File

@ -1,7 +1,7 @@
# BookStack
[![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest)
[![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
[![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/development/LICENSE)
[![Crowdin](https://badges.crowdin.net/bookstack/localized.svg)](https://crowdin.com/project/bookstack)
[![Discord](https://img.shields.io/static/v1?label=chat&message=discord&color=738adb&logo=discord)](https://discord.gg/ztkBqR2)
[![Repo Stats](https://img.shields.io/static/v1?label=GitHub+project&message=stats&color=f27e3f)](https://gh-stats.bookstackapp.com/)
@ -34,11 +34,14 @@ Big thanks to these companies for supporting the project.
Note: Listed services are not tested, vetted nor supported by the official BookStack project in any manner.
[View all sponsors](https://github.com/sponsors/ssddanbrown).
#### Silver Sponsor
#### Silver Sponsors
<table><tbody><tr>
<td><a href="https://www.diagrams.net/" target="_blank">
<img width="420" src="https://media.githubusercontent.com/media/BookStackApp/website/main/static/images/sponsors/diagramsnet.png" alt="Diagrams.net">
<img width="400" src="https://media.githubusercontent.com/media/BookStackApp/website/main/static/images/sponsors/diagramsnet.png" alt="Diagrams.net">
</a></td>
<td><a href="https://cloudabove.com/hosting" target="_blank">
<img height="100" src="https://raw.githubusercontent.com/BookStackApp/website/main/static/images/sponsors/cloudabove.svg" alt="Cloudabove">
</a></td>
</tr></tbody></table>
@ -79,7 +82,7 @@ Feature releases, and some patch releases, will be accompanied by a post on the
## 🛠️ Development & Testing
All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at its version. Here are the current development requirements:
All development on BookStack is currently done on the `development` branch. When it's time for a release the `development` branch is merged into release with built & minified CSS & JS then tagged at its version. Here are the current development requirements:
* [Node.js](https://nodejs.org/en/) v14.0+
@ -175,9 +178,9 @@ Feel free to create issues to request new features or to report bugs & problems.
Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge. Just because a feature request exists, or is tagged, does not mean that feature would be accepted into the core project.
Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
Pull requests should be created from the `development` branch since they will be merged back into `development` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
The project's code of conduct [can be found here](https://github.com/BookStackApp/BookStack/blob/master/.github/CODE_OF_CONDUCT.md).
The project's code of conduct [can be found here](https://github.com/BookStackApp/BookStack/blob/development/.github/CODE_OF_CONDUCT.md).
## 🔒 Security
@ -185,7 +188,7 @@ Security information for administering a BookStack instance can be found on the
If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](https://updates.bookstackapp.com/signup/bookstack-security-updates).
If you would like to report a security concern, details of doing so can [can be found here](https://github.com/BookStackApp/BookStack/blob/master/.github/SECURITY.md).
If you would like to report a security concern, details of doing so can [can be found here](https://github.com/BookStackApp/BookStack/blob/development/.github/SECURITY.md).
## ♿ Accessibility
@ -203,7 +206,7 @@ The BookStack source is provided under the MIT License. The libraries used by, a
The great people that have worked to build and improve BookStack can [be seen here](https://github.com/BookStackApp/BookStack/graphs/contributors).
The wonderful people that have provided translations, either through GitHub or via Crowdin [can be seen here](https://github.com/BookStackApp/BookStack/blob/master/.github/translators.txt).
The wonderful people that have provided translations, either through GitHub or via Crowdin [can be seen here](https://github.com/BookStackApp/BookStack/blob/development/.github/translators.txt).
These are the great open-source projects used to help build BookStack:

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'عرض القائمة',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Expand Header Menu',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Otvori meni u zaglavlju',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Expand Header Menu',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Aktivní',
'status_inactive' => 'Neaktivní',
'never' => 'Nikdy',
'none' => 'None',
// Header
'header_menu_expand' => 'Rozbalit menu v záhlaví',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Udvid header menu',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Aktiv',
'status_inactive' => 'Inaktiv',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Header-Menü erweitern',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Aktiv',
'status_inactive' => 'Inaktiv',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Header-Menü erweitern',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Expand Header Menu',

View File

@ -64,7 +64,7 @@ return [
'email_not_confirmed_resend_button' => 'Reenviar Correo Electrónico de confirmación',
// User Invite
'user_invite_email_subject' => 'Has sido invitado a unirte a :appName!',
'user_invite_email_subject' => 'As sido invitado a unirte a :appName!',
'user_invite_email_greeting' => 'Se ha creado una cuenta para usted en :appName.',
'user_invite_email_text' => 'Clica en el botón a continuación para ajustar una contraseña y poder acceder:',
'user_invite_email_action' => 'Ajustar la Contraseña de la Cuenta',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Activo',
'status_inactive' => 'Inactive',
'never' => 'Nunca',
'none' => 'Ninguno',
// Header
'header_menu_expand' => 'Expandir el Menú de la Cabecera',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Activo',
'status_inactive' => 'Inactivo',
'never' => 'Nunca',
'none' => 'Ninguno',
// Header
'header_menu_expand' => 'Expandir el Menú de Cabecera',

View File

@ -28,8 +28,8 @@ return [
'create_account' => 'Loo konto',
'already_have_account' => 'Kasutajakonto juba olemas?',
'dont_have_account' => 'Sul ei ole veel kontot?',
'social_login' => 'Social Login',
'social_registration' => 'Social Registration',
'social_login' => 'Sisene läbi sotsiaalmeedia',
'social_registration' => 'Registreeru läbi sotsiaalmeedia',
'social_registration_text' => 'Registreeru ja logi sisse välise teenuse kaudu.',
'register_thanks' => 'Aitäh, et registreerusid!',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Aktiivne',
'status_inactive' => 'Mitteaktiivne',
'never' => 'Mitte kunagi',
'none' => 'None',
// Header
'header_menu_expand' => 'Laienda päisemenüü',

View File

@ -23,7 +23,7 @@ return [
'meta_updated' => 'Muudetud :timeLength',
'meta_updated_name' => 'Muudetud :timeLength kasutaja :user poolt',
'meta_owned_name' => 'Kuulub kasutajale :user',
'entity_select' => 'Entity Select',
'entity_select' => 'Objekti valik',
'images' => 'Pildid',
'my_recent_drafts' => 'Minu hiljutised mustandid',
'my_recently_viewed' => 'Minu viimati vaadatud',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'فعال',
'status_inactive' => 'غیر فعال',
'never' => 'هرگز',
'none' => 'None',
// Header
'header_menu_expand' => 'گسترش منو',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Actif',
'status_inactive' => 'Inactif',
'never' => 'Jamais',
'none' => 'None',
// Header
'header_menu_expand' => 'Développer le menu',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Expand Header Menu',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Proširi izbornik',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Expand Header Menu',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Perluas Menu Tajuk',

View File

@ -74,7 +74,8 @@ return [
'status' => 'Stato',
'status_active' => 'Attivo',
'status_inactive' => 'Inattivo',
'never' => 'Never',
'never' => 'Mai',
'none' => 'None',
// Header
'header_menu_expand' => 'Espandi Menù Intestazione',

View File

@ -246,7 +246,7 @@ return [
'webhooks_events_warning' => 'Tieni presente che questi eventi saranno attivati per tutti gli eventi selezionati, anche se vengono applicati permessi personalizzati. Assicurarsi che l\'uso di questo webhook non esporrà contenuti riservati.',
'webhooks_events_all' => 'Tutti gli eventi di sistema',
'webhooks_name' => 'Nome Webhook',
'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
'webhooks_timeout' => 'Timeout Richiesta Webhook (Secondi)',
'webhooks_endpoint' => 'Endpoint Webhook',
'webhooks_active' => 'Webhook Attivo',
'webhook_events_table_header' => 'Eventi',
@ -255,10 +255,10 @@ return [
'webhooks_delete_confirm' => 'Sei sicuro di voler eliminare questo webhook?',
'webhooks_format_example' => 'Esempio Di Formato Webhook',
'webhooks_format_example_desc' => 'I dati Webhook vengono inviati come richiesta POST all\'endpoint configurato come JSON seguendo il formato sottostante. Le proprietà "related_item" e "url" sono opzionali e dipenderanno dal tipo di evento attivato.',
'webhooks_status' => 'Webhook Status',
'webhooks_last_called' => 'Last Called:',
'webhooks_last_errored' => 'Last Errored:',
'webhooks_last_error_message' => 'Last Error Message:',
'webhooks_status' => 'Stato Webhook',
'webhooks_last_called' => 'Ultima Chiamata:',
'webhooks_last_errored' => 'Ultimo Errore:',
'webhooks_last_error_message' => 'Ultimo Messaggio Di Errore:',
//! If editing translations files directly please ignore this in all

View File

@ -75,6 +75,7 @@ return [
'status_active' => '有効',
'status_inactive' => '無効',
'never' => '該当なし',
'none' => 'None',
// Header
'header_menu_expand' => 'ヘッダーメニューを展開',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Expand Header Menu',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Plėsti antraštės meniu',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Aktīvs',
'status_inactive' => 'Neaktīvs',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Izvērst galvenes izvēlni',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Utvid toppmeny',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Header menu uitvouwen',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Rozwiń menu nagłówka',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Ativo',
'status_inactive' => 'Inativo',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Expandir Menu de Cabeçalho',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Ativo',
'status_inactive' => 'Inativo',
'never' => 'Nunca',
'none' => 'None',
// Header
'header_menu_expand' => 'Expandir Cabeçalho do Menu',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Активен',
'status_inactive' => 'Неактивен',
'never' => 'Никогда',
'none' => 'None',
// Header
'header_menu_expand' => 'Развернуть меню заголовка',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Rozbaliť menu v záhlaví',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Expand Header Menu',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Expandera sidhuvudsmenyn',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Expand Header Menu',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Розгорнути меню заголовка',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => 'Expand Header Menu',

View File

@ -75,6 +75,7 @@ return [
'status_active' => '已激活',
'status_inactive' => '未激活',
'never' => '从未',
'none' => 'None',
// Header
'header_menu_expand' => '展开标头菜单',

View File

@ -75,6 +75,7 @@ return [
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
// Header
'header_menu_expand' => '展開選單',

View File

@ -1,12 +1,29 @@
@extends('layouts.simple')
<!DOCTYPE html>
<html lang="{{ config('app.lang') }}"
dir="{{ config('app.rtl') ? 'rtl' : 'ltr' }}">
<head>
<title>{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}</title>
@section('content')
<!-- Meta -->
<meta name="viewport" content="width=device-width">
<meta charset="utf-8">
<div class="container small mt-xl">
<div class="card content-wrap auto-height">
<h1 class="list-heading">{{ trans('errors.app_down', ['appName' => setting('app-name')]) }}</h1>
<p>{{ trans('errors.back_soon') }}</p>
<!-- Styles and Fonts -->
<link rel="stylesheet" href="{{ versioned_asset('dist/styles.css') }}">
<link rel="stylesheet" media="print" href="{{ versioned_asset('dist/print-styles.css') }}">
<!-- Custom Styles & Head Content -->
@include('common.custom-styles')
@include('common.custom-head')
</head>
<body>
<div id="content" class="block">
<div class="container small mt-xl">
<div class="card content-wrap auto-height">
<h1 class="list-heading">{{ trans('errors.app_down', ['appName' => setting('app-name')]) }}</h1>
<p>{{ trans('errors.back_soon') }}</p>
</div>
</div>
</div>
@stop
</body>
</html>

View File

@ -227,10 +227,11 @@
<label for="setting-registration-role">{{ trans('settings.reg_default_role') }}</label>
<select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
<option value="0" @if(intval(setting('registration-role', '0')) === 0) selected @endif>-- {{ trans('common.none') }} --</option>
@foreach(\BookStack\Auth\Role::all() as $role)
<option value="{{$role->id}}"
data-system-role-name="{{ $role->system_name ?? '' }}"
@if(setting('registration-role', \BookStack\Auth\Role::first()->id) == $role->id) selected @endif
@if(intval(setting('registration-role', '0')) === $role->id) selected @endif
>
{{ $role->display_name }}
</option>

View File

@ -16,6 +16,7 @@
<div class="setting-list">
@include('users.parts.form')
@include('users.parts.language-option-row', ['value' => old('setting.language') ?? config('app.default_locale')])
</div>
<div class="form-group text-right">

View File

@ -35,22 +35,7 @@
</div>
</div>
<div class="grid half gap-xl v-center">
<div>
<label for="user-language" class="setting-list-label">{{ trans('settings.users_preferred_language') }}</label>
<p class="small">
{{ trans('settings.users_preferred_language_desc') }}
</p>
</div>
<div>
<select name="setting[language]" id="user-language">
@foreach(trans('settings.language_select') as $lang => $label)
<option @if(setting()->getUser($user, 'language', config('app.default_locale')) === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
@endforeach
</select>
</div>
</div>
@include('users.parts.language-option-row', ['value' => setting()->getUser($user, 'language', config('app.default_locale'))])
</div>
<div class="text-right">

View File

@ -0,0 +1,18 @@
{{--
$value - Currently selected lanuage value
--}}
<div class="grid half gap-xl v-center">
<div>
<label for="user-language" class="setting-list-label">{{ trans('settings.users_preferred_language') }}</label>
<p class="small">
{{ trans('settings.users_preferred_language_desc') }}
</p>
</div>
<div>
<select name="setting[language]" id="user-language">
@foreach(trans('settings.language_select') as $lang => $label)
<option @if($value === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
@endforeach
</select>
</div>
</div>

View File

@ -3,6 +3,7 @@
namespace Tests\Auth;
use BookStack\Auth\Access\Mfa\MfaSession;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Page;
use BookStack\Notifications\ConfirmEmail;
@ -43,7 +44,10 @@ class AuthTest extends TestCase
public function test_normal_registration()
{
// Set settings and get user instance
$this->setSettings(['registration-enabled' => 'true']);
/** @var Role $registrationRole */
$registrationRole = Role::query()->first();
$this->setSettings(['registration-enabled' => 'true', 'registration-role' => $registrationRole->id]);
/** @var User $user */
$user = User::factory()->make();
// Test form and ensure user is created
@ -57,7 +61,12 @@ class AuthTest extends TestCase
$resp = $this->get('/');
$resp->assertOk();
$resp->assertSee($user->name);
$this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]);
$user = User::query()->where('email', '=', $user->email)->first();
$this->assertEquals(1, $user->roles()->count());
$this->assertEquals($registrationRole->id, $user->roles()->first()->id);
}
public function test_empty_registration_redirects_back_with_errors()
@ -189,6 +198,14 @@ class AuthTest extends TestCase
$this->assertNull(auth()->user());
}
public function test_registration_role_unset_by_default()
{
$this->assertFalse(setting('registration-role'));
$resp = $this->asAdmin()->get('/settings');
$resp->assertElementContains('select[name="setting-registration-role"] option[value="0"][selected]', '-- None --');
}
public function test_logout()
{
$this->asAdmin()->get('/')->assertOk();

View File

@ -318,6 +318,31 @@ class OidcTest extends TestCase
$this->assertCount(4, $transactions);
}
public function test_auth_login_with_autodiscovery_with_keys_that_do_not_have_alg_property()
{
$this->withAutodiscovery();
$keyArray = OidcJwtHelper::publicJwkKeyArray();
unset($keyArray['alg']);
$this->mockHttpClient([
$this->getAutoDiscoveryResponse(),
new Response(200, [
'Content-Type' => 'application/json',
'Cache-Control' => 'no-cache, no-store',
'Pragma' => 'no-cache',
], json_encode([
'keys' => [
$keyArray,
],
])),
]);
$this->assertFalse(auth()->check());
$this->runLogin();
$this->assertTrue(auth()->check());
}
protected function withAutodiscovery()
{
config()->set([

View File

@ -6,6 +6,7 @@ use BookStack\Auth\Access\UserInviteService;
use BookStack\Auth\User;
use BookStack\Notifications\UserInvite;
use Carbon\Carbon;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
@ -34,6 +35,32 @@ class UserInviteTest extends TestCase
]);
}
public function test_user_invite_sent_in_selected_language()
{
Notification::fake();
$admin = $this->getAdmin();
$email = Str::random(16) . '@example.com';
$resp = $this->actingAs($admin)->post('/settings/users/create', [
'name' => 'Barry',
'email' => $email,
'send_invite' => 'true',
'setting' => [
'language' => 'de',
],
]);
$resp->assertRedirect('/settings/users');
$newUser = User::query()->where('email', '=', $email)->orderBy('id', 'desc')->first();
Notification::assertSentTo($newUser, UserInvite::class, function ($notification, $channels, $notifiable) {
/** @var MailMessage $mail */
$mail = $notification->toMail($notifiable);
return 'Du wurdest eingeladen BookStack beizutreten!' === $mail->subject &&
'Ein Konto wurde für Sie auf BookStack erstellt.' === $mail->greeting;
});
}
public function test_invite_set_password()
{
Notification::fake();

View File

@ -1,27 +0,0 @@
<?php
namespace Tests\Commands;
use BookStack\Auth\User;
use Tests\TestCase;
class AddAdminCommandTest extends TestCase
{
public function test_add_admin_command()
{
$exitCode = \Artisan::call('bookstack:create-admin', [
'--email' => 'admintest@example.com',
'--name' => 'Admin Test',
'--password' => 'testing-4',
]);
$this->assertTrue($exitCode === 0, 'Command executed successfully');
$this->assertDatabaseHas('users', [
'email' => 'admintest@example.com',
'name' => 'Admin Test',
]);
$this->assertTrue(User::query()->where('email', '=', 'admintest@example.com')->first()->hasSystemRole('admin'), 'User has admin role as expected');
$this->assertTrue(\Auth::attempt(['email' => 'admintest@example.com', 'password' => 'testing-4']), 'Password stored as expected');
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Tests\Commands;
use BookStack\Auth\User;
use Illuminate\Support\Facades\Auth;
use Tests\TestCase;
class CreateAdminCommandTest extends TestCase
{
public function test_standard_command_usage()
{
$this->artisan('bookstack:create-admin', [
'--email' => 'admintest@example.com',
'--name' => 'Admin Test',
'--password' => 'testing-4',
])->assertExitCode(0);
$this->assertDatabaseHas('users', [
'email' => 'admintest@example.com',
'name' => 'Admin Test',
]);
/** @var User $user */
$user = User::query()->where('email', '=', 'admintest@example.com')->first();
$this->assertTrue($user->hasSystemRole('admin'));
$this->assertTrue(Auth::attempt(['email' => 'admintest@example.com', 'password' => 'testing-4']));
}
public function test_providing_external_auth_id()
{
$this->artisan('bookstack:create-admin', [
'--email' => 'admintest@example.com',
'--name' => 'Admin Test',
'--external-auth-id' => 'xX_admin_Xx',
])->assertExitCode(0);
$this->assertDatabaseHas('users', [
'email' => 'admintest@example.com',
'name' => 'Admin Test',
'external_auth_id' => 'xX_admin_Xx',
]);
/** @var User $user */
$user = User::query()->where('email', '=', 'admintest@example.com')->first();
$this->assertNotEmpty($user->password);
}
public function test_password_required_if_external_auth_id_not_given()
{
$this->artisan('bookstack:create-admin', [
'--email' => 'admintest@example.com',
'--name' => 'Admin Test',
])->expectsQuestion('Please specify a password for the new admin user (8 characters min)', 'hunter2000')
->assertExitCode(0);
$this->assertDatabaseHas('users', [
'email' => 'admintest@example.com',
'name' => 'Admin Test',
]);
$this->assertTrue(Auth::attempt(['email' => 'admintest@example.com', 'password' => 'hunter2000']));
}
}

View File

@ -82,6 +82,20 @@ class ConfigTest extends TestCase
$this->checkEnvConfigResult('ALLOW_UNTRUSTED_SERVER_FETCHING', 'true', 'dompdf.defines.enable_remote', true);
}
public function test_dompdf_paper_size_options_are_limited()
{
$this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'cat', 'dompdf.defines.default_paper_size', 'a4');
$this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'letter', 'dompdf.defines.default_paper_size', 'letter');
$this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'a4', 'dompdf.defines.default_paper_size', 'a4');
}
public function test_snappy_paper_size_options_are_limited()
{
$this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'cat', 'snappy.pdf.options.page-size', 'A4');
$this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'letter', 'snappy.pdf.options.page-size', 'Letter');
$this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'a4', 'snappy.pdf.options.page-size', 'A4');
}
/**
* Set an environment variable of the given name and value
* then check the given config key to see if it matches the given result.

View File

@ -183,6 +183,16 @@ class UserManagementTest extends TestCase
$resp->assertSee('cannot delete the guest user');
}
public function test_user_create_language_reflects_default_system_locale()
{
$langs = ['en', 'fr', 'hr'];
foreach ($langs as $lang) {
config()->set('app.locale', $lang);
$resp = $this->asAdmin()->get('/settings/users/create');
$resp->assertElementExists('select[name="setting[language]"] option[value="' . $lang . '"][selected]');
}
}
public function test_user_creation_is_not_performed_if_the_invitation_sending_fails()
{
/** @var User $user */