diff --git a/.env.example b/.env.example index 05383f04a..a0a1b72e6 100644 --- a/.env.example +++ b/.env.example @@ -41,4 +41,4 @@ MAIL_HOST=localhost MAIL_PORT=1025 MAIL_USERNAME=null MAIL_PASSWORD=null -MAIL_ENCRYPTION=null \ No newline at end of file +MAIL_ENCRYPTION=null diff --git a/.env.example.complete b/.env.example.complete index 49d834ff7..5eb65c27f 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -42,6 +42,14 @@ APP_TIMEZONE=UTC # overrides can be made. Defaults to disabled. APP_THEME=false +# Trusted Proxies +# Used to indicate trust of systems that proxy to the application so +# certain header values (Such as "X-Forwarded-For") can be used from the +# incoming proxy request to provide origin detail. +# Set to an IP address, or multiple comma seperated IP addresses. +# Can alternatively be set to "*" to trust all proxy addresses. +APP_PROXIES=null + # Database details # Host can contain a port (localhost:3306) or a separate DB_PORT option can be used. DB_HOST=localhost diff --git a/.github/translators.txt b/.github/translators.txt index cf844fbff..78dccde42 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -184,5 +184,9 @@ Frost-ZX :: Chinese Simplified Kuzma Simonov (ovmach) :: Russian Vojtěch Krystek (acantophis) :: Czech Michał Lipok (mLipok) :: Polish -Nicolas Pawlak (Mikolajek) :: French +Nicolas Pawlak (Mikolajek) :: French; Polish; German Thomas Hansen (thomasdk81) :: Danish +Hl2run :: Slovak +Ngo Tri Hoai (trihoai) :: Vietnamese +Atalonica :: Catalan +慕容潭谈 (591442386) :: Chinese Simplified diff --git a/app/Actions/ActivityService.php b/app/Actions/ActivityService.php index dce7dc7b2..bc7a6b6b7 100644 --- a/app/Actions/ActivityService.php +++ b/app/Actions/ActivityService.php @@ -55,9 +55,12 @@ class ActivityService */ protected function newActivityForUser(string $type): Activity { + $ip = request()->ip() ?? ''; + return $this->activity->newInstance()->forceFill([ 'type' => strtolower($type), 'user_id' => user()->id, + 'ip' => config('app.env') === 'demo' ? '127.0.0.1' : $ip, ]); } diff --git a/app/Actions/Comment.php b/app/Actions/Comment.php index ef390939e..34fd84709 100644 --- a/app/Actions/Comment.php +++ b/app/Actions/Comment.php @@ -7,10 +7,11 @@ use BookStack\Traits\HasCreatorAndUpdater; use Illuminate\Database\Eloquent\Relations\MorphTo; /** - * @property string text - * @property string html - * @property int|null parent_id - * @property int local_id + * @property int $id + * @property string $text + * @property string $html + * @property int|null $parent_id + * @property int $local_id */ class Comment extends Model { diff --git a/app/Actions/TagRepo.php b/app/Actions/TagRepo.php index ca65b78e8..b892efe57 100644 --- a/app/Actions/TagRepo.php +++ b/app/Actions/TagRepo.php @@ -4,8 +4,8 @@ namespace BookStack\Actions; use BookStack\Auth\Permissions\PermissionService; use BookStack\Entities\Models\Entity; -use DB; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; class TagRepo { diff --git a/app/Auth/Access/Mfa/TotpService.php b/app/Auth/Access/Mfa/TotpService.php index a3e9fc827..0d9bd37ce 100644 --- a/app/Auth/Access/Mfa/TotpService.php +++ b/app/Auth/Access/Mfa/TotpService.php @@ -54,7 +54,7 @@ class TotpService return (new Writer( new ImageRenderer( - new RendererStyle(192, 0, null, null, $color), + new RendererStyle(192, 4, null, null, $color), new SvgImageBackEnd() ) ))->writeString($url); diff --git a/app/Auth/Access/SocialAuthService.php b/app/Auth/Access/SocialAuthService.php index 8cf243fe7..d165e76b1 100644 --- a/app/Auth/Access/SocialAuthService.php +++ b/app/Auth/Access/SocialAuthService.php @@ -141,7 +141,7 @@ class SocialAuthService // When a user is not logged in and a matching SocialAccount exists, // Simply log the user into the application. if (!$isLoggedIn && $socialAccount !== null) { - $this->loginService->login($socialAccount->user, $socialAccount); + $this->loginService->login($socialAccount->user, $socialDriver); return redirect()->intended('/'); } diff --git a/app/Auth/Permissions/PermissionService.php b/app/Auth/Permissions/PermissionService.php index f84f51894..139725339 100644 --- a/app/Auth/Permissions/PermissionService.php +++ b/app/Auth/Permissions/PermissionService.php @@ -603,7 +603,7 @@ class PermissionService /** * Filter items that have entities set as a polymorphic relation. * - * @param Builder|\Illuminate\Database\Query\Builder $query + * @param Builder|QueryBuilder $query */ public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view') { @@ -611,9 +611,10 @@ class PermissionService $q = $query->where(function ($query) use ($tableDetails, $action) { $query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) { + /** @var Builder $permissionQuery */ $permissionQuery->select(['role_id'])->from('joint_permissions') - ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) - ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn']) + ->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) + ->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn']) ->where('action', '=', $action) ->whereIn('role_id', $this->getCurrentUserRoles()) ->where(function (QueryBuilder $query) { @@ -639,8 +640,9 @@ class PermissionService $q = $query->where(function ($query) use ($tableDetails, $morphClass) { $query->where(function ($query) use (&$tableDetails, $morphClass) { $query->whereExists(function ($permissionQuery) use (&$tableDetails, $morphClass) { + /** @var Builder $permissionQuery */ $permissionQuery->select('id')->from('joint_permissions') - ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) + ->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) ->where('entity_type', '=', $morphClass) ->where('action', '=', 'view') ->whereIn('role_id', $this->getCurrentUserRoles()) diff --git a/app/Auth/Role.php b/app/Auth/Role.php index dcd960948..46921caeb 100644 --- a/app/Auth/Role.php +++ b/app/Auth/Role.php @@ -13,12 +13,13 @@ use Illuminate\Database\Eloquent\Relations\HasMany; /** * Class Role. * - * @property int $id - * @property string $display_name - * @property string $description - * @property string $external_auth_id - * @property string $system_name - * @property bool $mfa_enforced + * @property int $id + * @property string $display_name + * @property string $description + * @property string $external_auth_id + * @property string $system_name + * @property bool $mfa_enforced + * @property Collection $users */ class Role extends Model implements Loggable { diff --git a/app/Auth/UserRepo.php b/app/Auth/UserRepo.php index e1a040fc2..6d48f1240 100644 --- a/app/Auth/UserRepo.php +++ b/app/Auth/UserRepo.php @@ -15,7 +15,7 @@ use Exception; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Pagination\LengthAwarePaginator; -use Log; +use Illuminate\Support\Facades\Log; class UserRepo { diff --git a/app/Config/database.php b/app/Config/database.php index 7fb51a13b..0c6966095 100644 --- a/app/Config/database.php +++ b/app/Config/database.php @@ -69,7 +69,10 @@ return [ 'port' => $mysql_port, 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', + // Prefixes are only semi-supported and may be unstable + // since they are not tested as part of our automated test suite. + // If used, the prefix should not be changed otherwise you will likely receive errors. + 'prefix' => env('DB_TABLE_PREFIX', ''), 'prefix_indexes' => true, 'strict' => false, 'engine' => null, diff --git a/app/Console/Commands/RegenerateSearch.php b/app/Console/Commands/RegenerateSearch.php index 3dc3ec0af..50e81a2b8 100644 --- a/app/Console/Commands/RegenerateSearch.php +++ b/app/Console/Commands/RegenerateSearch.php @@ -3,8 +3,8 @@ namespace BookStack\Console\Commands; use BookStack\Entities\Tools\SearchIndex; -use DB; use Illuminate\Console\Command; +use Illuminate\Support\Facades\DB; class RegenerateSearch extends Command { diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index df30c1c71..1e4591bd7 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -12,9 +12,12 @@ use Illuminate\Support\Collection; /** * Class Book. * - * @property string $description - * @property int $image_id - * @property Image|null $cover + * @property string $description + * @property int $image_id + * @property Image|null $cover + * @property \Illuminate\Database\Eloquent\Collection $chapters + * @property \Illuminate\Database\Eloquent\Collection $pages + * @property \Illuminate\Database\Eloquent\Collection $directPages */ class Book extends Entity implements HasCoverImage { diff --git a/app/Entities/Tools/PageContent.php b/app/Entities/Tools/PageContent.php index b4cc1b81c..661c554da 100644 --- a/app/Entities/Tools/PageContent.php +++ b/app/Entities/Tools/PageContent.php @@ -316,6 +316,7 @@ class PageContent } // Find page and skip this if page not found + /** @var ?Page $matchedPage */ $matchedPage = Page::visible()->find($pageId); if ($matchedPage === null) { $html = str_replace($fullMatch, '', $html); diff --git a/app/Exceptions/StoppedAuthenticationException.php b/app/Exceptions/StoppedAuthenticationException.php index ef7f24017..d10a6da5e 100644 --- a/app/Exceptions/StoppedAuthenticationException.php +++ b/app/Exceptions/StoppedAuthenticationException.php @@ -55,7 +55,7 @@ class StoppedAuthenticationException extends \Exception implements Responsable ], 401); } - if (session()->get('sent-email-confirmation') === true) { + if (session()->pull('sent-email-confirmation') === true) { return redirect('/register/confirm'); } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index bd1ffeac2..209827d6d 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -12,7 +12,7 @@ use BookStack\Http\Controllers\Controller; use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; -use Validator; +use Illuminate\Support\Facades\Validator; class RegisterController extends Controller { diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 6706de575..5451c0abf 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -96,9 +96,10 @@ class HomeController extends Controller if ($homepageOption === 'page') { $homepageSetting = setting('app-homepage', '0:'); $id = intval(explode(':', $homepageSetting)[0]); + /** @var Page $customHomepage */ $customHomepage = Page::query()->where('draft', '=', false)->findOrFail($id); $pageContent = new PageContent($customHomepage); - $customHomepage->html = $pageContent->render(true); + $customHomepage->html = $pageContent->render(false); return view('home.specific-page', array_merge($commonData, ['customHomepage' => $customHomepage])); } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 59704f4a1..8334bb179 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,7 +2,6 @@ namespace BookStack\Providers; -use Blade; use BookStack\Auth\Access\LoginService; use BookStack\Auth\Access\SocialAuthService; use BookStack\Entities\BreadcrumbsViewComposer; @@ -15,11 +14,12 @@ use BookStack\Settings\SettingService; use BookStack\Util\CspService; use Illuminate\Contracts\Cache\Repository; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; use Laravel\Socialite\Contracts\Factory as SocialiteFactory; -use Schema; -use URL; class AppServiceProvider extends ServiceProvider { diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 71b7ab016..37b0e83b9 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -2,7 +2,6 @@ namespace BookStack\Providers; -use Auth; use BookStack\Api\ApiTokenGuard; use BookStack\Auth\Access\ExternalBaseUserProvider; use BookStack\Auth\Access\Guards\LdapSessionGuard; @@ -10,6 +9,7 @@ use BookStack\Auth\Access\Guards\Saml2SessionGuard; use BookStack\Auth\Access\LdapService; use BookStack\Auth\Access\LoginService; use BookStack\Auth\Access\RegistrationService; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\ServiceProvider; class AuthServiceProvider extends ServiceProvider diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 8f0dab400..b60443a45 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -3,7 +3,7 @@ namespace BookStack\Providers; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; -use Route; +use Illuminate\Support\Facades\Route; class RouteServiceProvider extends ServiceProvider { diff --git a/app/Uploads/AttachmentService.php b/app/Uploads/AttachmentService.php index 298d53a04..b4cb1b88b 100644 --- a/app/Uploads/AttachmentService.php +++ b/app/Uploads/AttachmentService.php @@ -7,8 +7,8 @@ use Exception; use Illuminate\Contracts\Filesystem\Factory as FileSystem; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; -use Log; use Symfony\Component\HttpFoundation\File\UploadedFile; class AttachmentService diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index 51ddf9bdc..2c38c24f4 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -3,7 +3,6 @@ namespace BookStack\Uploads; use BookStack\Exceptions\ImageUploadException; -use DB; use ErrorException; use Exception; use Illuminate\Contracts\Cache\Repository as Cache; @@ -11,6 +10,7 @@ use Illuminate\Contracts\Filesystem\Factory as FileSystem; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; use Illuminate\Contracts\Filesystem\Filesystem as Storage; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; use Intervention\Image\Exception\NotSupportedException; use Intervention\Image\ImageManager; diff --git a/composer.json b/composer.json index 7362a085d..31ecbef84 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "facade/ignition": "^1.16.4", "fideloper/proxy": "^4.4.1", "intervention/image": "^2.5.1", - "laravel/framework": "^6.20.16", + "laravel/framework": "^6.20.33", "laravel/socialite": "^5.1", "league/commonmark": "^1.5", "league/flysystem-aws-s3-v3": "^1.0.29", @@ -41,9 +41,9 @@ "barryvdh/laravel-debugbar": "^3.5.1", "barryvdh/laravel-ide-helper": "^2.8.2", "fakerphp/faker": "^1.13.0", - "laravel/browser-kit-testing": "^5.2", "mockery/mockery": "^1.3.3", - "phpunit/phpunit": "^9.5.3" + "phpunit/phpunit": "^9.5.3", + "symfony/dom-crawler": "^5.3" }, "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index b2ad6b691..d267d13d6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,23 +4,74 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4d845f3c8b77c8d73bf92c9223ddd805", + "content-hash": "10825887b8f66d1d412b92bcc0ca864f", "packages": [ { - "name": "aws/aws-sdk-php", - "version": "3.191.8", + "name": "aws/aws-crt-php", + "version": "v1.0.2", "source": { "type": "git", - "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "949feb83cc0db46f07b12aa3128d47be3b9cca63" + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "3942776a8c99209908ee0b287746263725685732" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/949feb83cc0db46f07b12aa3128d47be3b9cca63", - "reference": "949feb83cc0db46f07b12aa3128d47be3b9cca63", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/3942776a8c99209908ee0b287746263725685732", + "reference": "3942776a8c99209908ee0b287746263725685732", "shasum": "" }, "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|^5.4.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.0.2" + }, + "time": "2021-09-03T22:57:30+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.194.1", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "67bdee05acef9e8ad60098090996690b49babd09" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/67bdee05acef9e8ad60098090996690b49babd09", + "reference": "67bdee05acef9e8ad60098090996690b49babd09", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.0.2", "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", @@ -92,9 +143,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.191.8" + "source": "https://github.com/aws/aws-sdk-php/tree/3.194.1" }, - "time": "2021-08-31T18:18:02+00:00" + "time": "2021-09-17T18:15:42+00:00" }, { "name": "bacon/bacon-qr-code", @@ -428,16 +479,16 @@ }, { "name": "doctrine/dbal", - "version": "2.13.2", + "version": "2.13.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "8dd39d2ead4409ce652fd4f02621060f009ea5e4" + "reference": "0d7adf4cadfee6f70850e5b163e6cdd706417838" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/8dd39d2ead4409ce652fd4f02621060f009ea5e4", - "reference": "8dd39d2ead4409ce652fd4f02621060f009ea5e4", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/0d7adf4cadfee6f70850e5b163e6cdd706417838", + "reference": "0d7adf4cadfee6f70850e5b163e6cdd706417838", "shasum": "" }, "require": { @@ -449,13 +500,14 @@ }, "require-dev": { "doctrine/coding-standard": "9.0.0", - "jetbrains/phpstorm-stubs": "2020.2", - "phpstan/phpstan": "0.12.81", + "jetbrains/phpstorm-stubs": "2021.1", + "phpstan/phpstan": "0.12.96", "phpunit/phpunit": "^7.5.20|^8.5|9.5.5", + "psalm/plugin-phpunit": "0.16.1", "squizlabs/php_codesniffer": "3.6.0", "symfony/cache": "^4.4", "symfony/console": "^2.0.5|^3.0|^4.0|^5.0", - "vimeo/psalm": "4.6.4" + "vimeo/psalm": "4.10.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -516,7 +568,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/2.13.2" + "source": "https://github.com/doctrine/dbal/tree/2.13.3" }, "funding": [ { @@ -532,7 +584,7 @@ "type": "tidelift" } ], - "time": "2021-06-18T21:48:39+00:00" + "time": "2021-09-12T19:11:48+00:00" }, { "name": "doctrine/deprecations", @@ -1052,16 +1104,16 @@ }, { "name": "facade/flare-client-php", - "version": "1.8.1", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/facade/flare-client-php.git", - "reference": "47b639dc02bcfdfc4ebb83de703856fa01e35f5f" + "reference": "b2adf1512755637d0cef4f7d1b54301325ac78ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/facade/flare-client-php/zipball/47b639dc02bcfdfc4ebb83de703856fa01e35f5f", - "reference": "47b639dc02bcfdfc4ebb83de703856fa01e35f5f", + "url": "https://api.github.com/repos/facade/flare-client-php/zipball/b2adf1512755637d0cef4f7d1b54301325ac78ed", + "reference": "b2adf1512755637d0cef4f7d1b54301325ac78ed", "shasum": "" }, "require": { @@ -1105,7 +1157,7 @@ ], "support": { "issues": "https://github.com/facade/flare-client-php/issues", - "source": "https://github.com/facade/flare-client-php/tree/1.8.1" + "source": "https://github.com/facade/flare-client-php/tree/1.9.1" }, "funding": [ { @@ -1113,7 +1165,7 @@ "type": "github" } ], - "time": "2021-05-31T19:23:29+00:00" + "time": "2021-09-13T12:16:46+00:00" }, { "name": "facade/ignition", @@ -1762,16 +1814,16 @@ }, { "name": "laravel/framework", - "version": "v6.20.33", + "version": "v6.20.34", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "49aa211f2dd1d419bfd9dbbd6f590d57a1dfda58" + "reference": "72a6da88c90cee793513b3fe49cf0fcb368eefa0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/49aa211f2dd1d419bfd9dbbd6f590d57a1dfda58", - "reference": "49aa211f2dd1d419bfd9dbbd6f590d57a1dfda58", + "url": "https://api.github.com/repos/laravel/framework/zipball/72a6da88c90cee793513b3fe49cf0fcb368eefa0", + "reference": "72a6da88c90cee793513b3fe49cf0fcb368eefa0", "shasum": "" }, "require": { @@ -1911,7 +1963,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2021-08-31T13:56:36+00:00" + "time": "2021-09-07T13:28:55+00:00" }, { "name": "laravel/socialite", @@ -2230,16 +2282,16 @@ }, { "name": "league/html-to-markdown", - "version": "5.0.0", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/thephpleague/html-to-markdown.git", - "reference": "c4dbebbebe0fe454b6b38e6c683a977615bd7dc2" + "reference": "e5600a2c5ce7b7571b16732c7086940f56f7abec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/c4dbebbebe0fe454b6b38e6c683a977615bd7dc2", - "reference": "c4dbebbebe0fe454b6b38e6c683a977615bd7dc2", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/e5600a2c5ce7b7571b16732c7086940f56f7abec", + "reference": "e5600a2c5ce7b7571b16732c7086940f56f7abec", "shasum": "" }, "require": { @@ -2295,7 +2347,7 @@ ], "support": { "issues": "https://github.com/thephpleague/html-to-markdown/issues", - "source": "https://github.com/thephpleague/html-to-markdown/tree/5.0.0" + "source": "https://github.com/thephpleague/html-to-markdown/tree/5.0.1" }, "funding": [ { @@ -2311,11 +2363,11 @@ "type": "github" }, { - "url": "https://www.patreon.com/colinodell", - "type": "patreon" + "url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown", + "type": "tidelift" } ], - "time": "2021-03-29T01:29:08+00:00" + "time": "2021-09-17T20:00:27+00:00" }, { "name": "league/mime-type-detection", @@ -2451,24 +2503,24 @@ }, { "name": "monolog/monolog", - "version": "2.3.2", + "version": "2.3.4", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "71312564759a7db5b789296369c1a264efc43aad" + "reference": "437e7a1c50044b92773b361af77620efb76fff59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/71312564759a7db5b789296369c1a264efc43aad", - "reference": "71312564759a7db5b789296369c1a264efc43aad", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/437e7a1c50044b92773b361af77620efb76fff59", + "reference": "437e7a1c50044b92773b361af77620efb76fff59", "shasum": "" }, "require": { "php": ">=7.2", - "psr/log": "^1.0.1" + "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "provide": { - "psr/log-implementation": "1.0.0" + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" }, "require-dev": { "aws/aws-sdk-php": "^2.4.9 || ^3.0", @@ -2483,7 +2535,7 @@ "phpunit/phpunit": "^8.5", "predis/predis": "^1.1", "rollbar/rollbar": "^1.3", - "ruflin/elastica": ">=0.90 <7.0.1", + "ruflin/elastica": ">=0.90@dev", "swiftmailer/swiftmailer": "^5.3|^6.0" }, "suggest": { @@ -2491,8 +2543,11 @@ "doctrine/couchdb": "Allow sending log messages to a CouchDB server", "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", "ext-mbstring": "Allow to work properly with unicode symbols", "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", @@ -2531,7 +2586,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/2.3.2" + "source": "https://github.com/Seldaek/monolog/tree/2.3.4" }, "funding": [ { @@ -2543,7 +2598,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T07:42:52+00:00" + "time": "2021-09-15T11:27:21+00:00" }, { "name": "mtdowling/jmespath.php", @@ -2608,16 +2663,16 @@ }, { "name": "nesbot/carbon", - "version": "2.52.0", + "version": "2.53.1", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "369c0e2737c56a0f39c946dd261855255a6fccbe" + "reference": "f4655858a784988f880c1b8c7feabbf02dfdf045" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/369c0e2737c56a0f39c946dd261855255a6fccbe", - "reference": "369c0e2737c56a0f39c946dd261855255a6fccbe", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/f4655858a784988f880c1b8c7feabbf02dfdf045", + "reference": "f4655858a784988f880c1b8c7feabbf02dfdf045", "shasum": "" }, "require": { @@ -2629,7 +2684,7 @@ }, "require-dev": { "doctrine/orm": "^2.7", - "friendsofphp/php-cs-fixer": "^2.14 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.0", "kylekatarnls/multi-tester": "^2.0", "phpmd/phpmd": "^2.9", "phpstan/extension-installer": "^1.0", @@ -2698,7 +2753,7 @@ "type": "tidelift" } ], - "time": "2021-08-14T19:10:52+00:00" + "time": "2021-09-06T09:29:23+00:00" }, { "name": "nunomaduro/collision", @@ -4492,20 +4547,20 @@ }, { "name": "symfony/css-selector", - "version": "v4.4.27", + "version": "v5.3.4", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "5194f18bd80d106f11efa8f7cd0fbdcc3af96ce6" + "reference": "7fb120adc7f600a59027775b224c13a33530dd90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/5194f18bd80d106f11efa8f7cd0fbdcc3af96ce6", - "reference": "5194f18bd80d106f11efa8f7cd0fbdcc3af96ce6", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/7fb120adc7f600a59027775b224c13a33530dd90", + "reference": "7fb120adc7f600a59027775b224c13a33530dd90", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", "symfony/polyfill-php80": "^1.16" }, "type": "library", @@ -4538,7 +4593,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v4.4.27" + "source": "https://github.com/symfony/css-selector/tree/v5.3.4" }, "funding": [ { @@ -4554,7 +4609,7 @@ "type": "tidelift" } ], - "time": "2021-07-21T12:19:41+00:00" + "time": "2021-07-21T12:38:00+00:00" }, { "name": "symfony/debug", @@ -6884,16 +6939,16 @@ }, { "name": "composer/composer", - "version": "2.1.6", + "version": "2.1.8", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "e5cac5f9d2354d08b67f1d21c664ae70d748c603" + "reference": "24d38e9686092de05214cafa187dc282a5d89497" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/e5cac5f9d2354d08b67f1d21c664ae70d748c603", - "reference": "e5cac5f9d2354d08b67f1d21c664ae70d748c603", + "url": "https://api.github.com/repos/composer/composer/zipball/24d38e9686092de05214cafa187dc282a5d89497", + "reference": "24d38e9686092de05214cafa187dc282a5d89497", "shasum": "" }, "require": { @@ -6962,7 +7017,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", - "source": "https://github.com/composer/composer/tree/2.1.6" + "source": "https://github.com/composer/composer/tree/2.1.8" }, "funding": [ { @@ -6978,7 +7033,7 @@ "type": "tidelift" } ], - "time": "2021-08-19T15:11:08+00:00" + "time": "2021-09-15T11:55:15+00:00" }, { "name": "composer/metadata-minifier", @@ -7344,21 +7399,21 @@ }, { "name": "fakerphp/faker", - "version": "v1.15.0", + "version": "v1.16.0", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "89c6201c74db25fa759ff16e78a4d8f32547770e" + "reference": "271d384d216e5e5c468a6b28feedf95d49f83b35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/89c6201c74db25fa759ff16e78a4d8f32547770e", - "reference": "89c6201c74db25fa759ff16e78a4d8f32547770e", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/271d384d216e5e5c468a6b28feedf95d49f83b35", + "reference": "271d384d216e5e5c468a6b28feedf95d49f83b35", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", - "psr/container": "^1.0", + "psr/container": "^1.0 || ^2.0", "symfony/deprecation-contracts": "^2.2" }, "conflict": { @@ -7378,7 +7433,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "v1.15-dev" + "dev-main": "v1.16-dev" } }, "autoload": { @@ -7403,9 +7458,9 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.15.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.16.0" }, - "time": "2021-07-06T20:39:40+00:00" + "time": "2021-09-06T14:53:37+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -7528,71 +7583,6 @@ }, "time": "2021-07-22T09:24:00+00:00" }, - { - "name": "laravel/browser-kit-testing", - "version": "v5.2.0", - "source": { - "type": "git", - "url": "https://github.com/laravel/browser-kit-testing.git", - "reference": "fa0efb279c009e2a276f934f8aff946caf66edc7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/fa0efb279c009e2a276f934f8aff946caf66edc7", - "reference": "fa0efb279c009e2a276f934f8aff946caf66edc7", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "illuminate/contracts": "~5.7.0|~5.8.0|^6.0", - "illuminate/database": "~5.7.0|~5.8.0|^6.0", - "illuminate/http": "~5.7.0|~5.8.0|^6.0", - "illuminate/support": "~5.7.0|~5.8.0|^6.0", - "mockery/mockery": "^1.0", - "php": "^7.1.3|^8.0", - "phpunit/phpunit": "^7.5|^8.0|^9.3", - "symfony/console": "^4.2", - "symfony/css-selector": "^4.2", - "symfony/dom-crawler": "^4.2", - "symfony/http-foundation": "^4.2", - "symfony/http-kernel": "^4.2" - }, - "require-dev": { - "laravel/framework": "~5.7.0|~5.8.0|^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "Laravel\\BrowserKitTesting\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "Provides backwards compatibility for BrowserKit testing in the latest Laravel release.", - "keywords": [ - "laravel", - "testing" - ], - "support": { - "issues": "https://github.com/laravel/browser-kit-testing/issues", - "source": "https://github.com/laravel/browser-kit-testing/tree/v5.2.0" - }, - "time": "2020-10-30T08:49:09+00:00" - }, { "name": "maximebf/debugbar", "version": "v1.17.1", @@ -7660,16 +7650,16 @@ }, { "name": "mockery/mockery", - "version": "1.4.3", + "version": "1.4.4", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "d1339f64479af1bee0e82a0413813fe5345a54ea" + "reference": "e01123a0e847d52d186c5eb4b9bf58b0c6d00346" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/d1339f64479af1bee0e82a0413813fe5345a54ea", - "reference": "d1339f64479af1bee0e82a0413813fe5345a54ea", + "url": "https://api.github.com/repos/mockery/mockery/zipball/e01123a0e847d52d186c5eb4b9bf58b0c6d00346", + "reference": "e01123a0e847d52d186c5eb4b9bf58b0c6d00346", "shasum": "" }, "require": { @@ -7726,9 +7716,9 @@ ], "support": { "issues": "https://github.com/mockery/mockery/issues", - "source": "https://github.com/mockery/mockery/tree/1.4.3" + "source": "https://github.com/mockery/mockery/tree/1.4.4" }, - "time": "2021-02-24T09:51:49+00:00" + "time": "2021-09-13T15:28:59+00:00" }, { "name": "myclabs/deep-copy", @@ -8066,16 +8056,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.4.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + "reference": "30f38bffc6f24293dadd1823936372dfa9e86e2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/30f38bffc6f24293dadd1823936372dfa9e86e2f", + "reference": "30f38bffc6f24293dadd1823936372dfa9e86e2f", "shasum": "" }, "require": { @@ -8083,7 +8073,8 @@ "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "ext-tokenizer": "*" + "ext-tokenizer": "*", + "psalm/phar": "^4.8" }, "type": "library", "extra": { @@ -8109,39 +8100,39 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.0" }, - "time": "2020-09-17T18:55:26+00:00" + "time": "2021-09-17T15:28:14+00:00" }, { "name": "phpspec/prophecy", - "version": "1.13.0", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" + "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e", + "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.1", + "php": "^7.2 || ~8.0, <8.2", "phpdocumentor/reflection-docblock": "^5.2", "sebastian/comparator": "^3.0 || ^4.0", "sebastian/recursion-context": "^3.0 || ^4.0" }, "require-dev": { - "phpspec/phpspec": "^6.0", + "phpspec/phpspec": "^6.0 || ^7.0", "phpunit/phpunit": "^8.0 || ^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { @@ -8176,29 +8167,29 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.13.0" + "source": "https://github.com/phpspec/prophecy/tree/1.14.0" }, - "time": "2021-03-17T13:42:18+00:00" + "time": "2021-09-10T09:02:12+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.6", + "version": "9.2.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f6293e1b30a2354e8428e004689671b83871edde" + "reference": "d4c798ed8d51506800b441f7a13ecb0f76f12218" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde", - "reference": "f6293e1b30a2354e8428e004689671b83871edde", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d4c798ed8d51506800b441f7a13ecb0f76f12218", + "reference": "d4c798ed8d51506800b441f7a13ecb0f76f12218", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.10.2", + "nikic/php-parser": "^4.12.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -8247,7 +8238,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.7" }, "funding": [ { @@ -8255,7 +8246,7 @@ "type": "github" } ], - "time": "2021-03-28T07:26:59+00:00" + "time": "2021-09-17T05:39:03+00:00" }, { "name": "phpunit/php-file-iterator", @@ -9728,20 +9719,21 @@ }, { "name": "symfony/dom-crawler", - "version": "v4.4.30", + "version": "v5.3.7", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "4632ae3567746c7e915c33c67a2fb6ab746090c4" + "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/4632ae3567746c7e915c33c67a2fb6ab746090c4", - "reference": "4632ae3567746c7e915c33c67a2fb6ab746090c4", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c7eef3a60ccfdd8eafe07f81652e769ac9c7146c", + "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.16" @@ -9751,7 +9743,7 @@ }, "require-dev": { "masterminds/html5": "^2.6", - "symfony/css-selector": "^3.4|^4.0|^5.0" + "symfony/css-selector": "^4.4|^5.0" }, "suggest": { "symfony/css-selector": "" @@ -9782,7 +9774,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v4.4.30" + "source": "https://github.com/symfony/dom-crawler/tree/v5.3.7" }, "funding": [ { @@ -9798,7 +9790,7 @@ "type": "tidelift" } ], - "time": "2021-08-28T15:40:01+00:00" + "time": "2021-08-29T19:32:13+00:00" }, { "name": "symfony/filesystem", diff --git a/database/migrations/2016_04_20_192649_create_joint_permissions_table.php b/database/migrations/2016_04_20_192649_create_joint_permissions_table.php index 5b43c7d54..8c3d9124c 100644 --- a/database/migrations/2016_04_20_192649_create_joint_permissions_table.php +++ b/database/migrations/2016_04_20_192649_create_joint_permissions_table.php @@ -2,6 +2,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Str; class CreateJointPermissionsTable extends Migration { @@ -53,7 +54,7 @@ class CreateJointPermissionsTable extends Migration // Ensure unique name while (DB::table('roles')->where('name', '=', $publicRoleData['display_name'])->count() > 0) { - $publicRoleData['display_name'] = $publicRoleData['display_name'] . str_random(2); + $publicRoleData['display_name'] = $publicRoleData['display_name'] . Str::random(2); } $publicRoleId = DB::table('roles')->insertGetId($publicRoleData); diff --git a/database/migrations/2021_09_26_044614_add_activities_ip_column.php b/database/migrations/2021_09_26_044614_add_activities_ip_column.php new file mode 100644 index 000000000..68391b1c2 --- /dev/null +++ b/database/migrations/2021_09_26_044614_add_activities_ip_column.php @@ -0,0 +1,32 @@ +string('ip', 45)->after('user_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('activities', function (Blueprint $table) { + $table->dropColumn('ip'); + }); + } +} diff --git a/resources/lang/ar/settings.php b/resources/lang/ar/settings.php index 05cf9e235..2ceb849bc 100755 --- a/resources/lang/ar/settings.php +++ b/resources/lang/ar/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'المستخدم', 'audit_table_event' => 'الحدث', 'audit_table_related' => 'العنصر أو التفاصيل ذات الصلة', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'تاريخ النشاط', 'audit_date_from' => 'نطاق التاريخ من', 'audit_date_to' => 'نطاق التاريخ إلى', diff --git a/resources/lang/bg/settings.php b/resources/lang/bg/settings.php index 0cf676851..5c1e1c903 100644 --- a/resources/lang/bg/settings.php +++ b/resources/lang/bg/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Потребител', 'audit_table_event' => 'Събитие', 'audit_table_related' => 'Related Item or Detail', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Дата на активност', 'audit_date_from' => 'Време от', 'audit_date_to' => 'Време до', diff --git a/resources/lang/bs/settings.php b/resources/lang/bs/settings.php index 4c1ae1345..0ab168b66 100644 --- a/resources/lang/bs/settings.php +++ b/resources/lang/bs/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'User', 'audit_table_event' => 'Event', 'audit_table_related' => 'Related Item or Detail', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Activity Date', 'audit_date_from' => 'Date Range From', 'audit_date_to' => 'Date Range To', diff --git a/resources/lang/ca/settings.php b/resources/lang/ca/settings.php index e6a6648d2..3a3fdddc1 100755 --- a/resources/lang/ca/settings.php +++ b/resources/lang/ca/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Usuari', 'audit_table_event' => 'Esdeveniment', 'audit_table_related' => 'Element relacionat o detall', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Data de l\'activitat', 'audit_date_from' => 'Rang de dates a partir de', 'audit_date_to' => 'Rang de rates fins a', diff --git a/resources/lang/cs/settings.php b/resources/lang/cs/settings.php index e7711bfd1..36d8bc0ec 100644 --- a/resources/lang/cs/settings.php +++ b/resources/lang/cs/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Uživatel', 'audit_table_event' => 'Událost', 'audit_table_related' => 'Související položka nebo detail', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Datum aktivity', 'audit_date_from' => 'Časový rozsah od', 'audit_date_to' => 'Časový rozsah do', diff --git a/resources/lang/da/settings.php b/resources/lang/da/settings.php index 55d15bf4e..cfb4ed908 100644 --- a/resources/lang/da/settings.php +++ b/resources/lang/da/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Bruger', 'audit_table_event' => 'Hændelse', 'audit_table_related' => 'Relateret element eller detalje', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Aktivitetsdato', 'audit_date_from' => 'Datointerval fra', 'audit_date_to' => 'Datointerval til', diff --git a/resources/lang/de/common.php b/resources/lang/de/common.php index 85808b67d..bd75e1737 100644 --- a/resources/lang/de/common.php +++ b/resources/lang/de/common.php @@ -33,7 +33,7 @@ return [ 'copy' => 'Kopieren', 'reply' => 'Antworten', 'delete' => 'Löschen', - 'delete_confirm' => 'Löschen Bestätigen', + 'delete_confirm' => 'Löschen bestätigen', 'search' => 'Suchen', 'search_clear' => 'Suche löschen', 'reset' => 'Zurücksetzen', @@ -41,7 +41,7 @@ return [ 'add' => 'Hinzufügen', 'configure' => 'Konfigurieren', 'fullscreen' => 'Vollbild', - 'favourite' => 'Favorit', + 'favourite' => 'Favoriten', 'unfavourite' => 'Kein Favorit', 'next' => 'Nächste', 'previous' => 'Vorheriges', @@ -57,9 +57,9 @@ return [ 'sort_updated_at' => 'Aktualisierungsdatum', // Misc - 'deleted_user' => 'Gelöschte Benutzer', + 'deleted_user' => 'Gelöschter Benutzer', 'no_activity' => 'Keine Aktivitäten zum Anzeigen', - 'no_items' => 'Keine Einträge gefunden.', + 'no_items' => 'Keine Einträge gefunden', 'back_to_top' => 'nach oben', 'skip_to_main_content' => 'Direkt zum Hauptinhalt', 'toggle_details' => 'Details zeigen/verstecken', diff --git a/resources/lang/de/entities.php b/resources/lang/de/entities.php index 30efc7984..fdb263758 100644 --- a/resources/lang/de/entities.php +++ b/resources/lang/de/entities.php @@ -99,7 +99,7 @@ return [ 'shelves_permissions' => 'Regal-Berechtigungen', 'shelves_permissions_updated' => 'Regal-Berechtigungen aktualisiert', 'shelves_permissions_active' => 'Regal-Berechtigungen aktiv', - 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', + 'shelves_permissions_cascade_warning' => 'Die Berechtigungen in Bücherregalen werden nicht automatisch auf enthaltene Bücher kaskadiert, weil ein Buch in mehreren Regalen existieren kann. Berechtigungen können jedoch mit der unten stehenden Option in untergeordnete Bücher kopiert werden.', 'shelves_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch', 'shelves_copy_permissions' => 'Berechtigungen kopieren', 'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher übertragen. Überprüfen Sie vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.', diff --git a/resources/lang/de/errors.php b/resources/lang/de/errors.php index 0a21857df..65174eada 100644 --- a/resources/lang/de/errors.php +++ b/resources/lang/de/errors.php @@ -5,7 +5,7 @@ return [ // Permissions - 'permission' => 'Sie haben keine Berechtigung, auf diese Seite zuzugreifen.', + 'permission' => 'Sie haben keine Zugriffsberechtigung auf die angeforderte Seite.', 'permissionJson' => 'Sie haben keine Berechtigung, die angeforderte Aktion auszuführen.', // Auth @@ -14,7 +14,7 @@ return [ 'email_confirmation_invalid' => 'Der Bestätigungslink ist nicht gültig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.', 'email_confirmation_expired' => 'Der Bestätigungslink ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.', 'email_confirmation_awaiting' => 'Die E-Mail-Adresse für das verwendete Konto muss bestätigt werden', - 'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlafgen', + 'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlagen', 'ldap_fail_authed' => 'LDAP-Zugriff mit DN und Passwort ist fehlgeschlagen', 'ldap_extension_not_installed' => 'LDAP-PHP-Erweiterung ist nicht installiert.', 'ldap_cannot_connect' => 'Die Verbindung zum LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.', @@ -43,14 +43,14 @@ return [ 'uploaded' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.', 'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.', 'image_upload_type_error' => 'Der Bildtyp der hochgeladenen Datei ist ungültig.', - 'file_upload_timeout' => 'Der Upload der Datei ist abgelaufen.', + 'file_upload_timeout' => 'Der Datei-Upload hat das Zeitlimit überschritten.', // Attachments 'attachment_not_found' => 'Anhang konnte nicht gefunden werden.', // Pages 'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stellen Sie sicher, dass Sie mit dem Internet verbunden sind, bevor Sie den Entwurf dieser Seite speichern.', - 'page_custom_home_deletion' => 'Eine als Startseite gesetzte Seite kann nicht gelöscht werden.', + 'page_custom_home_deletion' => 'Eine als Startseite gesetzte Seite kann nicht gelöscht werden', // Entities 'entity_not_found' => 'Eintrag nicht gefunden', @@ -58,48 +58,48 @@ return [ 'book_not_found' => 'Buch nicht gefunden', 'page_not_found' => 'Seite nicht gefunden', 'chapter_not_found' => 'Kapitel nicht gefunden', - 'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden.', + 'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden', 'selected_book_chapter_not_found' => 'Das gewählte Buch oder Kapitel wurde nicht gefunden.', 'guests_cannot_save_drafts' => 'Gäste können keine Entwürfe speichern', // Users - 'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen.', + 'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen', 'users_cannot_delete_guest' => 'Sie können den Gast-Benutzer nicht löschen', // Roles - 'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden.', + 'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden', 'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelöscht werden', 'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gelöscht werden, solange sie als Standardrolle für neue Registrierungen gesetzt ist', - 'role_cannot_remove_only_admin' => 'Dieser Benutzer ist der einzige Benutzer, welchem die Administratorrolle zugeordnet ist. Ordnen Sie die Administratorrolle einem anderen Benutzer zu, bevor Sie versuchen, sie hier zu entfernen.', + 'role_cannot_remove_only_admin' => 'Dieser Benutzer ist der einzige Benutzer, welchem die Administratorrolle zugeordnet ist. Ordnen Sie die Administratorrolle einem anderen Benutzer zu bevor Sie versuchen sie hier zu entfernen.', // Comments 'comment_list' => 'Beim Abrufen der Kommentare ist ein Fehler aufgetreten.', 'cannot_add_comment_to_draft' => 'Du kannst keine Kommentare zu einem Entwurf hinzufügen.', 'comment_add' => 'Beim Hinzufügen des Kommentars ist ein Fehler aufgetreten.', 'comment_delete' => 'Beim Löschen des Kommentars ist ein Fehler aufgetreten.', - 'empty_comment' => 'Kann keinen leeren Kommentar hinzufügen', + 'empty_comment' => 'Kann keinen leeren Kommentar hinzufügen.', // Error pages '404_page_not_found' => 'Seite nicht gefunden', - 'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben, wurde nicht gefunden.', + 'sorry_page_not_found' => 'Entschuldigung. Die angeforderte Seite wurde nicht gefunden.', 'sorry_page_not_found_permission_warning' => 'Wenn Sie erwartet haben, dass diese Seite existiert, haben Sie möglicherweise nicht die Berechtigung, sie anzuzeigen.', 'image_not_found' => 'Bild nicht gefunden', - 'image_not_found_subtitle' => 'Entschuldigung. Das Bild, die Sie angefordert haben, wurde nicht gefunden.', + 'image_not_found_subtitle' => 'Entschuldigung. Das angeforderte Bild wurde nicht gefunden.', 'image_not_found_details' => 'Wenn Sie erwartet haben, dass dieses Bild existiert, könnte es gelöscht worden sein.', 'return_home' => 'Zurück zur Startseite', 'error_occurred' => 'Es ist ein Fehler aufgetreten', - 'app_down' => ':appName befindet sich aktuell im Wartungsmodus.', + 'app_down' => ':appName befindet sich aktuell im Wartungsmodus', 'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.', // API errors - 'api_no_authorization_found' => 'Kein Autorisierungs-Token für die Anfrage gefunden', - 'api_bad_authorization_format' => 'Ein Autorisierungs-Token wurde auf die Anfrage gefunden, aber das Format schien falsch zu sein', - 'api_user_token_not_found' => 'Es wurde kein passender API-Token für den angegebenen Autorisierungs-Token gefunden', - 'api_incorrect_token_secret' => 'Das für den angegebenen API-Token angegebene Kennwort ist falsch', - 'api_user_no_api_permission' => 'Der Besitzer des verwendeten API-Token hat keine Berechtigung für API-Aufrufe', - 'api_user_token_expired' => 'Das verwendete Autorisierungs-Token ist abgelaufen', + 'api_no_authorization_found' => 'Kein Autorisierungstoken für die Anfrage gefunden', + 'api_bad_authorization_format' => 'Ein Autorisierungstoken wurde auf die Anfrage gefunden, aber das Format schien falsch zu sein', + 'api_user_token_not_found' => 'Es wurde kein passender API-Token für den angegebenen Autorisierungstoken gefunden', + 'api_incorrect_token_secret' => 'Das Kennwort für das angegebene API-Token ist falsch', + 'api_user_no_api_permission' => 'Der Besitzer des verwendeten API-Tokens hat keine Berechtigung für API-Aufrufe', + 'api_user_token_expired' => 'Das verwendete Autorisierungstoken ist abgelaufen', // Settings & Maintenance - 'maintenance_test_email_failure' => 'Fehler beim Senden einer Test E-Mail:', + 'maintenance_test_email_failure' => 'Fehler beim Versenden einer Test E-Mail:', ]; diff --git a/resources/lang/de/settings.php b/resources/lang/de/settings.php index c9e589347..d24319c18 100644 --- a/resources/lang/de/settings.php +++ b/resources/lang/de/settings.php @@ -18,7 +18,7 @@ return [ 'app_name_desc' => 'Dieser Name wird im Header und in E-Mails angezeigt.', 'app_name_header' => 'Anwendungsname im Header anzeigen?', 'app_public_access' => 'Öffentlicher Zugriff', - 'app_public_access_desc' => 'Wenn Sie diese Option aktivieren, können Besucher, die nicht angemeldet sind, auf Inhalte in Ihrer BookStack-Instanz zugreifen.', + 'app_public_access_desc' => 'Wenn Sie diese Option aktivieren können Besucher, die nicht angemeldet sind, auf Inhalte in Ihrer BookStack-Instanz zugreifen.', 'app_public_access_desc_guest' => 'Der Zugang für öffentliche Besucher kann über den Benutzer "Guest" gesteuert werden.', 'app_public_access_toggle' => 'Öffentlichen Zugriff erlauben', 'app_public_viewing' => 'Öffentliche Ansicht erlauben?', @@ -40,7 +40,7 @@ Wenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zurückgesetzt 'app_homepage_desc' => 'Wählen Sie eine Seite als Startseite aus, die statt der Standardansicht angezeigt werden soll. Seitenberechtigungen werden für die ausgewählten Seiten ignoriert.', 'app_homepage_select' => 'Wählen Sie eine Seite aus', 'app_footer_links' => 'Fußzeilen-Links', - 'app_footer_links_desc' => 'Fügen Sie Links hinzu, die innerhalb der Seitenfußzeile angezeigt werden. Diese werden am unteren Ende der meisten Seiten angezeigt, einschließlich derjenigen, die keinen Login benötigen. Sie können die Bezeichnung "trans::" verwenden, um systemdefinierte Übersetzungen zu verwenden. Beispiel: Mit "trans::common.privacy_policy" wird der übersetzte Text "Privacy Policy" bereitgestellt, und "trans::common.terms_of_service" liefert den übersetzten Text "Terms of Service".', + 'app_footer_links_desc' => 'Fügen Sie Links hinzu, die innerhalb der Seitenfußzeile angezeigt werden. Diese werden am unteren Ende der meisten Seiten angezeigt, einschließlich derjenigen, die keinen Login benötigen. Sie können die Bezeichnung "trans::" verwenden, um systemdefinierte Übersetzungen zu verwenden. Beispiel: Mit "trans::common.privacy_policy" wird der übersetzte Text "Privacy Policy" bereitgestellt und "trans::common.terms_of_service" liefert den übersetzten Text "Terms of Service".', 'app_footer_links_label' => 'Link-Label', 'app_footer_links_url' => 'Link-URL', 'app_footer_links_add' => 'Fußzeilen-Link hinzufügen', @@ -59,7 +59,7 @@ Wenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zurückgesetzt // Registration Settings 'reg_settings' => 'Registrierungseinstellungen', - 'reg_enable' => 'Registrierung erlauben?', + 'reg_enable' => 'Registrierung erlauben', 'reg_enable_toggle' => 'Registrierung erlauben', 'reg_enable_desc' => 'Wenn die Registrierung erlaubt ist, kann sich der Benutzer als Anwendungsbenutzer anmelden. Bei der Registrierung erhält er eine einzige, voreingestellte Benutzerrolle.', 'reg_default_role' => 'Standard-Benutzerrolle nach Registrierung', @@ -108,7 +108,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'recycle_bin_restore_list' => 'Zu wiederherzustellende Elemente', 'recycle_bin_restore_confirm' => 'Mit dieser Aktion wird das gelöschte Element einschließlich aller untergeordneten Elemente an seinen ursprünglichen Ort wiederherstellen. Wenn der ursprüngliche Ort gelöscht wurde und sich nun im Papierkorb befindet, muss auch das übergeordnete Element wiederhergestellt werden.', 'recycle_bin_restore_deleted_parent' => 'Das übergeordnete Elements wurde ebenfalls gelöscht. Dieses Element wird weiterhin als gelöscht zählen, bis auch das übergeordnete Element wiederhergestellt wurde.', - 'recycle_bin_restore_parent' => 'Elternteil wiederherstellen', + 'recycle_bin_restore_parent' => 'Übergeordneter Eintrag wiederherstellen', 'recycle_bin_destroy_notification' => ':count Elemente wurden aus dem Papierkorb gelöscht.', 'recycle_bin_restore_notification' => ':count Elemente wurden aus dem Papierkorb wiederhergestellt.', @@ -122,6 +122,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'audit_table_user' => 'Benutzer', 'audit_table_event' => 'Ereignis', 'audit_table_related' => 'Verknüpftes Element oder Detail', + 'audit_table_ip' => 'IP Adresse', 'audit_table_date' => 'Aktivitätsdatum', 'audit_date_from' => 'Zeitraum von', 'audit_date_to' => 'Zeitraum bis', @@ -151,7 +152,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'role_manage_page_templates' => 'Seitenvorlagen verwalten', 'role_access_api' => 'Systemzugriffs-API', 'role_manage_settings' => 'Globaleinstellungen verwalten', - 'role_export_content' => 'Export content', + 'role_export_content' => 'Inhalt exportieren', 'role_asset' => 'Berechtigungen', 'roles_system_warning' => 'Beachten Sie, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weisen Sie nur Rollen, mit diesen Berechtigungen, vertrauenswürdigen Benutzern zu.', 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.', diff --git a/resources/lang/de_informal/common.php b/resources/lang/de_informal/common.php index 594ee519d..898df928e 100644 --- a/resources/lang/de_informal/common.php +++ b/resources/lang/de_informal/common.php @@ -33,15 +33,15 @@ return [ 'copy' => 'Kopieren', 'reply' => 'Antworten', 'delete' => 'Löschen', - 'delete_confirm' => 'Löschen Bestätigen', + 'delete_confirm' => 'Löschen bestätigen', 'search' => 'Suchen', 'search_clear' => 'Suche löschen', 'reset' => 'Zurücksetzen', 'remove' => 'Entfernen', 'add' => 'Hinzufügen', - 'configure' => 'Configure', + 'configure' => 'Konfigurieren', 'fullscreen' => 'Vollbild', - 'favourite' => 'Favorit', + 'favourite' => 'Favoriten', 'unfavourite' => 'Kein Favorit', 'next' => 'Nächste', 'previous' => 'Vorheriges', @@ -57,7 +57,7 @@ return [ 'sort_updated_at' => 'Aktualisierungsdatum', // Misc - 'deleted_user' => 'Gelöschte Benutzer', + 'deleted_user' => 'Gelöschter Benutzer', 'no_activity' => 'Keine Aktivitäten zum Anzeigen', 'no_items' => 'Keine Einträge gefunden.', 'back_to_top' => 'nach oben', diff --git a/resources/lang/de_informal/entities.php b/resources/lang/de_informal/entities.php index 4bc530a52..3f8bccaed 100644 --- a/resources/lang/de_informal/entities.php +++ b/resources/lang/de_informal/entities.php @@ -36,7 +36,7 @@ return [ 'export_html' => 'HTML-Datei', 'export_pdf' => 'PDF-Datei', 'export_text' => 'Textdatei', - 'export_md' => 'Markdown-Datei', + 'export_md' => 'Markdown-Dateir', // Permissions and restrictions 'permissions' => 'Berechtigungen', @@ -99,7 +99,7 @@ return [ 'shelves_permissions' => 'Regal-Berechtigungen', 'shelves_permissions_updated' => 'Regal-Berechtigungen aktualisiert', 'shelves_permissions_active' => 'Regal-Berechtigungen aktiv', - 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', + 'shelves_permissions_cascade_warning' => 'Die Berechtigungen in Bücherregalen werden nicht automatisch auf enthaltene Bücher kaskadiert, weil ein Buch in mehreren Regalen existieren kann. Berechtigungen können jedoch mit der unten stehenden Option in untergeordnete Bücher kopiert werden.', 'shelves_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch', 'shelves_copy_permissions' => 'Berechtigungen kopieren', 'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher übertragen. Überprüfe vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.', diff --git a/resources/lang/de_informal/settings.php b/resources/lang/de_informal/settings.php index 3dbc320f9..53d8f8359 100644 --- a/resources/lang/de_informal/settings.php +++ b/resources/lang/de_informal/settings.php @@ -108,7 +108,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'recycle_bin_restore_list' => 'Wiederherzustellende Einträge', 'recycle_bin_restore_confirm' => 'Mit dieser Aktion wird der gelöschte Eintrag einschließlich aller untergeordneten Einträge an seinen ursprünglichen Ort wiederherstellen. Wenn der ursprüngliche Ort gelöscht wurde und sich nun im Papierkorb befindet, muss auch der übergeordnete Eintrag wiederhergestellt werden.', 'recycle_bin_restore_deleted_parent' => 'Der übergeordnete Eintrag wurde ebenfalls gelöscht. Dieser Eintrag wird weiterhin als gelöscht zählen, bis auch der übergeordnete Eintrag wiederhergestellt wurde.', - 'recycle_bin_restore_parent' => 'Elternteil wiederherstellen', + 'recycle_bin_restore_parent' => 'Übergeordneter Eintrag wiederherstellen', 'recycle_bin_destroy_notification' => ':count Einträge wurden aus dem Papierkorb gelöscht.', 'recycle_bin_restore_notification' => ':count Einträge wurden aus dem Papierkorb wiederhergestellt.', @@ -122,6 +122,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'audit_table_user' => 'Benutzer', 'audit_table_event' => 'Ereignis', 'audit_table_related' => 'Verknüpfter Eintrag oder Detail', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Aktivitätsdatum', 'audit_date_from' => 'Zeitraum von', 'audit_date_to' => 'Zeitraum bis', @@ -151,7 +152,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'role_manage_page_templates' => 'Seitenvorlagen verwalten', 'role_access_api' => 'Systemzugriffs-API', 'role_manage_settings' => 'Globaleinstellungen verwalten', - 'role_export_content' => 'Export content', + 'role_export_content' => 'Inhalt exportieren', 'role_asset' => 'Berechtigungen', 'roles_system_warning' => 'Beachten Sie, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weisen Sie nur Rollen, mit diesen Berechtigungen, vertrauenswürdigen Benutzern zu.', 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.', @@ -210,7 +211,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'users_api_tokens_expires' => 'Endet', 'users_api_tokens_docs' => 'API Dokumentation', 'users_mfa' => 'Multi-Faktor-Authentifizierung', - 'users_mfa_desc' => 'Richten Sie Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für Ihr Benutzerkonto ein.', + 'users_mfa_desc' => 'Richte die Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für dein Benutzerkonto ein.', 'users_mfa_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert', 'users_mfa_configure' => 'Methoden konfigurieren', diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index 4c1ae1345..0ab168b66 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'User', 'audit_table_event' => 'Event', 'audit_table_related' => 'Related Item or Detail', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Activity Date', 'audit_date_from' => 'Date Range From', 'audit_date_to' => 'Date Range To', diff --git a/resources/lang/es/settings.php b/resources/lang/es/settings.php index 1ffaf187c..bf9c89a63 100644 --- a/resources/lang/es/settings.php +++ b/resources/lang/es/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Usuario', 'audit_table_event' => 'Evento', 'audit_table_related' => 'Elemento o detalle relacionados', + 'audit_table_ip' => 'Dirección IP', 'audit_table_date' => 'Fecha de la actividad', 'audit_date_from' => 'Rango de fecha desde', 'audit_date_to' => 'Rango de fecha hasta', diff --git a/resources/lang/es_AR/settings.php b/resources/lang/es_AR/settings.php index c0ea221bd..99ec4c219 100644 --- a/resources/lang/es_AR/settings.php +++ b/resources/lang/es_AR/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Usuario', 'audit_table_event' => 'Evento', 'audit_table_related' => 'Elemento o detalle relacionados', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Fecha de la Actividad', 'audit_date_from' => 'Inicio del Rango de Fecha', 'audit_date_to' => 'Final del Rango de Fecha', @@ -149,7 +150,7 @@ return [ 'role_manage_page_templates' => 'Gestionar las plantillas de páginas', 'role_access_api' => 'API de sistema de acceso', 'role_manage_settings' => 'Gestionar ajustes de activos', - 'role_export_content' => 'Export content', + 'role_export_content' => 'Exportar contenido', 'role_asset' => 'Permisos de activos', 'roles_system_warning' => 'Tenga en cuenta que el acceso a cualquiera de los tres permisos anteriores puede permitir a un usuario modificar sus propios privilegios o los privilegios de otros usuarios en el sistema. Asignar roles con estos permisos sólo a usuarios de comfianza.', 'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los activos del sistema. Permisos definidos en Libros, Capítulos y Páginas ignorarán estos permisos.', diff --git a/resources/lang/fa/settings.php b/resources/lang/fa/settings.php index 4c1ae1345..0ab168b66 100644 --- a/resources/lang/fa/settings.php +++ b/resources/lang/fa/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'User', 'audit_table_event' => 'Event', 'audit_table_related' => 'Related Item or Detail', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Activity Date', 'audit_date_from' => 'Date Range From', 'audit_date_to' => 'Date Range To', diff --git a/resources/lang/fr/activities.php b/resources/lang/fr/activities.php index c1db4a644..9850bc93c 100644 --- a/resources/lang/fr/activities.php +++ b/resources/lang/fr/activities.php @@ -26,11 +26,11 @@ return [ 'chapter_move' => 'a déplacé le chapitre', // Books - 'book_create' => 'a créé le livre', + 'book_create' => 'a créé un livre', 'book_create_notification' => 'Livre créé avec succès', 'book_update' => 'a modifié le livre', 'book_update_notification' => 'Livre modifié avec succès', - 'book_delete' => 'a supprimé le livre', + 'book_delete' => 'a supprimé un livre', 'book_delete_notification' => 'Livre supprimé avec succès', 'book_sort' => 'a réordonné le livre', 'book_sort_notification' => 'Livre réordonné avec succès', @@ -53,5 +53,5 @@ return [ // Other 'commented_on' => 'a commenté', - 'permissions_update' => 'mettre à jour les autorisations', + 'permissions_update' => 'a mis à jour les autorisations sur', ]; diff --git a/resources/lang/fr/auth.php b/resources/lang/fr/auth.php index f6ad00759..c608f02fb 100644 --- a/resources/lang/fr/auth.php +++ b/resources/lang/fr/auth.php @@ -57,13 +57,13 @@ return [ 'email_confirm_action' => 'Confirmez votre adresse e-mail', 'email_confirm_send_error' => 'La confirmation par e-mail est requise mais le système n\'a pas pu envoyer l\'e-mail. Contactez l\'administrateur système.', 'email_confirm_success' => 'Votre adresse e-mail a été confirmée !', - 'email_confirm_resent' => 'L\'e-mail de confirmation a été ré-envoyé. Vérifiez votre boîte de récéption.', + 'email_confirm_resent' => 'L\'e-mail de confirmation a été ré-envoyé. Vérifiez votre boîte de réception.', 'email_not_confirmed' => 'Adresse e-mail non confirmée', 'email_not_confirmed_text' => 'Votre adresse e-mail n\'a pas été confirmée.', 'email_not_confirmed_click_link' => 'Merci de cliquer sur le lien dans l\'e-mail qui vous a été envoyé après l\'enregistrement.', 'email_not_confirmed_resend' => 'Si vous ne retrouvez plus l\'e-mail, vous pouvez renvoyer un e-mail de confirmation en utilisant le formulaire ci-dessous.', - 'email_not_confirmed_resend_button' => 'Renvoyez l\'e-mail de confirmation', + 'email_not_confirmed_resend_button' => 'Renvoyer l\'e-mail de confirmation', // User Invite 'user_invite_email_subject' => 'Vous avez été invité(e) à rejoindre :appName !', @@ -76,8 +76,8 @@ return [ 'user_invite_success' => 'Mot de passe renseigné, vous avez maintenant accès à :appName !', // Multi-factor Authentication - 'mfa_setup' => 'Configuration authentification multi-facteurs', - 'mfa_setup_desc' => 'Configurez l\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.', + 'mfa_setup' => 'Authentification multi-facteurs', + 'mfa_setup_desc' => 'Configurer l\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.', 'mfa_setup_configured' => 'Déjà configuré', 'mfa_setup_reconfigure' => 'Reconfigurer', 'mfa_setup_remove_confirmation' => 'Êtes-vous sûr de vouloir supprimer cette méthode d\'authentification multi-facteurs ?', @@ -90,14 +90,14 @@ return [ 'mfa_gen_confirm_and_enable' => 'Confirmer et activer', 'mfa_gen_backup_codes_title' => 'Configuration des codes de secours', 'mfa_gen_backup_codes_desc' => 'Stockez la liste des codes ci-dessous dans un endroit sûr. Lorsque vous accédez au système, vous pourrez utiliser l\'un des codes comme un deuxième mécanisme d\'authentification.', - 'mfa_gen_backup_codes_download' => 'Télécharger le code', + 'mfa_gen_backup_codes_download' => 'Télécharger les codes', 'mfa_gen_backup_codes_usage_warning' => 'Chaque code ne peut être utilisé qu\'une seule fois', 'mfa_gen_totp_title' => 'Configuration de l\'application mobile', 'mfa_gen_totp_desc' => 'Pour utiliser l\'authentification multi-facteurs, vous aurez besoin d\'une application mobile qui supporte TOTP comme Google Authenticator, Authy ou Microsoft Authenticator.', 'mfa_gen_totp_scan' => 'Scannez le QR code ci-dessous avec votre application d\'authentification préférée pour débuter.', 'mfa_gen_totp_verify_setup' => 'Vérifier la configuration', 'mfa_gen_totp_verify_setup_desc' => 'Vérifiez que tout fonctionne en utilisant un code généré par votre application d\'authentification, dans la zone ci-dessous :', - 'mfa_gen_totp_provide_code_here' => 'Fournir le code généré par votre application ici', + 'mfa_gen_totp_provide_code_here' => 'Fournissez le code généré par votre application ici', 'mfa_verify_access' => 'Vérifier l\'accès', 'mfa_verify_access_desc' => 'Votre compte d\'utilisateur vous demande de confirmer votre identité par un niveau supplémentaire de vérification avant que vous n\'ayez accès. Vérifiez-la en utilisant l\'une de vos méthodes configurées pour continuer.', 'mfa_verify_no_methods' => 'Aucune méthode configurée', diff --git a/resources/lang/fr/common.php b/resources/lang/fr/common.php index b25a817ab..e6e8bf1a2 100644 --- a/resources/lang/fr/common.php +++ b/resources/lang/fr/common.php @@ -27,14 +27,14 @@ return [ 'view_all' => 'Tout afficher', 'create' => 'Créer', 'update' => 'Modifier', - 'edit' => 'Editer', + 'edit' => 'Éditer', 'sort' => 'Trier', 'move' => 'Déplacer', 'copy' => 'Copier', 'reply' => 'Répondre', 'delete' => 'Supprimer', 'delete_confirm' => 'Confirmer la suppression', - 'search' => 'Chercher', + 'search' => 'Rechercher', 'search_clear' => 'Réinitialiser la recherche', 'reset' => 'Réinitialiser', 'remove' => 'Enlever', diff --git a/resources/lang/fr/components.php b/resources/lang/fr/components.php index 6cce4f804..fed157a47 100644 --- a/resources/lang/fr/components.php +++ b/resources/lang/fr/components.php @@ -26,7 +26,7 @@ return [ 'image_upload_remove' => 'Supprimer', // Code Editor - 'code_editor' => 'Editer le code', + 'code_editor' => 'Éditer le code', 'code_language' => 'Langage du code', 'code_content' => 'Contenu du code', 'code_session_history' => 'Historique de session', diff --git a/resources/lang/fr/entities.php b/resources/lang/fr/entities.php index e5779e10b..1ae697c40 100644 --- a/resources/lang/fr/entities.php +++ b/resources/lang/fr/entities.php @@ -22,12 +22,12 @@ return [ 'meta_created_name' => 'Créé :timeLength par :user', 'meta_updated' => 'Mis à jour :timeLength', 'meta_updated_name' => 'Mis à jour :timeLength par :user', - 'meta_owned_name' => 'Possédé par :user', + 'meta_owned_name' => 'Appartient à :user', 'entity_select' => 'Sélectionner l\'entité', 'images' => 'Images', 'my_recent_drafts' => 'Mes brouillons récents', 'my_recently_viewed' => 'Vus récemment', - 'my_most_viewed_favourites' => 'Mes Favoris les plus vus', + 'my_most_viewed_favourites' => 'Mes favoris les plus vus', 'my_favourites' => 'Mes favoris', 'no_pages_viewed' => 'Vous n\'avez rien visité récemment', 'no_pages_recently_created' => 'Aucune page créée récemment', @@ -40,7 +40,7 @@ return [ // Permissions and restrictions 'permissions' => 'Autorisations', - 'permissions_intro' => 'Une fois activées ces permissions prendront la priorité sur tous les sets de permissions préexistants.', + 'permissions_intro' => 'Une fois activées, ces permissions auront la priorité sur tous les jeux de permissions préexistants.', 'permissions_enable' => 'Activer les permissions personnalisées', 'permissions_save' => 'Enregistrer les permissions', 'permissions_owner' => 'Propriétaire', @@ -80,8 +80,8 @@ return [ 'shelves_empty' => 'Aucune étagère n\'a été créée', 'shelves_create' => 'Créer une nouvelle étagère', 'shelves_popular' => 'Étagères populaires', - 'shelves_new' => 'Nouvelles Étagères', - 'shelves_new_action' => 'Nouvelle Étagère', + 'shelves_new' => 'Nouvelles étagères', + 'shelves_new_action' => 'Nouvelle étagère', 'shelves_popular_empty' => 'Les étagères les plus populaires apparaîtront ici.', 'shelves_new_empty' => 'Les étagères les plus récentes apparaitront ici.', 'shelves_save' => 'Enregistrer l\'étagère', @@ -132,7 +132,7 @@ return [ 'books_empty_sort_current_book' => 'Trier les pages du livre', 'books_empty_add_chapter' => 'Ajouter un chapitre', 'books_permissions_active' => 'Permissions personnalisées activées', - 'books_search_this' => 'Chercher dans le livre', + 'books_search_this' => 'Rechercher dans ce livre', 'books_navigation' => 'Navigation dans le livre', 'books_sort' => 'Trier les contenus du livre', 'books_sort_named' => 'Trier le livre :bookName', @@ -174,7 +174,7 @@ return [ 'pages_popular' => 'Pages populaires', 'pages_new' => 'Nouvelle page', 'pages_attachments' => 'Fichiers joints', - 'pages_navigation' => 'Navigation des pages', + 'pages_navigation' => 'Navigation dans la page', 'pages_delete' => 'Supprimer la page', 'pages_delete_named' => 'Supprimer la page :pageName', 'pages_delete_draft_named' => 'supprimer le brouillon de la page :pageName', @@ -189,16 +189,16 @@ return [ 'pages_edit_draft' => 'Modifier le brouillon', 'pages_editing_draft' => 'Modification du brouillon', 'pages_editing_page' => 'Modification de la page', - 'pages_edit_draft_save_at' => 'Brouillon sauvé le ', + 'pages_edit_draft_save_at' => 'Brouillon enregistré le ', 'pages_edit_delete_draft' => 'Supprimer le brouillon', - 'pages_edit_discard_draft' => 'Ecarter le brouillon', + 'pages_edit_discard_draft' => 'Jeter le brouillon', 'pages_edit_set_changelog' => 'Remplir le journal des changements', 'pages_edit_enter_changelog_desc' => 'Entrez une brève description des changements effectués', - 'pages_edit_enter_changelog' => 'Entrer dans le journal des changements', - 'pages_save' => 'Enregistrez la page', + 'pages_edit_enter_changelog' => 'Ouvrir le journal des changements', + 'pages_save' => 'Enregistrer la page', 'pages_title' => 'Titre de la page', 'pages_name' => 'Nom de la page', - 'pages_md_editor' => 'Editeur', + 'pages_md_editor' => 'Éditeur', 'pages_md_preview' => 'Prévisualisation', 'pages_md_insert_image' => 'Insérer une image', 'pages_md_insert_link' => 'Insérer un lien', @@ -223,7 +223,7 @@ return [ 'pages_revisions_numbered_changes' => 'Modification #:id', 'pages_revisions_changelog' => 'Journal des changements', 'pages_revisions_changes' => 'Changements', - 'pages_revisions_current' => 'Version courante', + 'pages_revisions_current' => 'Version actuelle', 'pages_revisions_preview' => 'Prévisualisation', 'pages_revisions_restore' => 'Restaurer', 'pages_revisions_none' => 'Cette page n\'a aucune révision', @@ -232,8 +232,8 @@ return [ 'pages_permissions_active' => 'Permissions de page actives', 'pages_initial_revision' => 'Publication initiale', 'pages_initial_name' => 'Nouvelle page', - 'pages_editing_draft_notification' => 'Vous éditez actuellement un brouillon qui a été sauvé :timeDiff.', - 'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visite. Vous devriez écarter ce brouillon.', + 'pages_editing_draft_notification' => 'Vous éditez actuellement un brouillon qui a été enregistré :timeDiff.', + 'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visite. Vous devriez jeter ce brouillon.', 'pages_draft_edit_active' => [ 'start_a' => ':count utilisateurs ont commencé à éditer cette page', 'start_b' => ':userName a commencé à éditer cette page', @@ -242,7 +242,7 @@ return [ 'message' => ':start :time. Attention à ne pas écraser les mises à jour de quelqu\'un d\'autre !', ], 'pages_draft_discarded' => 'Brouillon écarté, la page est dans sa version actuelle.', - 'pages_specific' => 'Page Spécifique', + 'pages_specific' => 'Page spécifique', 'pages_is_template' => 'Modèle de page', // Editor Sidebar @@ -253,10 +253,10 @@ return [ 'tag' => 'Mot-clé', 'tags' => 'Mots-clés', 'tag_name' => 'Nom du tag', - 'tag_value' => 'Valeur du mot-clé (Optionnel)', + 'tag_value' => 'Valeur du mot-clé (optionnel)', 'tags_explain' => "Ajouter des mots-clés pour catégoriser votre contenu.", 'tags_add' => 'Ajouter un autre mot-clé', - 'tags_remove' => 'Supprimer le tag', + 'tags_remove' => 'Supprimer le mot-clé', 'attachments' => 'Fichiers joints', 'attachments_explain' => 'Ajouter des fichiers ou des liens pour les afficher sur votre page. Ils seront affichés dans la barre latérale', 'attachments_explain_instant_save' => 'Ces changements sont enregistrés immédiatement.', @@ -267,13 +267,13 @@ return [ 'attachments_delete' => 'Êtes-vous sûr de vouloir supprimer la pièce jointe ?', 'attachments_dropzone' => 'Glissez des fichiers ou cliquez ici pour attacher des fichiers', 'attachments_no_files' => 'Aucun fichier ajouté', - 'attachments_explain_link' => 'Vous pouvez attacher un lien si vous ne souhaitez pas uploader un fichier.', + 'attachments_explain_link' => 'Vous pouvez ajouter un lien si vous ne souhaitez pas uploader un fichier.', 'attachments_link_name' => 'Nom du lien', 'attachment_link' => 'Lien de l\'attachement', 'attachments_link_url' => 'Lien sur un fichier', 'attachments_link_url_hint' => 'URL du site ou du fichier', - 'attach' => 'Attacher', - 'attachments_insert_link' => 'Ajouter un lien de pièce jointe à la page', + 'attach' => 'Ajouter', + 'attachments_insert_link' => 'Ajouter un lien à la page', 'attachments_edit_file' => 'Modifier le fichier', 'attachments_edit_file_name' => 'Nom du fichier', 'attachments_edit_drop_upload' => 'Glissez un fichier ou cliquer pour mettre à jour le fichier', @@ -288,7 +288,7 @@ return [ 'templates_explain_set_as_template' => 'Vous pouvez définir cette page comme modèle pour que son contenu soit utilisé lors de la création d\'autres pages. Les autres utilisateurs pourront utiliser ce modèle s\'ils ont les permissions pour cette page.', 'templates_replace_content' => 'Remplacer le contenu de la page', 'templates_append_content' => 'Ajouter après le contenu de la page', - 'templates_prepend_content' => 'Ajouter devant le contenu de la page', + 'templates_prepend_content' => 'Ajouter avant le contenu de la page', // Profile View 'profile_user_for_x' => 'Utilisateur depuis :time', @@ -313,7 +313,7 @@ return [ 'comment_deleted_success' => 'Commentaire supprimé', 'comment_created_success' => 'Commentaire ajouté', 'comment_updated_success' => 'Commentaire mis à jour', - 'comment_delete_confirm' => 'Etes-vous sûr de vouloir supprimer ce commentaire ?', + 'comment_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer ce commentaire ?', 'comment_in_reply_to' => 'En réponse à :commentId', // Revision diff --git a/resources/lang/fr/errors.php b/resources/lang/fr/errors.php index 511683285..d7f00d8e1 100644 --- a/resources/lang/fr/errors.php +++ b/resources/lang/fr/errors.php @@ -16,24 +16,24 @@ return [ 'email_confirmation_awaiting' => 'L\'adresse e-mail du compte utilisé doit être confirmée', 'ldap_fail_anonymous' => 'L\'accès LDAP anonyme n\'a pas abouti', 'ldap_fail_authed' => 'L\'accès LDAP n\'a pas abouti avec cet utilisateur et ce mot de passe', - 'ldap_extension_not_installed' => 'L\'extension LDAP PHP n\'est pas installée', + 'ldap_extension_not_installed' => 'L\'extension PHP LDAP n\'est pas installée', 'ldap_cannot_connect' => 'Impossible de se connecter au serveur LDAP, la connexion initiale a échoué', 'saml_already_logged_in' => 'Déjà connecté', 'saml_user_not_registered' => 'L\'utilisateur :name n\'est pas enregistré et l\'enregistrement automatique est désactivé', 'saml_no_email_address' => 'Impossible de trouver une adresse e-mail, pour cet utilisateur, dans les données fournies par le système d\'authentification externe', 'saml_invalid_response_id' => 'La requête du système d\'authentification externe n\'est pas reconnue par un processus démarré par cette application. Naviguer après une connexion peut causer ce problème.', - 'saml_fail_authed' => 'Connexion avec :system échoue, le système n\'a pas fourni l\'autorisation réussie', + 'saml_fail_authed' => 'Connexion avec :system échouée, le système n\'a pas fourni l\'autorisation réussie', 'social_no_action_defined' => 'Pas d\'action définie', 'social_login_bad_response' => "Erreur pendant la tentative de connexion à :socialAccount : \n:error", 'social_account_in_use' => 'Ce compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.', - 'social_account_email_in_use' => 'L\'email :email est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le joindre à votre profil existant.', + 'social_account_email_in_use' => 'L\'email :email est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le rattacher à votre profil existant.', 'social_account_existing' => 'Ce compte :socialAccount est déjà rattaché à votre profil.', 'social_account_already_used_existing' => 'Ce compte :socialAccount est déjà utilisé par un autre utilisateur.', 'social_account_not_used' => 'Ce compte :socialAccount n\'est lié à aucun utilisateur. ', - 'social_account_register_instructions' => 'Si vous n\'avez pas encore de compte, vous pouvez le lier avec l\'option :socialAccount.', - 'social_driver_not_found' => 'Pilote de compte social absent', + 'social_account_register_instructions' => 'Si vous n\'avez pas encore de compte, vous pouvez en créer un avec l\'option :socialAccount.', + 'social_driver_not_found' => 'Pilote de compte de réseaux sociaux absent', 'social_driver_not_configured' => 'Vos préférences pour le compte :socialAccount sont incorrectes.', - 'invite_token_expired' => 'Le lien de cette invitation a expiré. Vous pouvez essayer de réinitiliser votre mot de passe.', + 'invite_token_expired' => 'Le lien de cette invitation a expiré. Vous pouvez essayer de réinitialiser votre mot de passe.', // System 'path_not_writable' => 'Impossible d\'écrire dans :filePath. Assurez-vous d\'avoir les droits d\'écriture sur le serveur', @@ -42,14 +42,14 @@ return [ 'server_upload_limit' => 'La taille du fichier est trop grande.', 'uploaded' => 'Le serveur n\'autorise pas l\'envoi d\'un fichier de cette taille. Veuillez essayer avec une taille de fichier réduite.', 'image_upload_error' => 'Une erreur est survenue pendant l\'envoi de l\'image', - 'image_upload_type_error' => 'LE format de l\'image envoyée n\'est pas valide', + 'image_upload_type_error' => 'Le format de l\'image envoyée n\'est pas valide', 'file_upload_timeout' => 'Le téléchargement du fichier a expiré.', // Attachments 'attachment_not_found' => 'Fichier joint non trouvé', // Pages - 'page_draft_autosave_fail' => 'Le brouillon n\'a pas pu être sauvé. Vérifiez votre connexion internet', + 'page_draft_autosave_fail' => 'Le brouillon n\'a pas pu être enregistré. Vérifiez votre connexion internet', 'page_custom_home_deletion' => 'Impossible de supprimer une page définie comme page d\'accueil', // Entities @@ -60,10 +60,10 @@ return [ 'chapter_not_found' => 'Chapitre non trouvé', 'selected_book_not_found' => 'Ce livre n\'a pas été trouvé', 'selected_book_chapter_not_found' => 'Ce livre ou chapitre n\'a pas été trouvé', - 'guests_cannot_save_drafts' => 'Les invités ne peuvent pas sauver de brouillons', + 'guests_cannot_save_drafts' => 'Les invités ne peuvent pas enregistrer de brouillons', // Users - 'users_cannot_delete_only_admin' => 'Vous ne pouvez pas supprimer le dernier admin', + 'users_cannot_delete_only_admin' => 'Vous ne pouvez pas supprimer le dernier administrateur', 'users_cannot_delete_guest' => 'Vous ne pouvez pas supprimer l\'utilisateur invité', // Roles @@ -74,7 +74,7 @@ return [ // Comments 'comment_list' => 'Une erreur s\'est produite lors de la récupération des commentaires.', - 'cannot_add_comment_to_draft' => 'Vous ne pouvez pas ajouter de commentaires à un projet.', + 'cannot_add_comment_to_draft' => 'Vous ne pouvez pas ajouter de commentaires à un brouillon.', 'comment_add' => 'Une erreur s\'est produite lors de l\'ajout du commentaire.', 'comment_delete' => 'Une erreur s\'est produite lors de la suppression du commentaire.', 'empty_comment' => 'Impossible d\'ajouter un commentaire vide.', @@ -82,10 +82,10 @@ return [ // Error pages '404_page_not_found' => 'Page non trouvée', 'sorry_page_not_found' => 'Désolé, cette page n\'a pas pu être trouvée.', - 'sorry_page_not_found_permission_warning' => 'Si vous vous attendiez à ce que cette page existe, il se peut que vous n\'ayez pas l\'autorisation de la consulter.', + 'sorry_page_not_found_permission_warning' => 'Si cette page est censée exister, il se peut que vous n\'ayez pas l\'autorisation de la consulter.', 'image_not_found' => 'Image non trouvée', 'image_not_found_subtitle' => 'Désolé, l\'image que vous cherchez ne peut être trouvée.', - 'image_not_found_details' => 'Si vous vous attendiez à ce que cette image existe, elle pourrait avoir été supprimée.', + 'image_not_found_details' => 'Si cette image était censée exister, il se pourrait qu\'elle ait été supprimée.', 'return_home' => 'Retour à l\'accueil', 'error_occurred' => 'Une erreur est survenue', 'app_down' => ':appName n\'est pas en service pour le moment', @@ -96,7 +96,7 @@ return [ 'api_bad_authorization_format' => 'Un jeton d\'autorisation a été trouvé pour la requête, mais le format semble incorrect', 'api_user_token_not_found' => 'Aucun jeton API correspondant n\'a été trouvé pour le jeton d\'autorisation fourni', 'api_incorrect_token_secret' => 'Le secret fourni pour le jeton d\'API utilisé est incorrect', - 'api_user_no_api_permission' => 'Le propriétaire du jeton API utilisé n\'a pas la permission de passer des appels API', + 'api_user_no_api_permission' => 'Le propriétaire du jeton API utilisé n\'a pas la permission de passer des requêtes API', 'api_user_token_expired' => 'Le jeton d\'autorisation utilisé a expiré', // Settings & Maintenance diff --git a/resources/lang/fr/passwords.php b/resources/lang/fr/passwords.php index b0ff20e28..e209c2178 100644 --- a/resources/lang/fr/passwords.php +++ b/resources/lang/fr/passwords.php @@ -6,10 +6,10 @@ */ return [ - 'password' => 'Les mots de passe doivent faire au moins 6 caractères et correspondre à la confirmation.', + 'password' => 'Les mots de passe doivent faire au moins 8 caractères et correspondre à la confirmation.', 'user' => "Nous n'avons pas trouvé d'utilisateur avec cette adresse.", 'token' => 'Le mot de passe reset du token n\'est pas valide pour cette adresse e-mail.', - 'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe !', + 'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe par e-mail !', 'reset' => 'Votre mot de passe a été réinitialisé !', ]; diff --git a/resources/lang/fr/settings.php b/resources/lang/fr/settings.php index 2baaefb45..aa58aeee0 100644 --- a/resources/lang/fr/settings.php +++ b/resources/lang/fr/settings.php @@ -21,15 +21,15 @@ return [ 'app_public_access_desc' => 'L\'activation de cette option permettra aux visiteurs, qui ne sont pas connectés, d\'accéder au contenu de votre instance BookStack.', 'app_public_access_desc_guest' => 'L\'accès pour les visiteurs publics peut être contrôlé par l\'utilisateur "Guest".', 'app_public_access_toggle' => 'Autoriser l\'accès public', - 'app_public_viewing' => 'Accepter le visionnage public des pages ?', - 'app_secure_images' => 'Activer l\'ajout d\'image sécurisé ?', + 'app_public_viewing' => 'Accepter l\'affichage public des pages ?', + 'app_secure_images' => 'Ajout d\'image sécurisé', 'app_secure_images_toggle' => 'Activer l\'ajout d\'image sécurisé', 'app_secure_images_desc' => 'Pour des questions de performances, toutes les images sont publiques. Cette option ajoute une chaîne aléatoire difficile à deviner dans les URLs des images.', 'app_editor' => 'Éditeur des pages', 'app_editor_desc' => 'Sélectionnez l\'éditeur qui sera utilisé pour modifier les pages.', 'app_custom_html' => 'HTML personnalisé dans l\'en-tête', 'app_custom_html_desc' => 'Le contenu inséré ici sera ajouté en bas de la balise de toutes les pages. Vous pouvez l\'utiliser pour ajouter du CSS personnalisé ou un tracker analytique.', - 'app_custom_html_disabled_notice' => 'Le contenu de la tête HTML personnalisée est désactivé sur cette page de paramètres pour garantir que les modifications les plus récentes peuvent être annulées.', + 'app_custom_html_disabled_notice' => 'Le contenu de l\'en-tête HTML personnalisé est désactivé sur cette page de paramètres pour garantir que les modifications les plus récentes puissent être annulées.', 'app_logo' => 'Logo de l\'application', 'app_logo_desc' => 'Cette image doit faire 43px de hauteur.
Les images plus larges seront réduites.', 'app_primary_color' => 'Couleur principale de l\'application', @@ -38,7 +38,7 @@ return [ 'app_homepage_desc' => 'Choisissez une page à afficher sur la page d\'accueil au lieu de la vue par défaut. Les permissions sont ignorées pour les pages sélectionnées.', 'app_homepage_select' => 'Choisissez une page', 'app_footer_links' => 'Liens de pied de page', - 'app_footer_links_desc' => 'Ajouter des liens à afficher dans le pied de page du site. Ils seront affichés en bas de la plupart des pages, y compris ceux qui ne nécessitent pas de connexion. Vous pouvez utiliser une étiquette de "trans::" pour utiliser les traductions définies par le système. Par exemple : utiliser "trans::common.privacy_policy" fournira le texte traduit "Privacy Policy" et "trans::common.terms_of_service" fournira le texte traduit "Terms of Service".', + 'app_footer_links_desc' => 'Ajouter des liens à afficher dans le pied de page du site. Ils seront affichés en bas de la plupart des pages, y compris celles qui ne nécessitent pas de connexion. Vous pouvez utiliser une étiquette de "trans::" pour utiliser les traductions définies par le système. Par exemple : utiliser "trans::common.privacy_policy" fournira le texte traduit "Privacy Policy" et "trans::common.terms_of_service" fournira le texte traduit "Terms of Service".', 'app_footer_links_label' => 'Libellé du lien', 'app_footer_links_url' => 'URL du lien', 'app_footer_links_add' => 'Ajouter un lien en pied de page', @@ -49,11 +49,11 @@ return [ // Color settings 'content_colors' => 'Couleur du contenu', 'content_colors_desc' => 'Définit les couleurs pour tous les éléments de la hiérarchie d\'organisation des pages. Choisir les couleurs avec une luminosité similaire aux couleurs par défaut est recommandé pour la lisibilité.', - 'bookshelf_color' => 'Couleur de l\'étagère', - 'book_color' => 'Couleur du livre', - 'chapter_color' => 'Couleur du chapitre', - 'page_color' => 'Couleur de la page', - 'page_draft_color' => 'Couleur du brouillon', + 'bookshelf_color' => 'Couleur des étagères', + 'book_color' => 'Couleur des livres', + 'chapter_color' => 'Couleur des chapitres', + 'page_color' => 'Couleur des pages', + 'page_draft_color' => 'Couleur des brouillons', // Registration Settings 'reg_settings' => 'Préférence pour l\'inscription', @@ -72,19 +72,19 @@ return [ // Maintenance settings 'maint' => 'Maintenance', 'maint_image_cleanup' => 'Nettoyer les images', - 'maint_image_cleanup_desc' => "Scan le contenu des pages et des révisions pour vérifier les images et les dessins en cours d'utilisation et lesquels sont redondant. Veuillez à faire une sauvegarde de la base de données et des images avant de lancer ceci.", + 'maint_image_cleanup_desc' => "Scanne le contenu des pages et des révisions pour vérifier les images, les dessins en cours d'utilisation et les doublons. Assurez-vous d'avoir une sauvegarde de la base de données et des images avant de lancer ceci.", 'maint_delete_images_only_in_revisions' => 'Supprimer également les images qui n\'existent que dans les anciennes révisions de page', 'maint_image_cleanup_run' => 'Lancer le nettoyage', - 'maint_image_cleanup_warning' => ':count images potentiellement inutilisées trouvées. Etes-vous sûr de vouloir supprimer ces images ?', + 'maint_image_cleanup_warning' => ':count images potentiellement inutilisées trouvées. Êtes-vous sûr de vouloir supprimer ces images ?', 'maint_image_cleanup_success' => ':count images potentiellement inutilisées trouvées et supprimées !', 'maint_image_cleanup_nothing_found' => 'Aucune image inutilisée trouvée, rien à supprimer !', - 'maint_send_test_email' => 'Envoyer un email de test', + 'maint_send_test_email' => 'Envoyer un e-mail de test', 'maint_send_test_email_desc' => 'Ceci envoie un e-mail de test à votre adresse e-mail spécifiée dans votre profil.', - 'maint_send_test_email_run' => 'Envoyer un email de test', - 'maint_send_test_email_success' => 'Email envoyé à :address', - 'maint_send_test_email_mail_subject' => 'Email de test', - 'maint_send_test_email_mail_greeting' => 'La livraison d\'email semble fonctionner !', - 'maint_send_test_email_mail_text' => 'Félicitations ! Lorsque vous avez reçu cette notification par courriel, vos paramètres d\'email semblent être configurés correctement.', + 'maint_send_test_email_run' => 'Envoyer un e-mail de test', + 'maint_send_test_email_success' => 'E-mail envoyé à :address', + 'maint_send_test_email_mail_subject' => 'E-mail de test', + 'maint_send_test_email_mail_greeting' => 'L\'envoi d\'e-mail semble fonctionner !', + 'maint_send_test_email_mail_text' => 'Félicitations ! Comme vous avez bien reçu cette notification, vos paramètres d\'e-mail semblent être configurés correctement.', 'maint_recycle_bin_desc' => 'Les étagères, livres, chapitres et pages supprimés sont envoyés dans la corbeille afin qu\'ils puissent être restaurés ou supprimés définitivement. Les éléments plus anciens de la corbeille peuvent être supprimés automatiquement après un certain temps selon la configuration du système.', 'maint_recycle_bin_open' => 'Ouvrir la corbeille', @@ -98,7 +98,7 @@ return [ 'recycle_bin_permanently_delete' => 'Supprimer définitivement', 'recycle_bin_restore' => 'Restaurer', 'recycle_bin_contents_empty' => 'La corbeille est vide', - 'recycle_bin_empty' => 'Vider la Corbeille', + 'recycle_bin_empty' => 'Vider la corbeille', 'recycle_bin_empty_confirm' => 'Cela détruira définitivement tous les éléments de la corbeille, y compris le contenu contenu de chaque élément. Êtes-vous sûr de vouloir vider la corbeille ?', 'recycle_bin_destroy_confirm' => 'Cette action supprimera définitivement cet élément, ainsi que tous les éléments enfants listés ci-dessous du système et vous ne pourrez pas restaurer ce contenu. Êtes-vous sûr de vouloir supprimer définitivement cet élément ?', 'recycle_bin_destroy_list' => 'Éléments à détruire', @@ -117,9 +117,10 @@ return [ 'audit_deleted_item' => 'Élément supprimé', 'audit_deleted_item_name' => 'Nom: :name', 'audit_table_user' => 'Utilisateur', - 'audit_table_event' => 'Evènement', - 'audit_table_related' => 'Élément ou détail lié', - 'audit_table_date' => 'Date d\'activation', + 'audit_table_event' => 'Événement', + 'audit_table_related' => 'Élément concerné ou action réalisée', + 'audit_table_ip' => 'Adresse IP', + 'audit_table_date' => 'Horodatage', 'audit_date_from' => 'À partir du', 'audit_date_to' => 'Jusqu\'au', @@ -150,7 +151,7 @@ return [ 'role_manage_settings' => 'Gérer les préférences de l\'application', 'role_export_content' => 'Exporter le contenu', 'role_asset' => 'Permissions des ressources', - 'roles_system_warning' => 'Sachez que l\'accès à l\'une des trois permissions ci-dessus peut permettre à un utilisateur de modifier ses propres privilèges ou les privilèges des autres utilisateurs du système. Attribuer uniquement des rôles avec ces permissions à des utilisateurs de confiance.', + 'roles_system_warning' => 'Sachez que l\'accès à l\'une des trois permissions ci-dessus peut permettre à un utilisateur de modifier ses propres privilèges ou les privilèges des autres utilisateurs du système. N\'attribuez uniquement des rôles avec ces permissions qu\'à des utilisateurs de confiance.', 'role_asset_desc' => 'Ces permissions contrôlent l\'accès par défaut des ressources dans le système. Les permissions dans les livres, les chapitres et les pages ignoreront ces permissions', 'role_asset_admins' => 'Les administrateurs ont automatiquement accès à tous les contenus mais les options suivantes peuvent afficher ou masquer certaines options de l\'interface.', 'role_all' => 'Tous', @@ -165,7 +166,7 @@ return [ 'users' => 'Utilisateurs', 'user_profile' => 'Profil d\'utilisateur', 'users_add_new' => 'Ajouter un nouvel utilisateur', - 'users_search' => 'Chercher les utilisateurs', + 'users_search' => 'Rechercher les utilisateurs', 'users_latest_activity' => 'Dernière activité', 'users_details' => 'Informations de l\'utilisateur', 'users_details_desc' => 'Définissez un nom et une adresse e-mail pour cet utilisateur. L\'adresse e-mail sera utilisée pour se connecter à l\'application.', @@ -173,8 +174,8 @@ return [ 'users_role' => 'Rôles de l\'utilisateur', 'users_role_desc' => 'Sélectionnez les rôles auxquels cet utilisateur sera affecté. Si un utilisateur est affecté à plusieurs rôles, les permissions de ces rôles s\'empileront et ils recevront toutes les capacités des rôles affectés.', 'users_password' => 'Mot de passe de l\'utilisateur', - 'users_password_desc' => 'Définissez un mot de passe utilisé pour vous connecter à l\'application. Il doit comporter au moins 5 caractères.', - 'users_send_invite_text' => 'Vous pouvez choisir d\'envoyer à cet utilisateur un email d\'invitation qui lui permet de définir son propre mot de passe, sinon vous pouvez définir son mot de passe vous-même.', + 'users_password_desc' => 'Définissez un mot de passe utilisé pour vous connecter à l\'application. Il doit comporter au moins 6 caractères.', + 'users_send_invite_text' => 'Vous pouvez choisir d\'envoyer à cet utilisateur un e-mail d\'invitation qui lui permet de définir son propre mot de passe, sinon vous pouvez définir son mot de passe vous-même.', 'users_send_invite_option' => 'Envoyer l\'e-mail d\'invitation', 'users_external_auth_id' => 'Identifiant d\'authentification externe', 'users_external_auth_id_desc' => 'C\'est l\'ID utilisé pour correspondre à cet utilisateur lors de la communication avec votre système d\'authentification externe.', @@ -184,9 +185,9 @@ return [ 'users_delete_named' => 'Supprimer l\'utilisateur :userName', 'users_delete_warning' => 'Ceci va supprimer \':userName\' du système.', 'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?', - 'users_migrate_ownership' => 'Migré propriété', + 'users_migrate_ownership' => 'Transférer la propriété', 'users_migrate_ownership_desc' => 'Sélectionnez un utilisateur ici si vous voulez qu\'un autre utilisateur devienne le propriétaire de tous les éléments actuellement détenus par cet utilisateur.', - 'users_none_selected' => 'Aucun utilisateur n\'a été séléctionné', + 'users_none_selected' => 'Aucun utilisateur n\'a été sélectionné', 'users_delete_success' => 'Utilisateur supprimé avec succès', 'users_edit' => 'Modifier l\'utilisateur', 'users_edit_profile' => 'Modifier le profil', @@ -195,19 +196,19 @@ return [ 'users_avatar_desc' => 'Cette image doit être un carré d\'environ 256 px.', 'users_preferred_language' => 'Langue préférée', 'users_preferred_language_desc' => 'Cette option changera la langue utilisée pour l\'interface utilisateur de l\'application. Ceci n\'affectera aucun contenu créé par l\'utilisateur.', - 'users_social_accounts' => 'Comptes sociaux', + 'users_social_accounts' => 'Réseaux sociaux', 'users_social_accounts_info' => 'Vous pouvez connecter des réseaux sociaux à votre compte pour vous connecter plus rapidement. Déconnecter un compte n\'enlèvera pas les accès autorisés précédemment sur votre compte de réseau social.', 'users_social_connect' => 'Connecter le compte', 'users_social_disconnect' => 'Déconnecter le compte', 'users_social_connected' => 'Votre compte :socialAccount a été ajouté avec succès.', 'users_social_disconnected' => 'Votre compte :socialAccount a été déconnecté avec succès', - 'users_api_tokens' => 'Jetons de l\'API', + 'users_api_tokens' => 'Jetons API', 'users_api_tokens_none' => 'Aucun jeton API n\'a été créé pour cet utilisateur', 'users_api_tokens_create' => 'Créer un jeton', 'users_api_tokens_expires' => 'Expiré', 'users_api_tokens_docs' => 'Documentation de l\'API', 'users_mfa' => 'Authentification multi-facteurs', - 'users_mfa_desc' => 'Configurez l\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.', + 'users_mfa_desc' => 'Configurer l\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.', 'users_mfa_x_methods' => ':count méthode configurée|:count méthodes configurées', 'users_mfa_configure' => 'Méthode de configuration', @@ -218,19 +219,19 @@ return [ 'user_api_token_expiry' => 'Date d\'expiration', 'user_api_token_expiry_desc' => 'Définissez une date à laquelle ce jeton expire. Après cette date, les demandes effectuées à l\'aide de ce jeton ne fonctionneront plus. Le fait de laisser ce champ vide entraînera une expiration dans 100 ans.', 'user_api_token_create_secret_message' => 'Immédiatement après la création de ce jeton, un "ID de jeton" "et" Secret de jeton "sera généré et affiché. Le secret ne sera affiché qu\'une seule fois, alors assurez-vous de copier la valeur dans un endroit sûr et sécurisé avant de continuer.', - 'user_api_token_create_success' => 'L\'API token a été créé avec succès', - 'user_api_token_update_success' => 'L\'API token a été mis à jour avec succès', - 'user_api_token' => 'Token API', + 'user_api_token_create_success' => 'Le jeton API a été créé avec succès', + 'user_api_token_update_success' => 'Le jeton API a été mis à jour avec succès', + 'user_api_token' => 'Jeton API', 'user_api_token_id' => 'Token ID', 'user_api_token_id_desc' => 'Il s\'agit d\'un identifiant généré par le système non modifiable pour ce jeton qui devra être fourni dans les demandes d\'API.', 'user_api_token_secret' => 'Token Secret', 'user_api_token_secret_desc' => 'Il s\'agit d\'un secret généré par le système pour ce jeton, qui devra être fourni dans les demandes d\'API. Cela ne sera affiché qu\'une seule fois, alors copiez cette valeur dans un endroit sûr et sécurisé.', 'user_api_token_created' => 'Jeton créé :timeAgo', 'user_api_token_updated' => 'Jeton mis à jour :timeAgo', - 'user_api_token_delete' => 'Supprimer le Token', + 'user_api_token_delete' => 'Supprimer le jeton', 'user_api_token_delete_warning' => 'Cela supprimera complètement le jeton d\'API avec le nom \':tokenName\'.', - 'user_api_token_delete_confirm' => 'Souhaitez-vous vraiment effacer l\'API Token ?', - 'user_api_token_delete_success' => 'L\'API token a été supprimé avec succès', + 'user_api_token_delete_confirm' => 'Souhaitez-vous vraiment effacer ce jeton API ?', + 'user_api_token_delete_success' => 'Le jeton API a été supprimé avec succès', //! If editing translations files directly please ignore this in all //! languages apart from en. Content will be auto-copied from en. diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php index ed1964f06..71a5e8e08 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -19,7 +19,7 @@ return [ 'before' => ':attribute doit être inférieur à :date.', 'between' => [ 'numeric' => ':attribute doit être compris entre :min et :max.', - 'file' => ':attribute doit être compris entre :min et :max kilobytes.', + 'file' => ':attribute doit être compris entre :min et :max Ko.', 'string' => ':attribute doit être compris entre :min et :max caractères.', 'array' => ':attribute doit être compris entre :min et :max éléments.', ], @@ -35,13 +35,13 @@ return [ 'filled' => ':attribute est un champ requis.', 'gt' => [ 'numeric' => ':attribute doit être plus grand que :value.', - 'file' => ':attribute doit être plus grand que :value kilobytes.', + 'file' => ':attribute doit être plus grand que :value Ko.', 'string' => ':attribute doit être plus grand que :value caractères.', 'array' => ':attribute doit avoir plus que :value éléments.', ], 'gte' => [ 'numeric' => ':attribute doit être plus grand ou égal à :value.', - 'file' => ':attribute doit être plus grand ou égal à :value kilobytes.', + 'file' => ':attribute doit être plus grand ou égal à :value Ko.', 'string' => ':attribute doit être plus grand ou égal à :value caractères.', 'array' => ':attribute doit avoir :value éléments ou plus.', ], @@ -53,22 +53,22 @@ return [ 'ip' => ':attribute doit être une adresse IP valide.', 'ipv4' => ':attribute doit être une adresse IPv4 valide.', 'ipv6' => ':attribute doit être une adresse IPv6 valide.', - 'json' => ':attribute doit être une chaine JSON valide.', + 'json' => ':attribute doit être une chaîne JSON valide.', 'lt' => [ 'numeric' => ':attribute doit être plus petit que :value.', - 'file' => ':attribute doit être plus petit que :value kilobytes.', + 'file' => ':attribute doit être plus petit que :value Ko.', 'string' => ':attribute doit être plus petit que :value caractères.', 'array' => ':attribute doit avoir moins de :value éléments.', ], 'lte' => [ 'numeric' => ':attribute doit être plus petit ou égal à :value.', - 'file' => ':attribute doit être plus petit ou égal à :value kilobytes.', + 'file' => ':attribute doit être plus petit ou égal à :value Ko.', 'string' => ':attribute doit être plus petit ou égal à :value caractères.', 'array' => ':attribute ne doit pas avoir plus de :value éléments.', ], 'max' => [ 'numeric' => ':attribute ne doit pas excéder :max.', - 'file' => ':attribute ne doit pas excéder :max kilobytes.', + 'file' => ':attribute ne doit pas excéder :max Ko.', 'string' => ':attribute ne doit pas excéder :max caractères.', 'array' => ':attribute ne doit pas contenir plus de :max éléments.', ], diff --git a/resources/lang/he/settings.php b/resources/lang/he/settings.php index c3b37eecf..e15886779 100755 --- a/resources/lang/he/settings.php +++ b/resources/lang/he/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'משתמש', 'audit_table_event' => 'אירוע', 'audit_table_related' => 'פריט או פרט קשור', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'זמן הפעילות', 'audit_date_from' => 'טווח תאריכים החל מ...', 'audit_date_to' => 'טווח תאריכים עד ל...', diff --git a/resources/lang/hr/settings.php b/resources/lang/hr/settings.php index eaed1a577..547f27a83 100644 --- a/resources/lang/hr/settings.php +++ b/resources/lang/hr/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Korisnik', 'audit_table_event' => 'Događaj', 'audit_table_related' => 'Povezana stavka ili detalj', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Datum aktivnosti', 'audit_date_from' => 'Rangiraj datum od', 'audit_date_to' => 'Rangiraj datum do', diff --git a/resources/lang/hu/settings.php b/resources/lang/hu/settings.php index ef8c506c0..9cc3f840d 100644 --- a/resources/lang/hu/settings.php +++ b/resources/lang/hu/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Felhasználó', 'audit_table_event' => 'Esemény', 'audit_table_related' => 'Related Item or Detail', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Activity Date', 'audit_date_from' => 'Date Range From', 'audit_date_to' => 'Date Range To', diff --git a/resources/lang/id/settings.php b/resources/lang/id/settings.php index 105377e0c..c01cbdb01 100644 --- a/resources/lang/id/settings.php +++ b/resources/lang/id/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Pengguna', 'audit_table_event' => 'Peristiwa', 'audit_table_related' => 'Item atau Detail Terkait', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Tanggal Kegiatan', 'audit_date_from' => 'Rentang Tanggal Dari', 'audit_date_to' => 'Rentang Tanggal Sampai', diff --git a/resources/lang/it/auth.php b/resources/lang/it/auth.php index 3e384a797..3e1500a6f 100755 --- a/resources/lang/it/auth.php +++ b/resources/lang/it/auth.php @@ -76,12 +76,12 @@ return [ 'user_invite_success' => 'Password impostata, ora hai accesso a :appName!', // Multi-factor Authentication - 'mfa_setup' => 'Setup Multi-Factor Authentication', - 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', - 'mfa_setup_configured' => 'Already configured', - 'mfa_setup_reconfigure' => 'Reconfigure', - 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', - 'mfa_setup_action' => 'Setup', + 'mfa_setup' => 'Imposta Autenticazione Multi-Fattore', + 'mfa_setup_desc' => 'Imposta l\'autenticazione multi-fattore come misura di sicurezza aggiuntiva per il tuo account.', + 'mfa_setup_configured' => 'Già configurata', + 'mfa_setup_reconfigure' => 'Riconfigura', + 'mfa_setup_remove_confirmation' => 'Sei sicuro di voler rimuovere questo metodo di autenticazione multi-fattore?', + 'mfa_setup_action' => 'Imposta', 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', 'mfa_option_totp_title' => 'Mobile App', 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', @@ -108,5 +108,5 @@ return [ 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', - 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', + 'mfa_setup_login_notification' => 'Metodo multi-fattore configurato, si prega di effettuare nuovamente il login utilizzando il metodo configurato.', ]; \ No newline at end of file diff --git a/resources/lang/it/common.php b/resources/lang/it/common.php index 76ee14bc4..bcd3aadf9 100755 --- a/resources/lang/it/common.php +++ b/resources/lang/it/common.php @@ -39,7 +39,7 @@ return [ 'reset' => 'Azzera', 'remove' => 'Rimuovi', 'add' => 'Aggiungi', - 'configure' => 'Configure', + 'configure' => 'Configura', 'fullscreen' => 'Schermo intero', 'favourite' => 'Aggiungi ai Preferiti', 'unfavourite' => 'Rimuovi dai preferiti', diff --git a/resources/lang/it/settings.php b/resources/lang/it/settings.php index 0709bb132..c5e016b35 100755 --- a/resources/lang/it/settings.php +++ b/resources/lang/it/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Utente', 'audit_table_event' => 'Evento', 'audit_table_related' => 'Elemento o Dettaglio correlato', + 'audit_table_ip' => 'Indirizzo IP', 'audit_table_date' => 'Data attività', 'audit_date_from' => 'Dalla data', 'audit_date_to' => 'Alla data', diff --git a/resources/lang/ja/settings.php b/resources/lang/ja/settings.php index 17532de41..c769174e7 100644 --- a/resources/lang/ja/settings.php +++ b/resources/lang/ja/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'User', 'audit_table_event' => 'Event', 'audit_table_related' => 'Related Item or Detail', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Activity Date', 'audit_date_from' => 'Date Range From', 'audit_date_to' => 'Date Range To', diff --git a/resources/lang/ko/settings.php b/resources/lang/ko/settings.php index 46856cfe4..6c81bc735 100755 --- a/resources/lang/ko/settings.php +++ b/resources/lang/ko/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => '사용자', 'audit_table_event' => '이벤트', 'audit_table_related' => 'Related Item or Detail', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => '활동 날짜', 'audit_date_from' => '날짜 범위 시작', 'audit_date_to' => '날짜 범위 끝', diff --git a/resources/lang/lt/settings.php b/resources/lang/lt/settings.php index e98f4b493..f5795edbc 100644 --- a/resources/lang/lt/settings.php +++ b/resources/lang/lt/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Naudotojas', 'audit_table_event' => 'Įvykis', 'audit_table_related' => 'Susijęs elementas arba detalė', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Veiklos data', 'audit_date_from' => 'Datos seka nuo', 'audit_date_to' => 'Datos seka iki', diff --git a/resources/lang/lv/settings.php b/resources/lang/lv/settings.php index 88614da5b..0108a9aa5 100644 --- a/resources/lang/lv/settings.php +++ b/resources/lang/lv/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Lietotājs', 'audit_table_event' => 'Notikums', 'audit_table_related' => 'Saistīta vienība vai detaļa', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Notikuma datums', 'audit_date_from' => 'Datums no', 'audit_date_to' => 'Datums līdz', diff --git a/resources/lang/nb/settings.php b/resources/lang/nb/settings.php index 6410e9082..cfa82f87c 100644 --- a/resources/lang/nb/settings.php +++ b/resources/lang/nb/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Kontoholder', 'audit_table_event' => 'Hendelse', 'audit_table_related' => 'Relaterte elementer eller detaljer', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Aktivitetsdato', 'audit_date_from' => 'Datoperiode fra', 'audit_date_to' => 'Datoperiode til', diff --git a/resources/lang/nl/settings.php b/resources/lang/nl/settings.php index a9f317e22..1cbc677ae 100644 --- a/resources/lang/nl/settings.php +++ b/resources/lang/nl/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Gebruiker', 'audit_table_event' => 'Gebeurtenis', 'audit_table_related' => 'Gerelateerd Item of Detail', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Activiteit datum', 'audit_date_from' => 'Datum bereik vanaf', 'audit_date_to' => 'Datum bereik tot', diff --git a/resources/lang/pl/settings.php b/resources/lang/pl/settings.php index 686435927..18121a9f2 100644 --- a/resources/lang/pl/settings.php +++ b/resources/lang/pl/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Użytkownik', 'audit_table_event' => 'Wydarzenie', 'audit_table_related' => 'Powiązany element lub szczegóły', + 'audit_table_ip' => 'Adres IP', 'audit_table_date' => 'Data Aktywności', 'audit_date_from' => 'Zakres dat od', 'audit_date_to' => 'Zakres dat do', diff --git a/resources/lang/pl/validation.php b/resources/lang/pl/validation.php index b1f28b8c1..d852d46b9 100644 --- a/resources/lang/pl/validation.php +++ b/resources/lang/pl/validation.php @@ -52,7 +52,7 @@ return [ 'integer' => ':attribute musi być liczbą całkowitą.', 'ip' => ':attribute musi być prawidłowym adresem IP.', 'ipv4' => ':attribute musi być prawidłowym adresem IPv4.', - 'ipv6' => ':attribute musi być prawidłowym adresem IPv6.', + 'ipv6' => ':attribute musi być prawidłowym adresem IPv6.', 'json' => ':attribute musi być prawidłowym ciągiem JSON.', 'lt' => [ 'numeric' => ':attribute musi być mniejszy niż :value.', diff --git a/resources/lang/pt/settings.php b/resources/lang/pt/settings.php index 39f28424e..aa8450dbf 100644 --- a/resources/lang/pt/settings.php +++ b/resources/lang/pt/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Utilizador', 'audit_table_event' => 'Evento', 'audit_table_related' => 'Item ou Detalhe Relacionado', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Data da Atividade', 'audit_date_from' => 'Intervalo De', 'audit_date_to' => 'Intervalo Até', diff --git a/resources/lang/pt_BR/settings.php b/resources/lang/pt_BR/settings.php index bb1bb2f31..c5b113da3 100644 --- a/resources/lang/pt_BR/settings.php +++ b/resources/lang/pt_BR/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Usuário', 'audit_table_event' => 'Evento', 'audit_table_related' => 'Related Item or Detail', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Data da Atividade', 'audit_date_from' => 'Date Range From', 'audit_date_to' => 'Date Range To', diff --git a/resources/lang/ru/auth.php b/resources/lang/ru/auth.php index 1c9c9309d..8410e40e4 100644 --- a/resources/lang/ru/auth.php +++ b/resources/lang/ru/auth.php @@ -76,37 +76,37 @@ return [ 'user_invite_success' => 'Пароль установлен, теперь у вас есть доступ к :appName!', // Multi-factor Authentication - 'mfa_setup' => 'Setup Multi-Factor Authentication', - 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', - 'mfa_setup_configured' => 'Already configured', - 'mfa_setup_reconfigure' => 'Reconfigure', - 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', - 'mfa_setup_action' => 'Setup', - 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', - 'mfa_option_totp_title' => 'Mobile App', - 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', - 'mfa_option_backup_codes_title' => 'Backup Codes', - 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', - 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', - 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', - 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', - 'mfa_gen_backup_codes_download' => 'Download Codes', - 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', - 'mfa_gen_totp_title' => 'Mobile App Setup', - 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', - 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', - 'mfa_gen_totp_verify_setup' => 'Verify Setup', - 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', - 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', - 'mfa_verify_access' => 'Verify Access', - 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', - 'mfa_verify_no_methods' => 'No Methods Configured', - 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', - 'mfa_verify_use_totp' => 'Verify using a mobile app', - 'mfa_verify_use_backup_codes' => 'Verify using a backup code', - 'mfa_verify_backup_code' => 'Backup Code', - 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', - 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', - 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', - 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', + 'mfa_setup' => 'Двухфакторная аутентификация', + 'mfa_setup_desc' => 'Двухфакторная аутентификация повышает степень безопасности вашей учетной записи.', + 'mfa_setup_configured' => 'Настроено', + 'mfa_setup_reconfigure' => 'Перенастроить', + 'mfa_setup_remove_confirmation' => 'Вы уверены, что хотите удалить этот двухфакторный метод аутентификации?', + 'mfa_setup_action' => 'Настройка', + 'mfa_backup_codes_usage_limit_warning' => 'У вас осталось менее 5 резервных кодов, пожалуйста, создайте и сохраните новый набор перед тем, как закончатся коды, чтобы предотвратить блокировку вашей учетной записи.', + 'mfa_option_totp_title' => 'Мобильное приложение', + 'mfa_option_totp_desc' => 'Для использования двухфакторной аутентификации вам понадобится мобильное приложение, поддерживающее TOTP, например Google Authenticator, Authy или Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Резервные коды', + 'mfa_option_backup_codes_desc' => 'Безопасно хранить набор одноразовых резервных кодов, которые вы можете ввести для проверки вашей личности.', + 'mfa_gen_confirm_and_enable' => 'Подтвердить и включить', + 'mfa_gen_backup_codes_title' => 'Настройка резервных кодов', + 'mfa_gen_backup_codes_desc' => 'Сохраните приведенный ниже список кодов в безопасном месте. При доступе к системе вы сможете использовать один из кодов в качестве второго механизма аутентификации.', + 'mfa_gen_backup_codes_download' => 'Скачать коды', + 'mfa_gen_backup_codes_usage_warning' => 'Каждый код может быть использован только один раз', + 'mfa_gen_totp_title' => 'Настройка мобильного приложения', + 'mfa_gen_totp_desc' => 'Для использования двухфакторной аутентификации вам понадобится мобильное приложение, поддерживающее TOTP, например Google Authenticator, Authy или Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Отсканируйте QR-код, используя приложение для аутентификации.', + 'mfa_gen_totp_verify_setup' => 'Проверить настройки', + 'mfa_gen_totp_verify_setup_desc' => 'Проверьте, что все работает введя код, сгенерированный внутри вашего приложения для аутентификации, в поле ввода ниже:', + 'mfa_gen_totp_provide_code_here' => 'Введите код, сгенерированный приложением', + 'mfa_verify_access' => 'Подтвердите доступ', + 'mfa_verify_access_desc' => 'Ваша учетная запись требует подтверждения личности на дополнительном уровне верификации, прежде чем вам будет предоставлен доступ. Для продолжения подтвердите вход, используя один из настроенных методов.', + 'mfa_verify_no_methods' => 'Методы не настроены', + 'mfa_verify_no_methods_desc' => 'Для вашей учетной записи не найдены двухфакторные методы аутентификации. Вам нужно настроить хотя бы один метод, прежде чем получить доступ.', + 'mfa_verify_use_totp' => 'Проверить используя мобильное приложение', + 'mfa_verify_use_backup_codes' => 'Проверить используя резервный код', + 'mfa_verify_backup_code' => 'Резервный код', + 'mfa_verify_backup_code_desc' => 'Введите один из оставшихся резервных кодов ниже:', + 'mfa_verify_backup_code_enter_here' => 'Введите резервный код', + 'mfa_verify_totp_desc' => 'Введите код, сгенерированный с помощью мобильного приложения, ниже:', + 'mfa_setup_login_notification' => 'Двухфакторный метод настроен, пожалуйста, войдите снова, используя сконфигурированный метод.', ]; \ No newline at end of file diff --git a/resources/lang/ru/settings.php b/resources/lang/ru/settings.php index af37b7b67..e4bd85340 100755 --- a/resources/lang/ru/settings.php +++ b/resources/lang/ru/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Пользователь', 'audit_table_event' => 'Событие', 'audit_table_related' => 'Связанный элемент', + 'audit_table_ip' => 'IP-адрес', 'audit_table_date' => 'Дата действия', 'audit_date_from' => 'Диапазон даты от', 'audit_date_to' => 'Диапазон даты до', @@ -206,10 +207,10 @@ return [ 'users_api_tokens_create' => 'Создать токен', 'users_api_tokens_expires' => 'Истекает', 'users_api_tokens_docs' => 'Документация', - 'users_mfa' => 'Multi-Factor Authentication', - 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa' => 'Двухфакторная аутентификация', + 'users_mfa_desc' => 'Двухфакторная аутентификация повышает степень безопасности вашей учетной записи.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', - 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_configure' => 'Настройка методов', // API Tokens 'user_api_token_create' => 'Создать токен', diff --git a/resources/lang/ru/validation.php b/resources/lang/ru/validation.php index 5b3f8f766..45cc96155 100644 --- a/resources/lang/ru/validation.php +++ b/resources/lang/ru/validation.php @@ -15,7 +15,7 @@ return [ 'alpha_dash' => ':attribute может содержать только буквы, цифры и тире.', 'alpha_num' => ':attribute должен содержать только буквы и цифры.', 'array' => ':attribute должен быть массивом.', - 'backup_codes' => 'The provided code is not valid or has already been used.', + 'backup_codes' => 'Указанный код недействителен или уже использован.', 'before' => ':attribute дата должна быть до :date.', 'between' => [ 'numeric' => ':attribute должен быть между :min и :max.', @@ -99,7 +99,7 @@ return [ ], 'string' => ':attribute должен быть строкой.', 'timezone' => ':attribute должен быть корректным часовым поясом.', - 'totp' => 'The provided code is not valid or has expired.', + 'totp' => 'Указанный код недействителен или истек.', 'unique' => ':attribute уже есть.', 'url' => 'Формат :attribute некорректен.', 'uploaded' => 'Не удалось загрузить файл. Сервер не может принимать файлы такого размера.', diff --git a/resources/lang/sk/activities.php b/resources/lang/sk/activities.php index 91b9e1880..9f9ded00e 100644 --- a/resources/lang/sk/activities.php +++ b/resources/lang/sk/activities.php @@ -8,7 +8,7 @@ return [ // Pages 'page_create' => 'vytvoril(a) stránku', 'page_create_notification' => 'Stránka úspešne vytvorená', - 'page_update' => 'aktualizoval stránku', + 'page_update' => 'aktualizoval(a) stránku', 'page_update_notification' => 'Stránka úspešne aktualizovaná', 'page_delete' => 'odstránil(a) stránku', 'page_delete_notification' => 'Stránka úspešne odstránená', @@ -44,12 +44,12 @@ return [ 'bookshelf_delete_notification' => 'Knižnica úspešne odstránená', // Favourites - 'favourite_add_notification' => '":name" has been added to your favourites', - 'favourite_remove_notification' => '":name" has been removed from your favourites', + 'favourite_add_notification' => '":name" bol pridaný medzi obľúbené', + 'favourite_remove_notification' => '":name" bol odstránený z obľúbených', // MFA - 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', - 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + 'mfa_setup_method_notification' => 'Viacúrovňový spôsob overenia úspešne nastavený', + 'mfa_remove_method_notification' => 'Viacúrovňový spôsob overenia úspešne odstránený', // Other 'commented_on' => 'komentoval(a)', diff --git a/resources/lang/sk/auth.php b/resources/lang/sk/auth.php index 92cf4f648..f79e79cca 100644 --- a/resources/lang/sk/auth.php +++ b/resources/lang/sk/auth.php @@ -76,37 +76,37 @@ return [ 'user_invite_success' => 'Heslo bolo nastavené, teraz máte prístup k :appName!', // Multi-factor Authentication - 'mfa_setup' => 'Setup Multi-Factor Authentication', - 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', - 'mfa_setup_configured' => 'Already configured', - 'mfa_setup_reconfigure' => 'Reconfigure', - 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', - 'mfa_setup_action' => 'Setup', + 'mfa_setup' => 'Nastaviť viacúrovňové prihlasovanie', + 'mfa_setup_desc' => 'Pre vyššiu úroveň bezpečnosti si nastavte viacúrovňové prihlasovanie.', + 'mfa_setup_configured' => 'Už nastavené', + 'mfa_setup_reconfigure' => 'Znovunastavenie', + 'mfa_setup_remove_confirmation' => 'Ste si istý, že chcete odstrániť tento spôsob viacúrovňového overenia?', + 'mfa_setup_action' => 'Nastaveine', 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', - 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_title' => 'Mobilná aplikácia', 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', - 'mfa_option_backup_codes_title' => 'Backup Codes', - 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', - 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', - 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', - 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', - 'mfa_gen_backup_codes_download' => 'Download Codes', - 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', - 'mfa_gen_totp_title' => 'Mobile App Setup', - 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', - 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', - 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_option_backup_codes_title' => 'Záložné kódy', + 'mfa_option_backup_codes_desc' => 'Bezpečne uložte jednorázové záložné kódy pre overenie vačej identity.', + 'mfa_gen_confirm_and_enable' => 'Potvrdiť a zapnúť', + 'mfa_gen_backup_codes_title' => 'Nastavenie záložných kódov', + 'mfa_gen_backup_codes_desc' => 'Uložte si tieto kódy na bezpečné miesto. Jeden z kódov budete môcť použiť ako druhý faktor overenia identiy na prihlásenie sa.', + 'mfa_gen_backup_codes_download' => 'Stiahnuť kódy', + 'mfa_gen_backup_codes_usage_warning' => 'Každý kód môže byť použitý len jeden krát', + 'mfa_gen_totp_title' => 'Nastavenie mobilnej aplikácie', + 'mfa_gen_totp_desc' => 'Pre používanie viacúrovňového prihlasovania budete potrebovať mobilnú aplikáciu, ktorá podporuje TOPS ako napríklad Google Authenticator, Authy alebo Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Naskenujte 1R k\'d pomocou vašej mobilnej aplikácie.', + 'mfa_gen_totp_verify_setup' => 'Overiť nastavenie', 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', - 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', - 'mfa_verify_access' => 'Verify Access', + 'mfa_gen_totp_provide_code_here' => 'Sem vložte kód vygenerovaný vašou mobilnou aplikáciou', + 'mfa_verify_access' => 'Overiť prístup', 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', - 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods' => 'Žiadny spôsob nebol nastavený', 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', - 'mfa_verify_use_totp' => 'Verify using a mobile app', - 'mfa_verify_use_backup_codes' => 'Verify using a backup code', - 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_use_totp' => 'Overiť pomocou mobilnej aplikácie', + 'mfa_verify_use_backup_codes' => 'Overiť pomocou záložného kódu', + 'mfa_verify_backup_code' => 'Záložný kód', 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', - 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', - 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_verify_backup_code_enter_here' => 'Zadajte záložný kód', + 'mfa_verify_totp_desc' => 'Zadajte kód vygenerovaný vašou mobilnou aplikáciou:', 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/sk/common.php b/resources/lang/sk/common.php index aab959305..b9913db59 100644 --- a/resources/lang/sk/common.php +++ b/resources/lang/sk/common.php @@ -39,12 +39,12 @@ return [ 'reset' => 'Resetovať', 'remove' => 'Odstrániť', 'add' => 'Pridať', - 'configure' => 'Configure', + 'configure' => 'Konfigurácia', 'fullscreen' => 'Celá obrazovka', - 'favourite' => 'Favourite', - 'unfavourite' => 'Unfavourite', - 'next' => 'Next', - 'previous' => 'Previous', + 'favourite' => 'Pridať do obľúbených', + 'unfavourite' => 'Odstrániť z obľúbených', + 'next' => 'Ďalej', + 'previous' => 'Späť', // Sort Options 'sort_options' => 'Možnosti triedenia', @@ -52,7 +52,7 @@ return [ 'sort_ascending' => 'Zoradiť vzostupne', 'sort_descending' => 'Zoradiť zostupne', 'sort_name' => 'Meno', - 'sort_default' => 'Default', + 'sort_default' => 'Východzie', 'sort_created_at' => 'Dátum vytvorenia', 'sort_updated_at' => 'Aktualizované dňa', @@ -61,7 +61,7 @@ return [ 'no_activity' => 'Žiadna aktivita na zobrazenie', 'no_items' => 'Žiadne položky nie sú dostupné', 'back_to_top' => 'Späť nahor', - 'skip_to_main_content' => 'Skip to main content', + 'skip_to_main_content' => 'Preskočiť na hlavný obsah', 'toggle_details' => 'Prepnúť detaily', 'toggle_thumbnails' => 'Prepnúť náhľady', 'details' => 'Podrobnosti', @@ -71,7 +71,7 @@ return [ 'breadcrumb' => 'Breadcrumb', // Header - 'header_menu_expand' => 'Expand Header Menu', + 'header_menu_expand' => 'Rozbaliť menu v záhlaví', 'profile_menu' => 'Menu profilu', 'view_profile' => 'Zobraziť profil', 'edit_profile' => 'Upraviť profil', @@ -80,9 +80,9 @@ return [ // Layout tabs 'tab_info' => 'Informácie', - 'tab_info_label' => 'Tab: Show Secondary Information', + 'tab_info_label' => 'Tab: Zobraziť vedľajšie informácie', 'tab_content' => 'Obsah', - 'tab_content_label' => 'Tab: Show Primary Content', + 'tab_content_label' => 'Tab: Zobraziť hlavné informácie', // Email Content 'email_action_help' => 'Ak máte problém klinkúť na tlačidlo ":actionText", skopírujte a vložte URL uvedenú nižšie do Vášho prehliadača:', @@ -90,6 +90,6 @@ return [ // Footer Link Options // Not directly used but available for convenience to users. - 'privacy_policy' => 'Privacy Policy', - 'terms_of_service' => 'Terms of Service', + 'privacy_policy' => 'Zásady ochrany osobných údajov', + 'terms_of_service' => 'Podmienky používania', ]; diff --git a/resources/lang/sk/entities.php b/resources/lang/sk/entities.php index 1e97e8c03..0d430dcf6 100644 --- a/resources/lang/sk/entities.php +++ b/resources/lang/sk/entities.php @@ -22,13 +22,13 @@ return [ 'meta_created_name' => 'Vytvorené :timeLength používateľom :user', 'meta_updated' => 'Aktualizované :timeLength', 'meta_updated_name' => 'Aktualizované :timeLength používateľom :user', - 'meta_owned_name' => 'Owned by :user', + 'meta_owned_name' => 'Vlastník :user', 'entity_select' => 'Entita vybraná', 'images' => 'Obrázky', 'my_recent_drafts' => 'Moje nedávne koncepty', 'my_recently_viewed' => 'Nedávno mnou zobrazené', - 'my_most_viewed_favourites' => 'My Most Viewed Favourites', - 'my_favourites' => 'My Favourites', + 'my_most_viewed_favourites' => 'Moje najčastejšie zobrazené obľubené', + 'my_favourites' => 'Moje obľúbené', 'no_pages_viewed' => 'Nepozreli ste si žiadne stránky', 'no_pages_recently_created' => 'Žiadne stránky neboli nedávno vytvorené', 'no_pages_recently_updated' => 'Žiadne stránky neboli nedávno aktualizované', @@ -36,14 +36,14 @@ return [ 'export_html' => 'Obsahovaný webový súbor', 'export_pdf' => 'PDF súbor', 'export_text' => 'Súbor s čistým textom', - 'export_md' => 'Markdown File', + 'export_md' => 'Súbor Markdown', // Permissions and restrictions 'permissions' => 'Oprávnenia', 'permissions_intro' => 'Ak budú tieto oprávnenia povolené, budú mať prioritu pred oprávneniami roly.', 'permissions_enable' => 'Povoliť vlastné oprávnenia', 'permissions_save' => 'Uložiť oprávnenia', - 'permissions_owner' => 'Owner', + 'permissions_owner' => 'Vlastník', // Search 'search_results' => 'Výsledky hľadania', @@ -63,7 +63,7 @@ return [ 'search_permissions_set' => 'Oprávnenia', 'search_created_by_me' => 'Vytvorené mnou', 'search_updated_by_me' => 'Aktualizované mnou', - 'search_owned_by_me' => 'Owned by me', + 'search_owned_by_me' => 'Patriace mne', 'search_date_options' => 'Možnosti dátumu', 'search_updated_before' => 'Aktualizované pred', 'search_updated_after' => 'Aktualizované po', @@ -100,22 +100,22 @@ return [ 'shelves_permissions_updated' => 'Oprávnenia knižnice aktualizované', 'shelves_permissions_active' => 'Oprávnenia knižnice aktívne', 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', - 'shelves_copy_permissions_to_books' => 'Copy Permissions to Books', - 'shelves_copy_permissions' => 'Copy Permissions', + 'shelves_copy_permissions_to_books' => 'Kopírovať oprávnenia pre knihy', + 'shelves_copy_permissions' => 'Kopírovať oprávnenia', 'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.', - 'shelves_copy_permission_success' => 'Bookshelf permissions copied to :count books', + 'shelves_copy_permission_success' => 'Oprávnenia knižnice boli skopírované {0}:count kníh|{1}:count kniha|[2,3,4]:count knihy|[5,*]:count kníh', // Books 'book' => 'Kniha', 'books' => 'Knihy', - 'x_books' => ':count Book|:count Books', + 'x_books' => '{0}:count kníh|{1}:count kniha|[2,3,4]:count knihy|[5,*]:count kníh', 'books_empty' => 'Žiadne knihy neboli vytvorené', 'books_popular' => 'Populárne knihy', 'books_recent' => 'Nedávne knihy', - 'books_new' => 'New Books', - 'books_new_action' => 'New Book', + 'books_new' => 'Nové knihy', + 'books_new_action' => 'Nová kniha', 'books_popular_empty' => 'Najpopulárnejšie knihy sa objavia tu.', - 'books_new_empty' => 'The most recently created books will appear here.', + 'books_new_empty' => 'Najnovšie knihy sa zobrazia tu.', 'books_create' => 'Vytvoriť novú knihu', 'books_delete' => 'Zmazať knihu', 'books_delete_named' => 'Zmazať knihu :bookName', @@ -136,24 +136,24 @@ return [ 'books_navigation' => 'Navigácia knihy', 'books_sort' => 'Zoradiť obsah knihy', 'books_sort_named' => 'Zoradiť knihu :bookName', - 'books_sort_name' => 'Sort by Name', - 'books_sort_created' => 'Sort by Created Date', - 'books_sort_updated' => 'Sort by Updated Date', - 'books_sort_chapters_first' => 'Chapters First', - 'books_sort_chapters_last' => 'Chapters Last', + 'books_sort_name' => 'Zoradiť podľa mena', + 'books_sort_created' => 'Zoradiť podľa dátumu vytvorenia', + 'books_sort_updated' => 'Zoradiť podľa dátumu aktualizácie', + 'books_sort_chapters_first' => 'Kapitoly ako prvé', + 'books_sort_chapters_last' => 'Kapitoly ako posledné', 'books_sort_show_other' => 'Zobraziť ostatné knihy', 'books_sort_save' => 'Uložiť nové zoradenie', // Chapters 'chapter' => 'Kapitola', 'chapters' => 'Kapitoly', - 'x_chapters' => ':count Chapter|:count Chapters', + 'x_chapters' => '{0}:count Kapitol|{1}:count Kapitola|[2,3,4]:count Kapitoly|[5,*]:count Kapitol', 'chapters_popular' => 'Populárne kapitoly', 'chapters_new' => 'Nová kapitola', 'chapters_create' => 'Vytvoriť novú kapitolu', 'chapters_delete' => 'Zmazať kapitolu', 'chapters_delete_named' => 'Zmazať kapitolu :chapterName', - 'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.', + 'chapters_delete_explain' => 'Týmto sa odstráni kapitola s názvom \':chapterName\'. Spolu s ňou sa odstránia všetky stránky v tejto kapitole.', 'chapters_delete_confirm' => 'Ste si istý, že chcete zmazať túto kapitolu?', 'chapters_edit' => 'Upraviť kapitolu', 'chapters_edit_named' => 'Upraviť kapitolu :chapterName', @@ -165,7 +165,7 @@ return [ 'chapters_empty' => 'V tejto kapitole nie sú teraz žiadne stránky.', 'chapters_permissions_active' => 'Oprávnenia kapitoly aktívne', 'chapters_permissions_success' => 'Oprávnenia kapitoly aktualizované', - 'chapters_search_this' => 'Search this chapter', + 'chapters_search_this' => 'Hladať v kapitole', // Pages 'page' => 'Stránka', @@ -184,7 +184,7 @@ return [ 'pages_delete_confirm' => 'Ste si istý, že chcete zmazať túto stránku?', 'pages_delete_draft_confirm' => 'Ste si istý, že chcete zmazať tento koncept stránky?', 'pages_editing_named' => 'Upraviť stránku :pageName', - 'pages_edit_draft_options' => 'Draft Options', + 'pages_edit_draft_options' => 'Možnosti konceptu', 'pages_edit_save_draft' => 'Uložiť koncept', 'pages_edit_draft' => 'Upraviť koncept stránky', 'pages_editing_draft' => 'Upravuje sa koncept', @@ -202,25 +202,25 @@ return [ 'pages_md_preview' => 'Náhľad', 'pages_md_insert_image' => 'Vložiť obrázok', 'pages_md_insert_link' => 'Vložiť odkaz na entitu', - 'pages_md_insert_drawing' => 'Insert Drawing', + 'pages_md_insert_drawing' => 'Vložiť kresbu', 'pages_not_in_chapter' => 'Stránka nie je v kapitole', 'pages_move' => 'Presunúť stránku', 'pages_move_success' => 'Stránka presunutá do ":parentName"', - 'pages_copy' => 'Copy Page', - 'pages_copy_desination' => 'Copy Destination', - 'pages_copy_success' => 'Page successfully copied', + 'pages_copy' => 'Kpoírovať stránku', + 'pages_copy_desination' => 'Ciel kopírovania', + 'pages_copy_success' => 'Stránka bola skopírovaná', 'pages_permissions' => 'Oprávnenia stránky', 'pages_permissions_success' => 'Oprávnenia stránky aktualizované', - 'pages_revision' => 'Revision', + 'pages_revision' => 'Revízia', 'pages_revisions' => 'Revízie stránky', 'pages_revisions_named' => 'Revízie stránky :pageName', 'pages_revision_named' => 'Revízia stránky :pageName', - 'pages_revision_restored_from' => 'Restored from #:id; :summary', + 'pages_revision_restored_from' => 'Obnovené z #:id; :summary', 'pages_revisions_created_by' => 'Vytvoril', 'pages_revisions_date' => 'Dátum revízie', - 'pages_revisions_number' => '#', - 'pages_revisions_numbered' => 'Revision #:id', - 'pages_revisions_numbered_changes' => 'Revision #:id Changes', + 'pages_revisions_number' => 'č.', + 'pages_revisions_numbered' => 'Revízia č. :id', + 'pages_revisions_numbered_changes' => 'Zmeny revízie č. ', 'pages_revisions_changelog' => 'Záznam zmien', 'pages_revisions_changes' => 'Zmeny', 'pages_revisions_current' => 'Aktuálna verzia', @@ -242,21 +242,21 @@ return [ 'message' => ':start :time. Dávajte pozor aby ste si navzájom neprepísali zmeny!', ], 'pages_draft_discarded' => 'Koncept ostránený, aktuálny obsah stránky bol nahraný do editora', - 'pages_specific' => 'Specific Page', - 'pages_is_template' => 'Page Template', + 'pages_specific' => 'Konkrétna stránka', + 'pages_is_template' => 'Šablóna stránky', // Editor Sidebar 'page_tags' => 'Štítky stránok', - 'chapter_tags' => 'Chapter Tags', - 'book_tags' => 'Book Tags', - 'shelf_tags' => 'Shelf Tags', + 'chapter_tags' => 'Štítky kapitol', + 'book_tags' => 'Štítky kníh', + 'shelf_tags' => 'Štítky knižníc', 'tag' => 'Štítok', 'tags' => 'Štítky', - 'tag_name' => 'Tag Name', + 'tag_name' => 'Názov štítku', 'tag_value' => 'Hodnota štítku (Voliteľné)', 'tags_explain' => "Pridajte pár štítkov pre uľahčenie kategorizácie Vášho obsahu. \n Štítku môžete priradiť hodnotu pre ešte lepšiu organizáciu.", 'tags_add' => 'Pridať ďalší štítok', - 'tags_remove' => 'Remove this tag', + 'tags_remove' => 'Odstrániť tento štítok', 'attachments' => 'Prílohy', 'attachments_explain' => 'Nahrajte nejaké súbory alebo priložte zopár odkazov pre zobrazenie na Vašej stránke. Budú viditeľné v bočnom paneli.', 'attachments_explain_instant_save' => 'Zmeny budú okamžite uložené.', @@ -283,8 +283,8 @@ return [ 'attachments_file_uploaded' => 'Súbor úspešne nahraný', 'attachments_file_updated' => 'Súbor úspešne aktualizovaný', 'attachments_link_attached' => 'Odkaz úspešne pripojený k stránke', - 'templates' => 'Templates', - 'templates_set_as_template' => 'Page is a template', + 'templates' => 'Šablóny', + 'templates_set_as_template' => 'Táto stránka je šablóna', 'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.', 'templates_replace_content' => 'Replace page content', 'templates_append_content' => 'Append to page content', @@ -301,18 +301,18 @@ return [ // Comments 'comment' => 'Komentár', 'comments' => 'Komentáre', - 'comment_add' => 'Add Comment', + 'comment_add' => 'Pridať komentár', 'comment_placeholder' => 'Tu zadajte svoje pripomienky', - 'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments', + 'comment_count' => '{0} Bez komentárov|{1} 1 komentár|[2,3,4] :count komentáre|[5,*] :count komentárov', 'comment_save' => 'Uložiť komentár', - 'comment_saving' => 'Saving comment...', - 'comment_deleting' => 'Deleting comment...', - 'comment_new' => 'New Comment', - 'comment_created' => 'commented :createDiff', + 'comment_saving' => 'Ukladanie komentára...', + 'comment_deleting' => 'Mazanie komentára...', + 'comment_new' => 'Nový komentár', + 'comment_created' => 'komentované :createDiff', 'comment_updated' => 'Updated :updateDiff by :username', - 'comment_deleted_success' => 'Comment deleted', - 'comment_created_success' => 'Comment added', - 'comment_updated_success' => 'Comment updated', + 'comment_deleted_success' => 'Komentár odstránený', + 'comment_created_success' => 'Komentár pridaný', + 'comment_updated_success' => 'Komentár aktualizovaný', 'comment_delete_confirm' => 'Ste si istý, že chcete odstrániť tento komentár?', 'comment_in_reply_to' => 'Odpovedať na :commentId', diff --git a/resources/lang/sk/errors.php b/resources/lang/sk/errors.php index e523110ed..bb30243e8 100644 --- a/resources/lang/sk/errors.php +++ b/resources/lang/sk/errors.php @@ -13,7 +13,7 @@ return [ 'email_already_confirmed' => 'Email bol už overený, skúste sa prihlásiť.', 'email_confirmation_invalid' => 'Tento potvrdzujúci token nie je platný alebo už bol použitý, skúste sa prosím registrovať znova.', 'email_confirmation_expired' => 'Potvrdzujúci token expiroval, bol odoslaný nový potvrdzujúci email.', - 'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed', + 'email_confirmation_awaiting' => 'Potvrďte emailovú adresu pre užívateľský účet', 'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind', 'ldap_fail_authed' => 'LDAP access failed using given dn & password details', 'ldap_extension_not_installed' => 'LDAP PHP extension not installed', @@ -83,7 +83,7 @@ return [ '404_page_not_found' => 'Stránka nenájdená', 'sorry_page_not_found' => 'Prepáčte, stránka ktorú hľadáte nebola nájdená.', 'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.', - 'image_not_found' => 'Image Not Found', + 'image_not_found' => 'Obrázok nebol nájdený', 'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.', 'image_not_found_details' => 'If you expected this image to exist it might have been deleted.', 'return_home' => 'Vrátiť sa domov', diff --git a/resources/lang/sk/settings.php b/resources/lang/sk/settings.php index b2d3421b0..9ec036802 100644 --- a/resources/lang/sk/settings.php +++ b/resources/lang/sk/settings.php @@ -12,15 +12,15 @@ return [ 'settings_save_success' => 'Nastavenia uložené', // App Settings - 'app_customization' => 'Customization', - 'app_features_security' => 'Features & Security', + 'app_customization' => 'Prispôsobenia', + 'app_features_security' => 'Funkcie a bezpečnosť', 'app_name' => 'Názov aplikácia', 'app_name_desc' => 'Tento názov sa zobrazuje v hlavičke a v emailoch.', 'app_name_header' => 'Zobraziť názov aplikácie v hlavičke?', - 'app_public_access' => 'Public Access', + 'app_public_access' => 'Verejný prístup', 'app_public_access_desc' => 'Enabling this option will allow visitors, that are not logged-in, to access content in your BookStack instance.', 'app_public_access_desc_guest' => 'Access for public visitors can be controlled through the "Guest" user.', - 'app_public_access_toggle' => 'Allow public access', + 'app_public_access_toggle' => 'Povoliť verejný prístup', 'app_public_viewing' => 'Povoliť verejné zobrazenie?', 'app_secure_images' => 'Povoliť nahrávanie súborov so zvýšeným zabezpečením?', 'app_secure_images_toggle' => 'Enable higher security image uploads', @@ -34,20 +34,20 @@ return [ 'app_logo_desc' => 'Tento obrázok by mal mať 43px na výšku.
Veľké obrázky budú preškálované na menší rozmer.', 'app_primary_color' => 'Primárna farba pre aplikáciu', 'app_primary_color_desc' => 'Toto by mala byť hodnota v hex tvare.
Nechajte prázdne ak chcete použiť prednastavenú farbu.', - 'app_homepage' => 'Application Homepage', + 'app_homepage' => 'Domovská stránka aplikácie', 'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.', - 'app_homepage_select' => 'Select a page', - 'app_footer_links' => 'Footer Links', + 'app_homepage_select' => 'Vybrať stránku', + 'app_footer_links' => 'Odkazy v pätičke', 'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of "trans::" to use system-defined translations. For example: Using "trans::common.privacy_policy" will provide the translated text "Privacy Policy" and "trans::common.terms_of_service" will provide the translated text "Terms of Service".', 'app_footer_links_label' => 'Link Label', 'app_footer_links_url' => 'Link URL', 'app_footer_links_add' => 'Add Footer Link', 'app_disable_comments' => 'Zakázať komentáre', - 'app_disable_comments_toggle' => 'Disable comments', + 'app_disable_comments_toggle' => 'Vypnúť komentáre', 'app_disable_comments_desc' => 'Zakázať komentáre na všetkých stránkach aplikácie. Existujúce komentáre sa nezobrazujú.', // Color settings - 'content_colors' => 'Content Colors', + 'content_colors' => 'Farby obsahu', 'content_colors_desc' => 'Sets colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.', 'bookshelf_color' => 'Shelf Color', 'book_color' => 'Book Color', @@ -57,12 +57,12 @@ return [ // Registration Settings 'reg_settings' => 'Nastavenia registrácie', - 'reg_enable' => 'Enable Registration', - 'reg_enable_toggle' => 'Enable registration', + 'reg_enable' => 'Povolenie registrácie', + 'reg_enable_toggle' => 'Povoliť registrácie', 'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.', 'reg_default_role' => 'Prednastavená používateľská rola po registrácii', 'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.', - 'reg_email_confirmation' => 'Email Confirmation', + 'reg_email_confirmation' => 'Potvrdenie e-mailom', 'reg_email_confirmation_toggle' => 'Require email confirmation', 'reg_confirm_email_desc' => 'Ak je použité obmedzenie domény, potom bude vyžadované overenie emailu a hodnota nižšie bude ignorovaná.', 'reg_confirm_restrict_domain' => 'Obmedziť registráciu na doménu', @@ -70,28 +70,28 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Nie sú nastavené žiadne obmedzenia', // Maintenance settings - 'maint' => 'Maintenance', - 'maint_image_cleanup' => 'Cleanup Images', + 'maint' => 'Údržba', + 'maint_image_cleanup' => 'Prečistenie obrázkov', 'maint_image_cleanup_desc' => "Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.", 'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions', - 'maint_image_cleanup_run' => 'Run Cleanup', + 'maint_image_cleanup_run' => 'Spustiť prečistenie', 'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?', 'maint_image_cleanup_success' => ':count potentially unused images found and deleted!', - 'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!', - 'maint_send_test_email' => 'Send a Test Email', + 'maint_image_cleanup_nothing_found' => 'Žiadne nepoužit obrázky neboli nájdené. Nič sa nezmazalo!', + 'maint_send_test_email' => 'Odoslať testovací email', 'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.', - 'maint_send_test_email_run' => 'Send test email', + 'maint_send_test_email_run' => 'Odoslať testovací email', 'maint_send_test_email_success' => 'Email sent to :address', - 'maint_send_test_email_mail_subject' => 'Test Email', + 'maint_send_test_email_mail_subject' => 'Testovací email', 'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!', 'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.', 'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.', - 'maint_recycle_bin_open' => 'Open Recycle Bin', + 'maint_recycle_bin_open' => 'Otvoriť kôš', // Recycle Bin - 'recycle_bin' => 'Recycle Bin', + 'recycle_bin' => 'Kôš', 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', - 'recycle_bin_deleted_item' => 'Deleted Item', + 'recycle_bin_deleted_item' => 'Odstránené položky', 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Deleted By', 'recycle_bin_deleted_at' => 'Deletion Time', @@ -116,10 +116,11 @@ return [ 'audit_event_filter_no_filter' => 'No Filter', 'audit_deleted_item' => 'Deleted Item', 'audit_deleted_item_name' => 'Name: :name', - 'audit_table_user' => 'User', - 'audit_table_event' => 'Event', + 'audit_table_user' => 'Užívateľ', + 'audit_table_event' => 'Udalosť', 'audit_table_related' => 'Related Item or Detail', - 'audit_table_date' => 'Activity Date', + 'audit_table_ip' => 'IP adresa', + 'audit_table_date' => 'Dátum aktivity', 'audit_date_from' => 'Date Range From', 'audit_date_to' => 'Date Range To', @@ -166,13 +167,13 @@ return [ 'user_profile' => 'Profil používateľa', 'users_add_new' => 'Pridať nového používateľa', 'users_search' => 'Hľadať medzi používateľmi', - 'users_latest_activity' => 'Latest Activity', - 'users_details' => 'User Details', + 'users_latest_activity' => 'Nedávna aktivita', + 'users_details' => 'Údaje o používateľovi', 'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.', 'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.', 'users_role' => 'Používateľské roly', 'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.', - 'users_password' => 'User Password', + 'users_password' => 'Heslo používateľa', 'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 6 characters long.', 'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.', 'users_send_invite_option' => 'Send user invite email', diff --git a/resources/lang/sl/settings.php b/resources/lang/sl/settings.php index ff7458fb6..cadba7bce 100644 --- a/resources/lang/sl/settings.php +++ b/resources/lang/sl/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Uporabnik', 'audit_table_event' => 'Dogodek', 'audit_table_related' => 'Povezani predmet ali podrobnost', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Datum zadnje dejavnosti', 'audit_date_from' => 'Časovno obdobje od', 'audit_date_to' => 'Časovno obdobje do', diff --git a/resources/lang/sv/settings.php b/resources/lang/sv/settings.php index 18f4d49ed..1aa51ee38 100644 --- a/resources/lang/sv/settings.php +++ b/resources/lang/sv/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Användare', 'audit_table_event' => 'Händelse', 'audit_table_related' => 'Relaterat objekt eller detalj', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Datum för senaste aktiviteten', 'audit_date_from' => 'Datumintervall från', 'audit_date_to' => 'Datumintervall till', diff --git a/resources/lang/tr/settings.php b/resources/lang/tr/settings.php index 22ec0965b..aca4a0628 100755 --- a/resources/lang/tr/settings.php +++ b/resources/lang/tr/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Kullanıcı', 'audit_table_event' => 'Etkinlik', 'audit_table_related' => 'İlgili Öğe veya Detay', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Aktivite Tarihi', 'audit_date_from' => 'Tarih Aralığından', 'audit_date_to' => 'Tarih Aralığına', diff --git a/resources/lang/uk/settings.php b/resources/lang/uk/settings.php index be9c5bb44..2c96d4a2b 100644 --- a/resources/lang/uk/settings.php +++ b/resources/lang/uk/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Користувач', 'audit_table_event' => 'Подія', 'audit_table_related' => 'Пов’язаний елемент', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Дата активності', 'audit_date_from' => 'Діапазон дат від', 'audit_date_to' => 'Діапазон дат до', diff --git a/resources/lang/vi/activities.php b/resources/lang/vi/activities.php index d5d5e76b3..255ce38aa 100644 --- a/resources/lang/vi/activities.php +++ b/resources/lang/vi/activities.php @@ -44,12 +44,12 @@ return [ 'bookshelf_delete_notification' => 'Giá sách đã được xóa thành công', // Favourites - 'favourite_add_notification' => '":name" has been added to your favourites', - 'favourite_remove_notification' => '":name" has been removed from your favourites', + 'favourite_add_notification' => '":name" đã được thêm vào danh sách yêu thích của bạn', + 'favourite_remove_notification' => '":name" đã được gỡ khỏi danh sách yêu thích của bạn', // MFA - 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', - 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + 'mfa_setup_method_notification' => 'Cấu hình xác thực nhiều bước thành công', + 'mfa_remove_method_notification' => 'Đã gỡ xác thực nhiều bước', // Other 'commented_on' => 'đã bình luận về', diff --git a/resources/lang/vi/auth.php b/resources/lang/vi/auth.php index fbb99b830..e95d26ac6 100644 --- a/resources/lang/vi/auth.php +++ b/resources/lang/vi/auth.php @@ -76,27 +76,27 @@ return [ 'user_invite_success' => 'Mật khẩu đã được thiết lập, bạn có quyền truy cập đến :appName!', // Multi-factor Authentication - 'mfa_setup' => 'Setup Multi-Factor Authentication', - 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', - 'mfa_setup_configured' => 'Already configured', - 'mfa_setup_reconfigure' => 'Reconfigure', - 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', - 'mfa_setup_action' => 'Setup', + 'mfa_setup' => 'Cài đặt xác thực nhiều bước', + 'mfa_setup_desc' => 'Cài đặt xác thực nhiều bước như một lớp bảo mật khác cho tài khoản của bạn.', + 'mfa_setup_configured' => 'Đã cài đặt', + 'mfa_setup_reconfigure' => 'Cài đặt lại', + 'mfa_setup_remove_confirmation' => 'Bạn có chắc muốn gỡ bỏ phương thức xác thực nhiều bước này?', + 'mfa_setup_action' => 'Cài đặt', 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', - 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_title' => 'Ứng dụng di động', 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', - 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_title' => 'Mã dự phòng', 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', - 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', - 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_confirm_and_enable' => 'Xác nhận và Mở', + 'mfa_gen_backup_codes_title' => 'Cài đặt Mã dự phòng', 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', - 'mfa_gen_backup_codes_download' => 'Download Codes', - 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', - 'mfa_gen_totp_title' => 'Mobile App Setup', - 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', - 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', - 'mfa_gen_totp_verify_setup' => 'Verify Setup', - 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_backup_codes_download' => 'Tải mã', + 'mfa_gen_backup_codes_usage_warning' => 'Mỗi mã chỉ có thể sử dụng một lần', + 'mfa_gen_totp_title' => 'Cài đặt ứng dụng di động', + 'mfa_gen_totp_desc' => 'Để sử dụng xác thực nhiều bước, bạn cần một ứng dụng di động hỗ trợ TOTP ví dụ như Google Authenticator, Authy hoặc Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Quét mã QR dưới đây bằng ứng dụng xác thực mà bạn muốn để bắt đầu.', + 'mfa_gen_totp_verify_setup' => 'Xác nhận cài đặt', + 'mfa_gen_totp_verify_setup_desc' => 'Xác nhận rằng tất cả hoạt động bằng cách nhập vào một mã, được tạo ra bởi ứng dụng xác thực của bạn vào ô dưới đây:', 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', 'mfa_verify_access' => 'Verify Access', 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', @@ -104,9 +104,9 @@ return [ 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', 'mfa_verify_use_totp' => 'Verify using a mobile app', 'mfa_verify_use_backup_codes' => 'Verify using a backup code', - 'mfa_verify_backup_code' => 'Backup Code', - 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', - 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', - 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', - 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', + 'mfa_verify_backup_code' => 'Mã dự phòng', + 'mfa_verify_backup_code_desc' => 'Nhập một trong các mã dự phòng còn lại của bạn vào ô phía dưới:', + 'mfa_verify_backup_code_enter_here' => 'Nhập mã xác thực của bạn tại đây', + 'mfa_verify_totp_desc' => 'Nhập mã do ứng dụng di động của bạn tạo ra vào dưới đây:', + 'mfa_setup_login_notification' => 'Đã cài đặt xác thực nhiều bước, bạn vui lòng đăng nhập lại sử dụng phương thức đã cài đặt.', ]; \ No newline at end of file diff --git a/resources/lang/vi/common.php b/resources/lang/vi/common.php index dd721d994..f118d34c3 100644 --- a/resources/lang/vi/common.php +++ b/resources/lang/vi/common.php @@ -39,7 +39,7 @@ return [ 'reset' => 'Thiết lập lại', 'remove' => 'Xóa bỏ', 'add' => 'Thêm', - 'configure' => 'Configure', + 'configure' => 'Cấu hình', 'fullscreen' => 'Toàn màn hình', 'favourite' => 'Yêu thích', 'unfavourite' => 'Bỏ yêu thích', diff --git a/resources/lang/vi/entities.php b/resources/lang/vi/entities.php index 5b04e8abf..0cbfd27a4 100644 --- a/resources/lang/vi/entities.php +++ b/resources/lang/vi/entities.php @@ -22,13 +22,13 @@ return [ 'meta_created_name' => 'Được tạo :timeLength bởi :user', 'meta_updated' => 'Được cập nhật :timeLength', 'meta_updated_name' => 'Được cập nhật :timeLength bởi :user', - 'meta_owned_name' => 'Owned by :user', + 'meta_owned_name' => 'Được sở hữu bởi :user', 'entity_select' => 'Chọn thực thể', 'images' => 'Ảnh', 'my_recent_drafts' => 'Bản nháp gần đây của tôi', 'my_recently_viewed' => 'Xem gần đây', 'my_most_viewed_favourites' => 'My Most Viewed Favourites', - 'my_favourites' => 'My Favourites', + 'my_favourites' => 'Danh sách yêu thích của tôi', 'no_pages_viewed' => 'Bạn chưa xem bất cứ trang nào', 'no_pages_recently_created' => 'Không có trang nào được tạo gần đây', 'no_pages_recently_updated' => 'Không có trang nào được cập nhật gần đây', @@ -36,14 +36,14 @@ return [ 'export_html' => 'Đang chứa tệp tin Web', 'export_pdf' => 'Tệp PDF', 'export_text' => 'Tệp văn bản thuần túy', - 'export_md' => 'Markdown File', + 'export_md' => 'Tệp Markdown', // Permissions and restrictions 'permissions' => 'Quyền', 'permissions_intro' => 'Một khi được bật, các quyền này sẽ được ưu tiên trên hết tất cả các quyền hạn khác.', 'permissions_enable' => 'Bật quyền hạn tùy chỉnh', 'permissions_save' => 'Lưu quyền hạn', - 'permissions_owner' => 'Owner', + 'permissions_owner' => 'Chủ sở hữu', // Search 'search_results' => 'Kết quả Tìm kiếm', @@ -63,7 +63,7 @@ return [ 'search_permissions_set' => 'Phân quyền', 'search_created_by_me' => 'Được tạo bởi tôi', 'search_updated_by_me' => 'Được cập nhật bởi tôi', - 'search_owned_by_me' => 'Owned by me', + 'search_owned_by_me' => 'Của tôi', 'search_date_options' => 'Tùy chọn ngày', 'search_updated_before' => 'Đã được cập nhật trước đó', 'search_updated_after' => 'Đã được cập nhật sau', @@ -99,7 +99,7 @@ return [ 'shelves_permissions' => 'Các quyền đối với kệ sách', 'shelves_permissions_updated' => 'Các quyền với kệ sách đã được cập nhật', 'shelves_permissions_active' => 'Đang bật các quyền hạn từ Kệ sách', - 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', + 'shelves_permissions_cascade_warning' => 'Các quyền trên giá sách sẽ không được tự động gán cho các sách trên đó. Vì một quyển sách có thể tồn tại trên nhiều giá sách. Các quyền có thể được sao chép xuống các quyển sách sử dụng tuỳ chọn dưới đây.', 'shelves_copy_permissions_to_books' => 'Sao chép các quyền cho sách', 'shelves_copy_permissions' => 'Sao chép các quyền', 'shelves_copy_permissions_explain' => 'Điều này sẽ áp dụng các cài đặt quyền của giá sách hiện tại với tất cả các cuốn sách bên trong. Trước khi kích hoạt, đảm bảo bất cứ thay đổi liên quan đến quyền của giá sách này đã được lưu.', @@ -153,7 +153,7 @@ return [ 'chapters_create' => 'Tạo Chương mới', 'chapters_delete' => 'Xóa Chương', 'chapters_delete_named' => 'Xóa Chương :chapterName', - 'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.', + 'chapters_delete_explain' => 'Hành động này sẽ xoá chương \':chapterName\'. Tất cả các trang trong chương này cũng sẽ bị xoá.', 'chapters_delete_confirm' => 'Bạn có chắc chắn muốn xóa chương này?', 'chapters_edit' => 'Sửa Chương', 'chapters_edit_named' => 'Sửa chương :chapterName', @@ -215,7 +215,7 @@ return [ 'pages_revisions' => 'Phiên bản Trang', 'pages_revisions_named' => 'Phiên bản Trang cho :pageName', 'pages_revision_named' => 'Phiên bản Trang cho :pageName', - 'pages_revision_restored_from' => 'Restored from #:id; :summary', + 'pages_revision_restored_from' => 'Khôi phục từ #:id; :summary', 'pages_revisions_created_by' => 'Tạo bởi', 'pages_revisions_date' => 'Ngày của Phiên bản', 'pages_revisions_number' => '#', diff --git a/resources/lang/vi/settings.php b/resources/lang/vi/settings.php index 7854268af..7dbed9018 100644 --- a/resources/lang/vi/settings.php +++ b/resources/lang/vi/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => 'Người dùng', 'audit_table_event' => 'Sự kiện', 'audit_table_related' => 'Related Item or Detail', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => 'Ngày hoạt động', 'audit_date_from' => 'Ngày từ khoảng', 'audit_date_to' => 'Ngày đến khoảng', diff --git a/resources/lang/zh_CN/settings.php b/resources/lang/zh_CN/settings.php index 7de5c87fc..910b9614d 100755 --- a/resources/lang/zh_CN/settings.php +++ b/resources/lang/zh_CN/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => '用户', 'audit_table_event' => '事件', 'audit_table_related' => '相关项目或详细信息', + 'audit_table_ip' => 'IP地址', 'audit_table_date' => '活动日期', 'audit_date_from' => '日期范围从', 'audit_date_to' => '日期范围至', diff --git a/resources/lang/zh_TW/settings.php b/resources/lang/zh_TW/settings.php index 839a6c719..aa0a87993 100644 --- a/resources/lang/zh_TW/settings.php +++ b/resources/lang/zh_TW/settings.php @@ -119,6 +119,7 @@ return [ 'audit_table_user' => '使用者', 'audit_table_event' => '活動', 'audit_table_related' => '相關的項目或詳細資訊', + 'audit_table_ip' => 'IP Address', 'audit_table_date' => '活動日期', 'audit_date_from' => '日期範圍,從', 'audit_date_to' => '日期範圍,到', diff --git a/resources/views/settings/audit.blade.php b/resources/views/settings/audit.blade.php index 7a66cc3f3..84f180f3b 100644 --- a/resources/views/settings/audit.blade.php +++ b/resources/views/settings/audit.blade.php @@ -62,6 +62,7 @@ {{ trans('settings.audit_table_event') }} {{ trans('settings.audit_table_related') }} + {{ trans('settings.audit_table_ip') }} {{ trans('settings.audit_table_date') }} @@ -88,6 +89,7 @@
{{ $activity->detail }}
@endif + {{ $activity->ip }} {{ $activity->created_at }} @endforeach diff --git a/tests/ActivityTrackingTest.php b/tests/ActivityTrackingTest.php deleted file mode 100644 index 494a1f506..000000000 --- a/tests/ActivityTrackingTest.php +++ /dev/null @@ -1,37 +0,0 @@ -take(10); - - $this->asAdmin()->visit('/books') - ->dontSeeInElement('#recents', $books[0]->name) - ->dontSeeInElement('#recents', $books[1]->name) - ->visit($books[0]->getUrl()) - ->visit($books[1]->getUrl()) - ->visit('/books') - ->seeInElement('#recents', $books[0]->name) - ->seeInElement('#recents', $books[1]->name); - } - - public function test_popular_books() - { - $books = Book::all()->take(10); - - $this->asAdmin()->visit('/books') - ->dontSeeInElement('#popular', $books[0]->name) - ->dontSeeInElement('#popular', $books[1]->name) - ->visit($books[0]->getUrl()) - ->visit($books[1]->getUrl()) - ->visit($books[0]->getUrl()) - ->visit('/books') - ->seeInNthElement('#popular .book', 0, $books[0]->name) - ->seeInNthElement('#popular .book', 1, $books[1]->name); - } -} diff --git a/tests/AuditLogTest.php b/tests/AuditLogTest.php index bc36a184d..8d13670ca 100644 --- a/tests/AuditLogTest.php +++ b/tests/AuditLogTest.php @@ -140,4 +140,53 @@ class AuditLogTest extends TestCase $resp->assertSeeText($chapter->name); $resp->assertDontSeeText($page->name); } + + public function test_ip_address_logged_and_visible() + { + config()->set('app.proxies', '*'); + $editor = $this->getEditor(); + /** @var Page $page */ + $page = Page::query()->first(); + + $this->actingAs($editor)->put($page->getUrl(), [ + 'name' => 'Updated page', + 'html' => '

Updated content

', + ], [ + 'X-Forwarded-For' => '192.123.45.1', + ])->assertRedirect($page->refresh()->getUrl()); + + $this->assertDatabaseHas('activities', [ + 'type' => ActivityType::PAGE_UPDATE, + 'ip' => '192.123.45.1', + 'user_id' => $editor->id, + 'entity_id' => $page->id, + ]); + + $resp = $this->asAdmin()->get('/settings/audit'); + $resp->assertSee('192.123.45.1'); + } + + public function test_ip_address_not_logged_in_demo_mode() + { + config()->set('app.proxies', '*'); + config()->set('app.env', 'demo'); + $editor = $this->getEditor(); + /** @var Page $page */ + $page = Page::query()->first(); + + $this->actingAs($editor)->put($page->getUrl(), [ + 'name' => 'Updated page', + 'html' => '

Updated content

', + ], [ + 'X-Forwarded-For' => '192.123.45.1', + 'REMOTE_ADDR' => '192.123.45.2', + ])->assertRedirect($page->refresh()->getUrl()); + + $this->assertDatabaseHas('activities', [ + 'type' => ActivityType::PAGE_UPDATE, + 'ip' => '127.0.0.1', + 'user_id' => $editor->id, + 'entity_id' => $page->id, + ]); + } } diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php index 2380aad7b..d037b5701 100644 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@ -3,49 +3,41 @@ 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; use BookStack\Notifications\ResetPassword; -use BookStack\Settings\SettingService; -use DB; -use Hash; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Notification; -use Illuminate\Support\Str; -use Tests\BrowserKitTest; +use Tests\TestCase; +use Tests\TestResponse; -class AuthTest extends BrowserKitTest +class AuthTest extends TestCase { public function test_auth_working() { - $this->visit('/') - ->seePageIs('/login'); + $this->get('/')->assertRedirect('/login'); } public function test_login() { - $this->login('admin@admin.com', 'password') - ->seePageIs('/'); + $this->login('admin@admin.com', 'password')->assertRedirect('/'); } public function test_public_viewing() { - $settings = app(SettingService::class); - $settings->put('app-public', 'true'); - $this->visit('/') - ->seePageIs('/') - ->see('Log In'); + $this->setSettings(['app-public' => 'true']); + $this->get('/') + ->assertOk() + ->assertSee('Log in'); } public function test_registration_showing() { // Ensure registration form is showing $this->setSettings(['registration-enabled' => 'true']); - $this->visit('/login') - ->see('Sign up') - ->click('Sign up') - ->seePageIs('/register'); + $this->get('/login') + ->assertElementContains('a[href="' . url('/register') . '"]', 'Sign up'); } public function test_normal_registration() @@ -55,15 +47,17 @@ class AuthTest extends BrowserKitTest $user = factory(User::class)->make(); // Test form and ensure user is created - $this->visit('/register') - ->see('Sign Up') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/') - ->see($user->name) - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email]); + $this->get('/register') + ->assertSee('Sign Up') + ->assertElementContains('form[action="' . url('/register') . '"]', 'Create Account'); + + $resp = $this->post('/register', $user->only('password', 'name', 'email')); + $resp->assertRedirect('/'); + + $resp = $this->get('/'); + $resp->assertOk(); + $resp->assertSee($user->name); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]); } public function test_empty_registration_redirects_back_with_errors() @@ -72,36 +66,33 @@ class AuthTest extends BrowserKitTest $this->setSettings(['registration-enabled' => 'true']); // Test form and ensure user is created - $this->visit('/register') - ->press('Create Account') - ->see('The name field is required') - ->seePageIs('/register'); + $this->get('/register'); + $this->post('/register', [])->assertRedirect('/register'); + $this->get('/register')->assertSee('The name field is required'); } public function test_registration_validation() { $this->setSettings(['registration-enabled' => 'true']); - $this->visit('/register') - ->type('1', '#name') - ->type('1', '#email') - ->type('1', '#password') - ->press('Create Account') - ->see('The name must be at least 2 characters.') - ->see('The email must be a valid email address.') - ->see('The password must be at least 8 characters.') - ->seePageIs('/register'); + $this->get('/register'); + $resp = $this->followingRedirects()->post('/register', [ + 'name' => '1', + 'email' => '1', + 'password' => '1', + ]); + $resp->assertSee('The name must be at least 2 characters.'); + $resp->assertSee('The email must be a valid email address.'); + $resp->assertSee('The password must be at least 8 characters.'); } public function test_sign_up_link_on_login() { - $this->visit('/login') - ->dontSee('Sign up'); + $this->get('/login')->assertDontSee('Sign up'); $this->setSettings(['registration-enabled' => 'true']); - $this->visit('/login') - ->see('Sign up'); + $this->get('/login')->assertSee('Sign up'); } public function test_confirmed_registration() @@ -114,27 +105,24 @@ class AuthTest extends BrowserKitTest $user = factory(User::class)->make(); // Go through registration process - $this->visit('/register') - ->see('Sign Up') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register/confirm') - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); + $resp = $this->post('/register', $user->only('name', 'email', 'password')); + $resp->assertRedirect('/register/confirm'); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); // Ensure notification sent - $dbUser = User::where('email', '=', $user->email)->first(); + /** @var User $dbUser */ + $dbUser = User::query()->where('email', '=', $user->email)->first(); Notification::assertSentTo($dbUser, ConfirmEmail::class); // Test access and resend confirmation email - $this->login($user->email, $user->password) - ->seePageIs('/register/confirm/awaiting') - ->see('Resend') - ->visit('/books') - ->seePageIs('/login') - ->visit('/register/confirm/awaiting') - ->press('Resend Confirmation Email'); + $resp = $this->login($user->email, $user->password); + $resp->assertRedirect('/register/confirm/awaiting'); + + $resp = $this->get('/register/confirm/awaiting'); + $resp->assertElementContains('form[action="' . url('/register/confirm/resend') . '"]', 'Resend'); + + $this->get('/books')->assertRedirect('/login'); + $this->post('/register/confirm/resend', $user->only('email')); // Get confirmation and confirm notification matches $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first(); @@ -143,188 +131,69 @@ class AuthTest extends BrowserKitTest }); // Check confirmation email confirmation activation. - $this->visit('/register/confirm/' . $emailConfirmation->token) - ->seePageIs('/') - ->see($user->name) - ->notSeeInDatabase('email_confirmations', ['token' => $emailConfirmation->token]) - ->seeInDatabase('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]); + $this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/'); + $this->get('/')->assertSee($user->name); + $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]); + $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]); } public function test_restricted_registration() { $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']); $user = factory(User::class)->make(); + // Go through registration process - $this->visit('/register') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register') - ->dontSeeInDatabase('users', ['email' => $user->email]) - ->see('That email domain does not have access to this application'); + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register'); + $resp = $this->get('/register'); + $resp->assertSee('That email domain does not have access to this application'); + $this->assertDatabaseMissing('users', $user->only('email')); $user->email = 'barry@example.com'; - $this->visit('/register') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register/confirm') - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register/confirm'); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); $this->assertNull(auth()->user()); - $this->visit('/')->seePageIs('/login') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Log In') - ->seePageIs('/register/confirm/awaiting') - ->seeText('Email Address Not Confirmed'); + $this->get('/')->assertRedirect('/login'); + $resp = $this->followingRedirects()->post('/login', $user->only('email', 'password')); + $resp->assertSee('Email Address Not Confirmed'); + $this->assertNull(auth()->user()); } public function test_restricted_registration_with_confirmation_disabled() { $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']); $user = factory(User::class)->make(); + // Go through registration process - $this->visit('/register') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register') - ->dontSeeInDatabase('users', ['email' => $user->email]) - ->see('That email domain does not have access to this application'); + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register'); + $this->assertDatabaseMissing('users', $user->only('email')); + $this->get('/register')->assertSee('That email domain does not have access to this application'); $user->email = 'barry@example.com'; - $this->visit('/register') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register/confirm') - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register/confirm'); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); $this->assertNull(auth()->user()); - $this->visit('/')->seePageIs('/login') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Log In') - ->seePageIs('/register/confirm/awaiting') - ->seeText('Email Address Not Confirmed'); - } - - public function test_user_creation() - { - /** @var User $user */ - $user = factory(User::class)->make(); - $adminRole = Role::getRole('admin'); - - $this->asAdmin() - ->visit('/settings/users') - ->click('Add New User') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->check("roles[{$adminRole->id}]") - ->type($user->password, '#password') - ->type($user->password, '#password-confirm') - ->press('Save') - ->seePageIs('/settings/users') - ->seeInDatabase('users', $user->only(['name', 'email'])) - ->see($user->name); - - $user->refresh(); - $this->assertStringStartsWith(Str::slug($user->name), $user->slug); - } - - public function test_user_updating() - { - $user = $this->getNormalUser(); - $password = $user->password; - $this->asAdmin() - ->visit('/settings/users') - ->click($user->name) - ->seePageIs('/settings/users/' . $user->id) - ->see($user->email) - ->type('Barry Scott', '#name') - ->press('Save') - ->seePageIs('/settings/users') - ->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password]) - ->notSeeInDatabase('users', ['name' => $user->name]); - - $user->refresh(); - $this->assertStringStartsWith(Str::slug($user->name), $user->slug); - } - - public function test_user_password_update() - { - $user = $this->getNormalUser(); - $userProfilePage = '/settings/users/' . $user->id; - $this->asAdmin() - ->visit($userProfilePage) - ->type('newpassword', '#password') - ->press('Save') - ->seePageIs($userProfilePage) - ->see('Password confirmation required') - - ->type('newpassword', '#password') - ->type('newpassword', '#password-confirm') - ->press('Save') - ->seePageIs('/settings/users'); - - $userPassword = User::find($user->id)->password; - $this->assertTrue(Hash::check('newpassword', $userPassword)); - } - - public function test_user_deletion() - { - $userDetails = factory(User::class)->make(); - $user = $this->getEditor($userDetails->toArray()); - - $this->asAdmin() - ->visit('/settings/users/' . $user->id) - ->click('Delete User') - ->see($user->name) - ->press('Confirm') - ->seePageIs('/settings/users') - ->notSeeInDatabase('users', ['name' => $user->name]); - } - - public function test_user_cannot_be_deleted_if_last_admin() - { - $adminRole = Role::getRole('admin'); - - // Delete all but one admin user if there are more than one - $adminUsers = $adminRole->users; - if (count($adminUsers) > 1) { - foreach ($adminUsers->splice(1) as $user) { - $user->delete(); - } - } - - // Ensure we currently only have 1 admin user - $this->assertEquals(1, $adminRole->users()->count()); - $user = $adminRole->users->first(); - - $this->asAdmin()->visit('/settings/users/' . $user->id) - ->click('Delete User') - ->press('Confirm') - ->seePageIs('/settings/users/' . $user->id) - ->see('You cannot delete the only admin'); + $this->get('/')->assertRedirect('/login'); + $resp = $this->post('/login', $user->only('email', 'password')); + $resp->assertRedirect('/register/confirm/awaiting'); + $this->get('/register/confirm/awaiting')->assertSee('Email Address Not Confirmed'); + $this->assertNull(auth()->user()); } public function test_logout() { - $this->asAdmin() - ->visit('/') - ->seePageIs('/') - ->visit('/logout') - ->visit('/') - ->seePageIs('/login'); + $this->asAdmin()->get('/')->assertOk(); + $this->get('/logout')->assertRedirect('/'); + $this->get('/')->assertRedirect('/login'); } public function test_mfa_session_cleared_on_logout() @@ -335,7 +204,7 @@ class AuthTest extends BrowserKitTest $mfaSession->markVerifiedForUser($user); $this->assertTrue($mfaSession->isVerifiedForUser($user)); - $this->asAdmin()->visit('/logout'); + $this->asAdmin()->get('/logout'); $this->assertFalse($mfaSession->isVerifiedForUser($user)); } @@ -343,69 +212,85 @@ class AuthTest extends BrowserKitTest { Notification::fake(); - $this->visit('/login')->click('Forgot Password?') - ->seePageIs('/password/email') - ->type('admin@admin.com', 'email') - ->press('Send Reset Link') - ->see('A password reset link will be sent to admin@admin.com if that email address is found in the system.'); + $this->get('/login') + ->assertElementContains('a[href="' . url('/password/email') . '"]', 'Forgot Password?'); - $this->seeInDatabase('password_resets', [ + $this->get('/password/email') + ->assertElementContains('form[action="' . url('/password/email') . '"]', 'Send Reset Link'); + + $resp = $this->post('/password/email', [ + 'email' => 'admin@admin.com', + ]); + $resp->assertRedirect('/password/email'); + + $resp = $this->get('/password/email'); + $resp->assertSee('A password reset link will be sent to admin@admin.com if that email address is found in the system.'); + + $this->assertDatabaseHas('password_resets', [ 'email' => 'admin@admin.com', ]); - $user = User::where('email', '=', 'admin@admin.com')->first(); + /** @var User $user */ + $user = User::query()->where('email', '=', 'admin@admin.com')->first(); Notification::assertSentTo($user, ResetPassword::class); $n = Notification::sent($user, ResetPassword::class); - $this->visit('/password/reset/' . $n->first()->token) - ->see('Reset Password') - ->submitForm('Reset Password', [ - 'email' => 'admin@admin.com', - 'password' => 'randompass', - 'password_confirmation' => 'randompass', - ])->seePageIs('/') - ->see('Your password has been successfully reset'); + $this->get('/password/reset/' . $n->first()->token) + ->assertOk() + ->assertSee('Reset Password'); + + $resp = $this->post('/password/reset', [ + 'email' => 'admin@admin.com', + 'password' => 'randompass', + 'password_confirmation' => 'randompass', + 'token' => $n->first()->token, + ]); + $resp->assertRedirect('/'); + + $this->get('/')->assertSee('Your password has been successfully reset'); } public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery() { - $this->visit('/login')->click('Forgot Password?') - ->seePageIs('/password/email') - ->type('barry@admin.com', 'email') - ->press('Send Reset Link') - ->see('A password reset link will be sent to barry@admin.com if that email address is found in the system.') - ->dontSee('We can\'t find a user'); + $this->get('/password/email'); + $resp = $this->followingRedirects()->post('/password/email', [ + 'email' => 'barry@admin.com', + ]); + $resp->assertSee('A password reset link will be sent to barry@admin.com if that email address is found in the system.'); + $resp->assertDontSee('We can\'t find a user'); - $this->visit('/password/reset/arandometokenvalue') - ->see('Reset Password') - ->submitForm('Reset Password', [ - 'email' => 'barry@admin.com', - 'password' => 'randompass', - 'password_confirmation' => 'randompass', - ])->followRedirects() - ->seePageIs('/password/reset/arandometokenvalue') - ->dontSee('We can\'t find a user') - ->see('The password reset token is invalid for this email address.'); + $this->get('/password/reset/arandometokenvalue')->assertSee('Reset Password'); + $resp = $this->post('/password/reset', [ + 'email' => 'barry@admin.com', + 'password' => 'randompass', + 'password_confirmation' => 'randompass', + 'token' => 'arandometokenvalue', + ]); + $resp->assertRedirect('/password/reset/arandometokenvalue'); + + $this->get('/password/reset/arandometokenvalue') + ->assertDontSee('We can\'t find a user') + ->assertSee('The password reset token is invalid for this email address.'); } public function test_reset_password_page_shows_sign_links() { $this->setSettings(['registration-enabled' => 'true']); - $this->visit('/password/email') - ->seeLink('Log in') - ->seeLink('Sign up'); + $this->get('/password/email') + ->assertElementContains('a', 'Log in') + ->assertElementContains('a', 'Sign up'); } public function test_login_redirects_to_initially_requested_url_correctly() { config()->set('app.url', 'http://localhost'); + /** @var Page $page */ $page = Page::query()->first(); - $this->visit($page->getUrl()) - ->seePageUrlIs(url('/login')); + $this->get($page->getUrl())->assertRedirect(url('/login')); $this->login('admin@admin.com', 'password') - ->seePageUrlIs($page->getUrl()); + ->assertRedirect($page->getUrl()); } public function test_login_intended_redirect_does_not_redirect_to_external_pages() @@ -416,15 +301,15 @@ class AuthTest extends BrowserKitTest $this->get('/login', ['referer' => 'https://example.com']); $login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']); - $login->assertRedirectedTo('http://localhost'); + $login->assertRedirect('http://localhost'); } public function test_login_intended_redirect_does_not_factor_mfa_routes() { - $this->get('/books')->assertRedirectedTo('/login'); - $this->get('/mfa/setup')->assertRedirectedTo('/login'); + $this->get('/books')->assertRedirect('/login'); + $this->get('/mfa/setup')->assertRedirect('/login'); $login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']); - $login->assertRedirectedTo('/books'); + $login->assertRedirect('/books'); } public function test_login_authenticates_admins_on_all_guards() @@ -469,20 +354,15 @@ class AuthTest extends BrowserKitTest auth()->login($user); $this->assertTrue(auth()->check()); - $this->get('/books'); - $this->assertRedirectedTo('/'); - + $this->get('/books')->assertRedirect('/'); $this->assertFalse(auth()->check()); } /** * Perform a login. */ - protected function login(string $email, string $password): AuthTest + protected function login(string $email, string $password): TestResponse { - return $this->visit('/login') - ->type($email, '#email') - ->type($password, '#password') - ->press('Log In'); + return $this->post('/login', compact('email', 'password')); } } diff --git a/tests/Auth/SocialAuthTest.php b/tests/Auth/SocialAuthTest.php index 5818cbb74..f70263dd2 100644 --- a/tests/Auth/SocialAuthTest.php +++ b/tests/Auth/SocialAuthTest.php @@ -2,9 +2,10 @@ namespace Tests\Auth; +use BookStack\Actions\ActivityType; use BookStack\Auth\SocialAccount; use BookStack\Auth\User; -use DB; +use Illuminate\Support\Facades\DB; use Laravel\Socialite\Contracts\Factory; use Laravel\Socialite\Contracts\Provider; use Mockery; @@ -82,6 +83,7 @@ class SocialAuthTest extends TestCase ]); $resp = $this->followingRedirects()->get('/login/service/github/callback'); $resp->assertDontSee('login-form'); + $this->assertActivityExists(ActivityType::AUTH_LOGIN, null, 'github; (' . $this->getAdmin()->id . ') ' . $this->getAdmin()->name); } public function test_social_account_detach() diff --git a/tests/Auth/UserInviteTest.php b/tests/Auth/UserInviteTest.php index c5c4b01af..dcf9e23df 100644 --- a/tests/Auth/UserInviteTest.php +++ b/tests/Auth/UserInviteTest.php @@ -6,9 +6,9 @@ use BookStack\Auth\Access\UserInviteService; use BookStack\Auth\User; use BookStack\Notifications\UserInvite; use Carbon\Carbon; -use DB; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Notification; use Illuminate\Support\Str; -use Notification; use Tests\TestCase; class UserInviteTest extends TestCase diff --git a/tests/BrowserKitTest.php b/tests/BrowserKitTest.php deleted file mode 100644 index 23eb10887..000000000 --- a/tests/BrowserKitTest.php +++ /dev/null @@ -1,218 +0,0 @@ -make(Kernel::class)->bootstrap(); - - return $app; - } - - /** - * Quickly sets an array of settings. - * - * @param $settingsArray - */ - protected function setSettings($settingsArray) - { - $settings = app(SettingService::class); - foreach ($settingsArray as $key => $value) { - $settings->put($key, $value); - } - } - - /** - * Create a group of entities that belong to a specific user. - */ - protected function createEntityChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array - { - if (empty($updaterUser)) { - $updaterUser = $creatorUser; - } - - $userAttrs = ['created_by' => $creatorUser->id, 'owned_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]; - $book = factory(Book::class)->create($userAttrs); - $chapter = factory(Chapter::class)->create(array_merge(['book_id' => $book->id], $userAttrs)); - $page = factory(Page::class)->create(array_merge(['book_id' => $book->id, 'chapter_id' => $chapter->id], $userAttrs)); - $restrictionService = $this->app[PermissionService::class]; - $restrictionService->buildJointPermissionsForEntity($book); - - return compact('book', 'chapter', 'page'); - } - - /** - * Helper for updating entity permissions. - * - * @param Entity $entity - */ - protected function updateEntityPermissions(Entity $entity) - { - $restrictionService = $this->app[PermissionService::class]; - $restrictionService->buildJointPermissionsForEntity($entity); - } - - /** - * Quick way to create a new user without any permissions. - * - * @param array $attributes - * - * @return mixed - */ - protected function getNewBlankUser($attributes = []) - { - $user = factory(User::class)->create($attributes); - - return $user; - } - - /** - * Assert that a given string is seen inside an element. - * - * @param bool|string|null $element - * @param int $position - * @param string $text - * @param bool $negate - * - * @return $this - */ - protected function seeInNthElement($element, $position, $text, $negate = false) - { - $method = $negate ? 'assertDoesNotMatchRegularExpression' : 'assertMatchesRegularExpression'; - - $rawPattern = preg_quote($text, '/'); - - $escapedPattern = preg_quote(e($text), '/'); - - $content = $this->crawler->filter($element)->eq($position)->html(); - - $pattern = $rawPattern == $escapedPattern - ? $rawPattern : "({$rawPattern}|{$escapedPattern})"; - - $this->$method("/$pattern/i", $content); - - return $this; - } - - /** - * Assert that the current page matches a given URI. - * - * @param string $uri - * - * @return $this - */ - protected function seePageUrlIs($uri) - { - $this->assertEquals( - $uri, - $this->currentUri, - "Did not land on expected page [{$uri}].\n" - ); - - return $this; - } - - /** - * Do a forced visit that does not error out on exception. - * - * @param string $uri - * @param array $parameters - * @param array $cookies - * @param array $files - * - * @return $this - */ - protected function forceVisit($uri, $parameters = [], $cookies = [], $files = []) - { - $method = 'GET'; - $uri = $this->prepareUrlForRequest($uri); - $this->call($method, $uri, $parameters, $cookies, $files); - $this->clearInputs()->followRedirects(); - $this->currentUri = $this->app->make('request')->fullUrl(); - $this->crawler = new Crawler($this->response->getContent(), $uri); - - return $this; - } - - /** - * Click the text within the selected element. - * - * @param $parentElement - * @param $linkText - * - * @return $this - */ - protected function clickInElement($parentElement, $linkText) - { - $elem = $this->crawler->filter($parentElement); - $link = $elem->selectLink($linkText); - $this->visit($link->link()->getUri()); - - return $this; - } - - /** - * Check if the page contains the given element. - * - * @param string $selector - */ - protected function pageHasElement($selector) - { - $elements = $this->crawler->filter($selector); - $this->assertTrue(count($elements) > 0, 'The page does not contain an element matching ' . $selector); - - return $this; - } - - /** - * Check if the page contains the given element. - * - * @param string $selector - */ - protected function pageNotHasElement($selector) - { - $elements = $this->crawler->filter($selector); - $this->assertFalse(count($elements) > 0, 'The page contains ' . count($elements) . ' elements matching ' . $selector); - - return $this; - } -} diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php index 480d29002..1780ddee8 100644 --- a/tests/Entity/BookShelfTest.php +++ b/tests/Entity/BookShelfTest.php @@ -369,4 +369,12 @@ class BookShelfTest extends TestCase $resp = $this->asEditor()->get($newBook->getUrl()); $resp->assertDontSee($shelfInfo['name']); } + + public function test_cancel_on_child_book_creation_returns_to_original_shelf() + { + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); + $resp = $this->asEditor()->get($shelf->getUrl('/create-book')); + $resp->assertElementContains('form a[href="' . $shelf->getUrl() . '"]', 'Cancel'); + } } diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php index b4ba2fa82..fa63c0bf9 100644 --- a/tests/Entity/BookTest.php +++ b/tests/Entity/BookTest.php @@ -7,7 +7,69 @@ use Tests\TestCase; class BookTest extends TestCase { - public function test_book_delete() + public function test_create() + { + $book = factory(Book::class)->make([ + 'name' => 'My First Book', + ]); + + $resp = $this->asEditor()->get('/books'); + $resp->assertElementContains('a[href="' . url('/create-book') . '"]', 'Create New Book'); + + $resp = $this->get('/create-book'); + $resp->assertElementContains('form[action="' . url('/books') . '"][method="POST"]', 'Save Book'); + + $resp = $this->post('/books', $book->only('name', 'description')); + $resp->assertRedirect('/books/my-first-book'); + + $resp = $this->get('/books/my-first-book'); + $resp->assertSee($book->name); + $resp->assertSee($book->description); + } + + public function test_create_uses_different_slugs_when_name_reused() + { + $book = factory(Book::class)->make([ + 'name' => 'My First Book', + ]); + + $this->asEditor()->post('/books', $book->only('name', 'description')); + $this->asEditor()->post('/books', $book->only('name', 'description')); + + $books = Book::query()->where('name', '=', $book->name) + ->orderBy('id', 'desc') + ->take(2) + ->get(); + + $this->assertMatchesRegularExpression('/my-first-book-[0-9a-zA-Z]{3}/', $books[0]->slug); + $this->assertEquals('my-first-book', $books[1]->slug); + } + + public function test_update() + { + /** @var Book $book */ + $book = Book::query()->first(); + // Cheeky initial update to refresh slug + $this->asEditor()->put($book->getUrl(), ['name' => $book->name . '5', 'description' => $book->description]); + $book->refresh(); + + $newName = $book->name . ' Updated'; + $newDesc = $book->description . ' with more content'; + + $resp = $this->get($book->getUrl('/edit')); + $resp->assertSee($book->name); + $resp->assertSee($book->description); + $resp->assertElementContains('form[action="' . $book->getUrl() . '"]', 'Save Book'); + + $resp = $this->put($book->getUrl(), ['name' => $newName, 'description' => $newDesc]); + $resp->assertRedirect($book->getUrl() . '-updated'); + + $resp = $this->get($book->getUrl() . '-updated'); + $resp->assertSee($newName); + $resp->assertSee($newDesc); + } + + public function test_delete() { $book = Book::query()->whereHas('pages')->whereHas('chapters')->first(); $this->assertNull($book->deleted_at); @@ -34,6 +96,20 @@ class BookTest extends TestCase $redirectReq->assertNotificationContains('Book Successfully Deleted'); } + public function test_cancel_on_create_page_leads_back_to_books_listing() + { + $resp = $this->asEditor()->get('/create-book'); + $resp->assertElementContains('form a[href="' . url('/books') . '"]', 'Cancel'); + } + + public function test_cancel_on_edit_book_page_leads_back_to_book() + { + /** @var Book $book */ + $book = Book::query()->first(); + $resp = $this->asEditor()->get($book->getUrl('/edit')); + $resp->assertElementContains('form a[href="' . $book->getUrl() . '"]', 'Cancel'); + } + public function test_next_previous_navigation_controls_show_within_book_content() { $book = Book::query()->first(); @@ -48,4 +124,84 @@ class BookTest extends TestCase $resp->assertElementContains('#sibling-navigation', 'Previous'); $resp->assertElementContains('#sibling-navigation', substr($chapter->name, 0, 20)); } + + public function test_recently_viewed_books_updates_as_expected() + { + $books = Book::all()->take(2); + + $this->asAdmin()->get('/books') + ->assertElementNotContains('#recents', $books[0]->name) + ->assertElementNotContains('#recents', $books[1]->name); + + $this->get($books[0]->getUrl()); + $this->get($books[1]->getUrl()); + + $this->get('/books') + ->assertElementContains('#recents', $books[0]->name) + ->assertElementContains('#recents', $books[1]->name); + } + + public function test_popular_books_updates_upon_visits() + { + $books = Book::all()->take(2); + + $this->asAdmin()->get('/books') + ->assertElementNotContains('#popular', $books[0]->name) + ->assertElementNotContains('#popular', $books[1]->name); + + $this->get($books[0]->getUrl()); + $this->get($books[1]->getUrl()); + $this->get($books[0]->getUrl()); + + $this->get('/books') + ->assertElementContains('#popular .book:nth-child(1)', $books[0]->name) + ->assertElementContains('#popular .book:nth-child(2)', $books[1]->name); + } + + public function test_books_view_shows_view_toggle_option() + { + /** @var Book $book */ + $editor = $this->getEditor(); + setting()->putUser($editor, 'books_view_type', 'list'); + + $resp = $this->actingAs($editor)->get('/books'); + $resp->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'Grid View'); + $resp->assertElementExists('input[name="view_type"][value="grid"]'); + + $resp = $this->patch("/settings/users/{$editor->id}/switch-books-view", ['view_type' => 'grid']); + $resp->assertRedirect(); + $this->assertEquals('grid', setting()->getUser($editor, 'books_view_type')); + + $resp = $this->actingAs($editor)->get('/books'); + $resp->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'List View'); + $resp->assertElementExists('input[name="view_type"][value="list"]'); + + $resp = $this->patch("/settings/users/{$editor->id}/switch-books-view", ['view_type' => 'list']); + $resp->assertRedirect(); + $this->assertEquals('list', setting()->getUser($editor, 'books_view_type')); + } + + public function test_slug_multi_byte_url_safe() + { + $book = $this->newBook([ + 'name' => 'информация', + ]); + + $this->assertEquals('informatsiya', $book->slug); + + $book = $this->newBook([ + 'name' => '¿Qué?', + ]); + + $this->assertEquals('que', $book->slug); + } + + public function test_slug_format() + { + $book = $this->newBook([ + 'name' => 'PartA / PartB / PartC', + ]); + + $this->assertEquals('parta-partb-partc', $book->slug); + } } diff --git a/tests/Entity/ChapterTest.php b/tests/Entity/ChapterTest.php index 45c132e89..ea29ece5d 100644 --- a/tests/Entity/ChapterTest.php +++ b/tests/Entity/ChapterTest.php @@ -2,12 +2,36 @@ namespace Tests\Entity; +use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use Tests\TestCase; class ChapterTest extends TestCase { - public function test_chapter_delete() + public function test_create() + { + /** @var Book $book */ + $book = Book::query()->first(); + + $chapter = factory(Chapter::class)->make([ + 'name' => 'My First Chapter', + ]); + + $resp = $this->asEditor()->get($book->getUrl()); + $resp->assertElementContains('a[href="' . $book->getUrl('/create-chapter') . '"]', 'New Chapter'); + + $resp = $this->get($book->getUrl('/create-chapter')); + $resp->assertElementContains('form[action="' . $book->getUrl('/create-chapter') . '"][method="POST"]', 'Save Chapter'); + + $resp = $this->post($book->getUrl('/create-chapter'), $chapter->only('name', 'description')); + $resp->assertRedirect($book->getUrl('/chapter/my-first-chapter')); + + $resp = $this->get($book->getUrl('/chapter/my-first-chapter')); + $resp->assertSee($chapter->name); + $resp->assertSee($chapter->description); + } + + public function test_delete() { $chapter = Chapter::query()->whereHas('pages')->first(); $this->assertNull($chapter->deleted_at); diff --git a/tests/Entity/EntityAccessTest.php b/tests/Entity/EntityAccessTest.php new file mode 100644 index 000000000..f2f244538 --- /dev/null +++ b/tests/Entity/EntityAccessTest.php @@ -0,0 +1,52 @@ +getEditor(); + $updater = $this->getViewer(); + $entities = $this->createEntityChainBelongingToUser($creator, $updater); + app()->make(UserRepo::class)->destroy($creator); + app()->make(PageRepo::class)->update($entities['page'], ['html' => '

hello!

>']); + + $this->checkEntitiesViewable($entities); + } + + public function test_entities_viewable_after_updater_deletion() + { + // Create required assets and revisions + $creator = $this->getViewer(); + $updater = $this->getEditor(); + $entities = $this->createEntityChainBelongingToUser($creator, $updater); + app()->make(UserRepo::class)->destroy($updater); + app()->make(PageRepo::class)->update($entities['page'], ['html' => '

Hello there!

']); + + $this->checkEntitiesViewable($entities); + } + + /** + * @param array $entities + */ + private function checkEntitiesViewable(array $entities) + { + // Check pages and books are visible. + $this->asAdmin(); + foreach ($entities as $entity) { + $this->get($entity->getUrl()) + ->assertStatus(200) + ->assertSee($entity->name); + } + + // Check revision listing shows no errors. + $this->get($entities['page']->getUrl('/revisions'))->assertStatus(200); + } +} diff --git a/tests/Entity/EntityTest.php b/tests/Entity/EntityTest.php deleted file mode 100644 index f8c88b1fe..000000000 --- a/tests/Entity/EntityTest.php +++ /dev/null @@ -1,319 +0,0 @@ -bookCreation(); - $chapter = $this->chapterCreation($book); - $this->pageCreation($chapter); - - // Test Updating - $this->bookUpdate($book); - } - - public function bookUpdate(Book $book) - { - $newName = $book->name . ' Updated'; - $this->asAdmin() - // Go to edit screen - ->visit($book->getUrl() . '/edit') - ->see($book->name) - // Submit new name - ->type($newName, '#name') - ->press('Save Book') - // Check page url and text - ->seePageIs($book->getUrl() . '-updated') - ->see($newName); - - return Book::find($book->id); - } - - public function test_book_sort_page_shows() - { - $books = Book::all(); - $bookToSort = $books[0]; - $this->asAdmin() - ->visit($bookToSort->getUrl()) - ->click('Sort') - ->seePageIs($bookToSort->getUrl() . '/sort') - ->seeStatusCode(200) - ->see($bookToSort->name); - } - - public function test_book_sort_item_returns_book_content() - { - $books = Book::all(); - $bookToSort = $books[0]; - $firstPage = $bookToSort->pages[0]; - $firstChapter = $bookToSort->chapters[0]; - $this->asAdmin() - ->visit($bookToSort->getUrl() . '/sort-item') - // Ensure book details are returned - ->see($bookToSort->name) - ->see($firstPage->name) - ->see($firstChapter->name); - } - - public function test_toggle_book_view() - { - $editor = $this->getEditor(); - setting()->putUser($editor, 'books_view_type', 'grid'); - - $this->actingAs($editor) - ->visit('/books') - ->pageHasElement('.featured-image-container') - ->submitForm('List View') - // Check redirection. - ->seePageIs('/books') - ->pageNotHasElement('.featured-image-container'); - - $this->actingAs($editor) - ->visit('/books') - ->submitForm('Grid View') - ->seePageIs('/books') - ->pageHasElement('.featured-image-container'); - } - - public function pageCreation($chapter) - { - $page = factory(Page::class)->make([ - 'name' => 'My First Page', - ]); - - $this->asAdmin() - // Navigate to page create form - ->visit($chapter->getUrl()) - ->click('New Page'); - - $draftPage = Page::where('draft', '=', true)->orderBy('created_at', 'desc')->first(); - - $this->seePageIs($draftPage->getUrl()) - // Fill out form - ->type($page->name, '#name') - ->type($page->html, '#html') - ->press('Save Page') - // Check redirect and page - ->seePageIs($chapter->book->getUrl() . '/page/my-first-page') - ->see($page->name); - - $page = Page::where('slug', '=', 'my-first-page')->where('chapter_id', '=', $chapter->id)->first(); - - return $page; - } - - public function chapterCreation(Book $book) - { - $chapter = factory(Chapter::class)->make([ - 'name' => 'My First Chapter', - ]); - - $this->asAdmin() - // Navigate to chapter create page - ->visit($book->getUrl()) - ->click('New Chapter') - ->seePageIs($book->getUrl() . '/create-chapter') - // Fill out form - ->type($chapter->name, '#name') - ->type($chapter->description, '#description') - ->press('Save Chapter') - // Check redirect and landing page - ->seePageIs($book->getUrl() . '/chapter/my-first-chapter') - ->see($chapter->name)->see($chapter->description); - - $chapter = Chapter::where('slug', '=', 'my-first-chapter')->where('book_id', '=', $book->id)->first(); - - return $chapter; - } - - public function bookCreation() - { - $book = factory(Book::class)->make([ - 'name' => 'My First Book', - ]); - $this->asAdmin() - ->visit('/books') - // Choose to create a book - ->click('Create New Book') - ->seePageIs('/create-book') - // Fill out form & save - ->type($book->name, '#name') - ->type($book->description, '#description') - ->press('Save Book') - // Check it redirects correctly - ->seePageIs('/books/my-first-book') - ->see($book->name)->see($book->description); - - // Ensure duplicate names are given different slugs - $this->asAdmin() - ->visit('/create-book') - ->type($book->name, '#name') - ->type($book->description, '#description') - ->press('Save Book'); - - $expectedPattern = '/\/books\/my-first-book-[0-9a-zA-Z]{3}/'; - $this->assertMatchesRegularExpression($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n"); - - $book = Book::where('slug', '=', 'my-first-book')->first(); - - return $book; - } - - public function test_entities_viewable_after_creator_deletion() - { - // Create required assets and revisions - $creator = $this->getEditor(); - $updater = $this->getEditor(); - $entities = $this->createEntityChainBelongingToUser($creator, $updater); - $this->actingAs($creator); - app(UserRepo::class)->destroy($creator); - app(PageRepo::class)->update($entities['page'], ['html' => '

hello!

>']); - - $this->checkEntitiesViewable($entities); - } - - public function test_entities_viewable_after_updater_deletion() - { - // Create required assets and revisions - $creator = $this->getEditor(); - $updater = $this->getEditor(); - $entities = $this->createEntityChainBelongingToUser($creator, $updater); - $this->actingAs($updater); - app(UserRepo::class)->destroy($updater); - app(PageRepo::class)->update($entities['page'], ['html' => '

Hello there!

']); - - $this->checkEntitiesViewable($entities); - } - - private function checkEntitiesViewable($entities) - { - // Check pages and books are visible. - $this->asAdmin(); - $this->visit($entities['book']->getUrl())->seeStatusCode(200) - ->visit($entities['chapter']->getUrl())->seeStatusCode(200) - ->visit($entities['page']->getUrl())->seeStatusCode(200); - // Check revision listing shows no errors. - $this->visit($entities['page']->getUrl()) - ->click('Revisions')->seeStatusCode(200); - } - - public function test_recently_updated_pages_view() - { - $user = $this->getEditor(); - $content = $this->createEntityChainBelongingToUser($user); - - $this->asAdmin()->visit('/pages/recently-updated') - ->seeInNthElement('.entity-list .page', 0, $content['page']->name); - } - - public function test_old_page_slugs_redirect_to_new_pages() - { - $page = Page::first(); - $pageUrl = $page->getUrl(); - $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page'; - // Need to save twice since revisions are not generated in seeder. - $this->asAdmin()->visit($pageUrl) - ->clickInElement('#content', 'Edit') - ->type('super test', '#name') - ->press('Save Page'); - - $page = Page::first(); - $pageUrl = $page->getUrl(); - - // Second Save - $this->visit($pageUrl) - ->clickInElement('#content', 'Edit') - ->type('super test page', '#name') - ->press('Save Page') - // Check redirect - ->seePageIs($newPageUrl); - - $this->visit($pageUrl) - ->seePageIs($newPageUrl); - } - - public function test_recently_updated_pages_on_home() - { - $page = Page::orderBy('updated_at', 'asc')->first(); - Page::where('id', '!=', $page->id)->update([ - 'updated_at' => Carbon::now()->subSecond(1), - ]); - $this->asAdmin()->visit('/') - ->dontSeeInElement('#recently-updated-pages', $page->name); - $this->visit($page->getUrl() . '/edit') - ->press('Save Page') - ->visit('/') - ->seeInElement('#recently-updated-pages', $page->name); - } - - public function test_slug_multi_byte_url_safe() - { - $book = $this->newBook([ - 'name' => 'информация', - ]); - - $this->assertEquals('informatsiya', $book->slug); - - $book = $this->newBook([ - 'name' => '¿Qué?', - ]); - - $this->assertEquals('que', $book->slug); - } - - public function test_slug_format() - { - $book = $this->newBook([ - 'name' => 'PartA / PartB / PartC', - ]); - - $this->assertEquals('parta-partb-partc', $book->slug); - } - - public function test_shelf_cancel_creation_returns_to_correct_place() - { - $shelf = Bookshelf::first(); - - // Cancel button from shelf goes back to shelf - $this->asEditor()->visit($shelf->getUrl('/create-book')) - ->see('Cancel') - ->click('Cancel') - ->seePageIs($shelf->getUrl()); - - // Cancel button from books goes back to books - $this->asEditor()->visit('/create-book') - ->see('Cancel') - ->click('Cancel') - ->seePageIs('/books'); - - // Cancel button from book edit goes back to book - $book = Book::first(); - - $this->asEditor()->visit($book->getUrl('/edit')) - ->see('Cancel') - ->click('Cancel') - ->seePageIs($book->getUrl()); - } - - public function test_page_within_chapter_deletion_returns_to_chapter() - { - $chapter = Chapter::query()->first(); - $page = $chapter->pages()->first(); - - $this->asEditor()->visit($page->getUrl('/delete')) - ->submitForm('Confirm') - ->seePageIs($chapter->getUrl()); - } -} diff --git a/tests/Entity/MarkdownTest.php b/tests/Entity/MarkdownTest.php deleted file mode 100644 index 7de7ea11b..000000000 --- a/tests/Entity/MarkdownTest.php +++ /dev/null @@ -1,53 +0,0 @@ -page = \BookStack\Entities\Models\Page::first(); - } - - protected function setMarkdownEditor() - { - $this->setSettings(['app-editor' => 'markdown']); - } - - public function test_default_editor_is_wysiwyg() - { - $this->assertEquals(setting('app-editor'), 'wysiwyg'); - $this->asAdmin()->visit($this->page->getUrl() . '/edit') - ->pageHasElement('#html-editor'); - } - - public function test_markdown_setting_shows_markdown_editor() - { - $this->setMarkdownEditor(); - $this->asAdmin()->visit($this->page->getUrl() . '/edit') - ->pageNotHasElement('#html-editor') - ->pageHasElement('#markdown-editor'); - } - - public function test_markdown_content_given_to_editor() - { - $this->setMarkdownEditor(); - $mdContent = '# hello. This is a test'; - $this->page->markdown = $mdContent; - $this->page->save(); - $this->asAdmin()->visit($this->page->getUrl() . '/edit') - ->seeInField('markdown', $mdContent); - } - - public function test_html_content_given_to_editor_if_no_markdown() - { - $this->setMarkdownEditor(); - $this->asAdmin()->visit($this->page->getUrl() . '/edit') - ->seeInField('markdown', $this->page->html); - } -} diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php index 68059af6e..b2fa4bb31 100644 --- a/tests/Entity/PageDraftTest.php +++ b/tests/Entity/PageDraftTest.php @@ -2,12 +2,16 @@ namespace Tests\Entity; +use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\PageRepo; -use Tests\BrowserKitTest; +use Tests\TestCase; -class PageDraftTest extends BrowserKitTest +class PageDraftTest extends TestCase { + /** + * @var Page + */ protected $page; /** @@ -18,99 +22,101 @@ class PageDraftTest extends BrowserKitTest public function setUp(): void { parent::setUp(); - $this->page = \BookStack\Entities\Models\Page::first(); - $this->pageRepo = app(PageRepo::class); + $this->page = Page::query()->first(); + $this->pageRepo = app()->make(PageRepo::class); } public function test_draft_content_shows_if_available() { $addedContent = '

test message content

'; - $this->asAdmin()->visit($this->page->getUrl('/edit')) - ->dontSeeInField('html', $addedContent); + + $this->asAdmin()->get($this->page->getUrl('/edit')) + ->assertElementNotContains('[name="html"]', $addedContent); $newContent = $this->page->html . $addedContent; $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]); - $this->asAdmin()->visit($this->page->getUrl('/edit')) - ->seeInField('html', $newContent); + $this->asAdmin()->get($this->page->getUrl('/edit')) + ->assertElementContains('[name="html"]', $newContent); } public function test_draft_not_visible_by_others() { $addedContent = '

test message content

'; - $this->asAdmin()->visit($this->page->getUrl('/edit')) - ->dontSeeInField('html', $addedContent); + $this->asAdmin()->get($this->page->getUrl('/edit')) + ->assertElementNotContains('[name="html"]', $addedContent); $newContent = $this->page->html . $addedContent; $newUser = $this->getEditor(); $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]); - $this->actingAs($newUser)->visit($this->page->getUrl('/edit')) - ->dontSeeInField('html', $newContent); + $this->actingAs($newUser)->get($this->page->getUrl('/edit')) + ->assertElementNotContains('[name="html"]', $newContent); } public function test_alert_message_shows_if_editing_draft() { $this->asAdmin(); $this->pageRepo->updatePageDraft($this->page, ['html' => 'test content']); - $this->asAdmin()->visit($this->page->getUrl('/edit')) - ->see('You are currently editing a draft'); + $this->asAdmin()->get($this->page->getUrl('/edit')) + ->assertSee('You are currently editing a draft'); } public function test_alert_message_shows_if_someone_else_editing() { - $nonEditedPage = \BookStack\Entities\Models\Page::take(10)->get()->last(); + $nonEditedPage = Page::query()->take(10)->get()->last(); $addedContent = '

test message content

'; - $this->asAdmin()->visit($this->page->getUrl('/edit')) - ->dontSeeInField('html', $addedContent); + $this->asAdmin()->get($this->page->getUrl('/edit')) + ->assertElementNotContains('[name="html"]', $addedContent); $newContent = $this->page->html . $addedContent; $newUser = $this->getEditor(); $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]); $this->actingAs($newUser) - ->visit($this->page->getUrl('/edit')) - ->see('Admin has started editing this page'); + ->get($this->page->getUrl('/edit')) + ->assertSee('Admin has started editing this page'); $this->flushSession(); - $this->visit($nonEditedPage->getUrl() . '/edit') - ->dontSeeInElement('.notification', 'Admin has started editing this page'); + $this->get($nonEditedPage->getUrl() . '/edit') + ->assertElementNotContains('.notification', 'Admin has started editing this page'); } public function test_draft_pages_show_on_homepage() { - $book = \BookStack\Entities\Models\Book::first(); - $this->asAdmin()->visit('/') - ->dontSeeInElement('#recent-drafts', 'New Page') - ->visit($book->getUrl() . '/create-page') - ->visit('/') - ->seeInElement('#recent-drafts', 'New Page'); + /** @var Book $book */ + $book = Book::query()->first(); + $this->asAdmin()->get('/') + ->assertElementNotContains('#recent-drafts', 'New Page'); + + $this->get($book->getUrl() . '/create-page'); + + $this->get('/')->assertElementContains('#recent-drafts', 'New Page'); } public function test_draft_pages_not_visible_by_others() { - $book = \BookStack\Entities\Models\Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $chapter = $book->chapters->first(); $newUser = $this->getEditor(); - $this->actingAs($newUser)->visit('/') - ->visit($book->getUrl('/create-page')) - ->visit($chapter->getUrl('/create-page')) - ->visit($book->getUrl()) - ->seeInElement('.book-contents', 'New Page'); + $this->actingAs($newUser)->get($book->getUrl('/create-page')); + $this->get($chapter->getUrl('/create-page')); + $this->get($book->getUrl()) + ->assertElementContains('.book-contents', 'New Page'); - $this->asAdmin() - ->visit($book->getUrl()) - ->dontSeeInElement('.book-contents', 'New Page') - ->visit($chapter->getUrl()) - ->dontSeeInElement('.book-contents', 'New Page'); + $this->asAdmin()->get($book->getUrl()) + ->assertElementNotContains('.book-contents', 'New Page'); + $this->get($chapter->getUrl()) + ->assertElementNotContains('.book-contents', 'New Page'); } public function test_page_html_in_ajax_fetch_response() { $this->asAdmin(); + /** @var Page $page */ $page = Page::query()->first(); - $this->getJson('/ajax/page/' . $page->id); - $this->seeJson([ + $this->getJson('/ajax/page/' . $page->id)->assertJson([ 'html' => $page->html, ]); } diff --git a/tests/Entity/PageEditorTest.php b/tests/Entity/PageEditorTest.php new file mode 100644 index 000000000..9b0a8f188 --- /dev/null +++ b/tests/Entity/PageEditorTest.php @@ -0,0 +1,77 @@ +page = Page::query()->first(); + } + + public function test_default_editor_is_wysiwyg() + { + $this->assertEquals('wysiwyg', setting('app-editor')); + $this->asAdmin()->get($this->page->getUrl() . '/edit') + ->assertElementExists('#html-editor'); + } + + public function test_markdown_setting_shows_markdown_editor() + { + $this->setSettings(['app-editor' => 'markdown']); + $this->asAdmin()->get($this->page->getUrl() . '/edit') + ->assertElementNotExists('#html-editor') + ->assertElementExists('#markdown-editor'); + } + + public function test_markdown_content_given_to_editor() + { + $this->setSettings(['app-editor' => 'markdown']); + + $mdContent = '# hello. This is a test'; + $this->page->markdown = $mdContent; + $this->page->save(); + + $this->asAdmin()->get($this->page->getUrl() . '/edit') + ->assertElementContains('[name="markdown"]', $mdContent); + } + + public function test_html_content_given_to_editor_if_no_markdown() + { + $this->setSettings(['app-editor' => 'markdown']); + $this->asAdmin()->get($this->page->getUrl() . '/edit') + ->assertElementContains('[name="markdown"]', $this->page->html); + } + + public function test_empty_markdown_still_saves_without_error() + { + $this->setSettings(['app-editor' => 'markdown']); + /** @var Book $book */ + $book = Book::query()->first(); + + $this->asEditor()->get($book->getUrl('/create-page')); + $draft = Page::query()->where('book_id', '=', $book->id) + ->where('draft', '=', true)->first(); + + $details = [ + 'name' => 'my page', + 'markdown' => '', + ]; + $resp = $this->post($book->getUrl("/draft/{$draft->id}"), $details); + $resp->assertRedirect(); + + $this->assertDatabaseHas('pages', [ + 'markdown' => $details['markdown'], + 'id' => $draft->id, + 'draft' => false, + ]); + } +} diff --git a/tests/Entity/PageTest.php b/tests/Entity/PageTest.php index 2721c225c..313fc77f0 100644 --- a/tests/Entity/PageTest.php +++ b/tests/Entity/PageTest.php @@ -3,11 +3,40 @@ namespace Tests\Entity; use BookStack\Entities\Models\Book; +use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; +use Carbon\Carbon; use Tests\TestCase; class PageTest extends TestCase { + public function test_create() + { + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); + $page = factory(Page::class)->make([ + 'name' => 'My First Page', + ]); + + $resp = $this->asEditor()->get($chapter->getUrl()); + $resp->assertElementContains('a[href="' . $chapter->getUrl('/create-page') . '"]', 'New Page'); + + $resp = $this->get($chapter->getUrl('/create-page')); + /** @var Page $draftPage */ + $draftPage = Page::query() + ->where('draft', '=', true) + ->orderBy('created_at', 'desc') + ->first(); + $resp->assertRedirect($draftPage->getUrl()); + + $resp = $this->get($draftPage->getUrl()); + $resp->assertElementContains('form[action="' . $draftPage->getUrl() . '"][method="POST"]', 'Save Page'); + + $resp = $this->post($draftPage->getUrl(), $draftPage->only('name', 'html')); + $draftPage->refresh(); + $resp->assertRedirect($draftPage->getUrl()); + } + public function test_page_view_when_creator_is_deleted_but_owner_exists() { $page = Page::query()->first(); @@ -190,26 +219,65 @@ class PageTest extends TestCase ]); } - public function test_empty_markdown_still_saves_without_error() + public function test_old_page_slugs_redirect_to_new_pages() { - $this->setSettings(['app-editor' => 'markdown']); - $book = Book::query()->first(); + /** @var Page $page */ + $page = Page::query()->first(); - $this->asEditor()->get($book->getUrl('/create-page')); - $draft = Page::query()->where('book_id', '=', $book->id) - ->where('draft', '=', true)->first(); - - $details = [ - 'name' => 'my page', - 'markdown' => '', - ]; - $resp = $this->post($book->getUrl("/draft/{$draft->id}"), $details); - $resp->assertRedirect(); - - $this->assertDatabaseHas('pages', [ - 'markdown' => $details['markdown'], - 'id' => $draft->id, - 'draft' => false, + // Need to save twice since revisions are not generated in seeder. + $this->asAdmin()->put($page->getUrl(), [ + 'name' => 'super test', + 'html' => '

', ]); + + $page->refresh(); + $pageUrl = $page->getUrl(); + + $this->put($pageUrl, [ + 'name' => 'super test page', + 'html' => '

', + ]); + + $this->get($pageUrl) + ->assertRedirect("/books/{$page->book->slug}/page/super-test-page"); + } + + public function test_page_within_chapter_deletion_returns_to_chapter() + { + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); + $page = $chapter->pages()->first(); + + $this->asEditor()->delete($page->getUrl()) + ->assertRedirect($chapter->getUrl()); + } + + public function test_recently_updated_pages_view() + { + $user = $this->getEditor(); + $content = $this->createEntityChainBelongingToUser($user); + + $this->asAdmin()->get('/pages/recently-updated') + ->assertElementContains('.entity-list .page:nth-child(1)', $content['page']->name); + } + + public function test_recently_updated_pages_on_home() + { + /** @var Page $page */ + $page = Page::query()->orderBy('updated_at', 'asc')->first(); + Page::query()->where('id', '!=', $page->id)->update([ + 'updated_at' => Carbon::now()->subSecond(1), + ]); + + $this->asAdmin()->get('/') + ->assertElementNotContains('#recently-updated-pages', $page->name); + + $this->put($page->getUrl(), [ + 'name' => $page->name, + 'html' => $page->html, + ]); + + $this->get('/') + ->assertElementContains('#recently-updated-pages', $page->name); } } diff --git a/tests/Entity/SortTest.php b/tests/Entity/SortTest.php index e058b39aa..5cfc5c3c5 100644 --- a/tests/Entity/SortTest.php +++ b/tests/Entity/SortTest.php @@ -216,6 +216,19 @@ class SortTest extends TestCase $this->assertEquals($newBook->id, $pageToCheck->book_id); } + public function test_book_sort_page_shows() + { + /** @var Book $bookToSort */ + $bookToSort = Book::query()->first(); + + $resp = $this->asAdmin()->get($bookToSort->getUrl()); + $resp->assertElementExists('a[href="' . $bookToSort->getUrl('/sort') . '"]'); + + $resp = $this->get($bookToSort->getUrl('/sort')); + $resp->assertStatus(200); + $resp->assertSee($bookToSort->name); + } + public function test_book_sort() { $oldBook = Book::query()->first(); @@ -259,6 +272,21 @@ class SortTest extends TestCase $checkResp->assertSee($newBook->name); } + public function test_book_sort_item_returns_book_content() + { + $books = Book::all(); + $bookToSort = $books[0]; + $firstPage = $bookToSort->pages[0]; + $firstChapter = $bookToSort->chapters[0]; + + $resp = $this->asAdmin()->get($bookToSort->getUrl() . '/sort-item'); + + // Ensure book details are returned + $resp->assertSee($bookToSort->name); + $resp->assertSee($firstPage->name); + $resp->assertSee($firstChapter->name); + } + public function test_pages_in_book_show_sorted_by_priority() { /** @var Book $book */ diff --git a/tests/HomepageTest.php b/tests/HomepageTest.php index db4e94c6d..e27b78774 100644 --- a/tests/HomepageTest.php +++ b/tests/HomepageTest.php @@ -5,6 +5,7 @@ namespace Tests; use BookStack\Auth\Role; use BookStack\Auth\User; use BookStack\Entities\Models\Bookshelf; +use BookStack\Entities\Models\Page; class HomepageTest extends TestCase { @@ -78,6 +79,25 @@ class HomepageTest extends TestCase $pageDeleteReq->assertSessionMissing('error'); } + public function test_custom_homepage_renders_includes() + { + $this->asEditor(); + /** @var Page $included */ + $included = Page::query()->first(); + $content = str_repeat('This is the body content of my custom homepage.', 20); + $included->html = $content; + $included->save(); + + $name = 'My custom homepage'; + $customPage = $this->newPage(['name' => $name, 'html' => '{{@' . $included->id . '}}']); + $this->setSettings(['app-homepage' => $customPage->id]); + $this->setSettings(['app-homepage-type' => 'page']); + + $homeVisit = $this->get('/'); + $homeVisit->assertSee($name); + $homeVisit->assertSee($content); + } + public function test_set_book_homepage() { $editor = $this->getEditor(); diff --git a/tests/Permissions/EntityPermissionsTest.php b/tests/Permissions/EntityPermissionsTest.php index 77c62fdb5..bb011cfc6 100644 --- a/tests/Permissions/EntityPermissionsTest.php +++ b/tests/Permissions/EntityPermissionsTest.php @@ -9,9 +9,9 @@ use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use Illuminate\Support\Str; -use Tests\BrowserKitTest; +use Tests\TestCase; -class EntityPermissionsTest extends BrowserKitTest +class EntityPermissionsTest extends TestCase { /** * @var User @@ -41,608 +41,598 @@ class EntityPermissionsTest extends BrowserKitTest public function test_bookshelf_view_restriction() { - $shelf = Bookshelf::first(); + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); $this->actingAs($this->user) - ->visit($shelf->getUrl()) - ->seePageIs($shelf->getUrl()); + ->get($shelf->getUrl()) + ->assertStatus(200); $this->setRestrictionsForTestRoles($shelf, []); - $this->forceVisit($shelf->getUrl()) - ->see('Bookshelf not found'); + $this->followingRedirects()->get($shelf->getUrl()) + ->assertSee('Bookshelf not found'); $this->setRestrictionsForTestRoles($shelf, ['view']); - $this->visit($shelf->getUrl()) - ->see($shelf->name); + $this->get($shelf->getUrl()) + ->assertSee($shelf->name); } public function test_bookshelf_update_restriction() { - $shelf = Bookshelf::first(); + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); $this->actingAs($this->user) - ->visit($shelf->getUrl('/edit')) - ->see('Edit Book'); + ->get($shelf->getUrl('/edit')) + ->assertSee('Edit Book'); $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']); - $this->forceVisit($shelf->getUrl('/edit')) - ->see('You do not have permission')->seePageIs('/'); + $resp = $this->get($shelf->getUrl('/edit')) + ->assertRedirect('/'); + $this->followRedirects($resp)->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($shelf, ['view', 'update']); - $this->visit($shelf->getUrl('/edit')) - ->seePageIs($shelf->getUrl('/edit')); + $this->get($shelf->getUrl('/edit')) + ->assertOk(); } public function test_bookshelf_delete_restriction() { - $shelf = Book::first(); + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); $this->actingAs($this->user) - ->visit($shelf->getUrl('/delete')) - ->see('Delete Book'); + ->get($shelf->getUrl('/delete')) + ->assertSee('Delete Book'); $this->setRestrictionsForTestRoles($shelf, ['view', 'update']); - $this->forceVisit($shelf->getUrl('/delete')) - ->see('You do not have permission')->seePageIs('/'); + $this->get($shelf->getUrl('/delete'))->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']); - $this->visit($shelf->getUrl('/delete')) - ->seePageIs($shelf->getUrl('/delete'))->see('Delete Book'); + $this->get($shelf->getUrl('/delete')) + ->assertOk() + ->assertSee('Delete Book'); } public function test_book_view_restriction() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); $bookUrl = $book->getUrl(); $this->actingAs($this->user) - ->visit($bookUrl) - ->seePageIs($bookUrl); + ->get($bookUrl) + ->assertOk(); $this->setRestrictionsForTestRoles($book, []); - $this->forceVisit($bookUrl) - ->see('Book not found'); - $this->forceVisit($bookPage->getUrl()) - ->see('Page not found'); - $this->forceVisit($bookChapter->getUrl()) - ->see('Chapter not found'); + $this->followingRedirects()->get($bookUrl) + ->assertSee('Book not found'); + $this->followingRedirects()->get($bookPage->getUrl()) + ->assertSee('Page not found'); + $this->followingRedirects()->get($bookChapter->getUrl()) + ->assertSee('Chapter not found'); $this->setRestrictionsForTestRoles($book, ['view']); - $this->visit($bookUrl) - ->see($book->name); - $this->visit($bookPage->getUrl()) - ->see($bookPage->name); - $this->visit($bookChapter->getUrl()) - ->see($bookChapter->name); + $this->get($bookUrl) + ->assertSee($book->name); + $this->get($bookPage->getUrl()) + ->assertSee($bookPage->name); + $this->get($bookChapter->getUrl()) + ->assertSee($bookChapter->name); } public function test_book_create_restriction() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookUrl = $book->getUrl(); $this->actingAs($this->viewer) - ->visit($bookUrl) - ->dontSeeInElement('.actions', 'New Page') - ->dontSeeInElement('.actions', 'New Chapter'); + ->get($bookUrl) + ->assertElementNotContains('.actions', 'New Page') + ->assertElementNotContains('.actions', 'New Chapter'); $this->actingAs($this->user) - ->visit($bookUrl) - ->seeInElement('.actions', 'New Page') - ->seeInElement('.actions', 'New Chapter'); + ->get($bookUrl) + ->assertElementContains('.actions', 'New Page') + ->assertElementContains('.actions', 'New Chapter'); $this->setRestrictionsForTestRoles($book, ['view', 'delete', 'update']); - $this->forceVisit($bookUrl . '/create-chapter') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookUrl . '/create-page') - ->see('You do not have permission')->seePageIs('/'); - $this->visit($bookUrl)->dontSeeInElement('.actions', 'New Page') - ->dontSeeInElement('.actions', 'New Chapter'); + $this->get($bookUrl . '/create-chapter')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + + $this->get($bookUrl . '/create-page')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + + $this->get($bookUrl) + ->assertElementNotContains('.actions', 'New Page') + ->assertElementNotContains('.actions', 'New Chapter'); $this->setRestrictionsForTestRoles($book, ['view', 'create']); - $this->visit($bookUrl . '/create-chapter') - ->type('test chapter', 'name') - ->type('test description for chapter', 'description') - ->press('Save Chapter') - ->seePageIs($bookUrl . '/chapter/test-chapter'); - $this->visit($bookUrl . '/create-page') - ->type('test page', 'name') - ->type('test content', 'html') - ->press('Save Page') - ->seePageIs($bookUrl . '/page/test-page'); - $this->visit($bookUrl)->seeInElement('.actions', 'New Page') - ->seeInElement('.actions', 'New Chapter'); + $resp = $this->post($book->getUrl('/create-chapter'), [ + 'name' => 'test chapter', + 'description' => 'desc', + ]); + $resp->assertRedirect($book->getUrl('/chapter/test-chapter')); + + $this->get($book->getUrl('/create-page')); + /** @var Page $page */ + $page = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first(); + $resp = $this->post($page->getUrl(), [ + 'name' => 'test page', + 'html' => 'test content', + ]); + $resp->assertRedirect($book->getUrl('/page/test-page')); + + $this->get($bookUrl) + ->assertElementContains('.actions', 'New Page') + ->assertElementContains('.actions', 'New Chapter'); } public function test_book_update_restriction() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); $bookUrl = $book->getUrl(); $this->actingAs($this->user) - ->visit($bookUrl . '/edit') - ->see('Edit Book'); + ->get($bookUrl . '/edit') + ->assertSee('Edit Book'); $this->setRestrictionsForTestRoles($book, ['view', 'delete']); - $this->forceVisit($bookUrl . '/edit') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookPage->getUrl() . '/edit') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookChapter->getUrl() . '/edit') - ->see('You do not have permission')->seePageIs('/'); + $this->get($bookUrl . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookPage->getUrl() . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookChapter->getUrl() . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($book, ['view', 'update']); - $this->visit($bookUrl . '/edit') - ->seePageIs($bookUrl . '/edit'); - $this->visit($bookPage->getUrl() . '/edit') - ->seePageIs($bookPage->getUrl() . '/edit'); - $this->visit($bookChapter->getUrl() . '/edit') - ->see('Edit Chapter'); + $this->get($bookUrl . '/edit')->assertOk(); + $this->get($bookPage->getUrl() . '/edit')->assertOk(); + $this->get($bookChapter->getUrl() . '/edit')->assertSee('Edit Chapter'); } public function test_book_delete_restriction() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); $bookUrl = $book->getUrl(); - $this->actingAs($this->user) - ->visit($bookUrl . '/delete') - ->see('Delete Book'); + $this->actingAs($this->user)->get($bookUrl . '/delete') + ->assertSee('Delete Book'); $this->setRestrictionsForTestRoles($book, ['view', 'update']); - $this->forceVisit($bookUrl . '/delete') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookPage->getUrl() . '/delete') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookChapter->getUrl() . '/delete') - ->see('You do not have permission')->seePageIs('/'); + $this->get($bookUrl . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookPage->getUrl() . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookChapter->getUrl() . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($book, ['view', 'delete']); - $this->visit($bookUrl . '/delete') - ->seePageIs($bookUrl . '/delete')->see('Delete Book'); - $this->visit($bookPage->getUrl() . '/delete') - ->seePageIs($bookPage->getUrl() . '/delete')->see('Delete Page'); - $this->visit($bookChapter->getUrl() . '/delete') - ->see('Delete Chapter'); + $this->get($bookUrl . '/delete')->assertOk()->assertSee('Delete Book'); + $this->get($bookPage->getUrl('/delete'))->assertOk()->assertSee('Delete Page'); + $this->get($bookChapter->getUrl('/delete'))->assertSee('Delete Chapter'); } public function test_chapter_view_restriction() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $chapterPage = $chapter->pages->first(); $chapterUrl = $chapter->getUrl(); - $this->actingAs($this->user) - ->visit($chapterUrl) - ->seePageIs($chapterUrl); + $this->actingAs($this->user)->get($chapterUrl)->assertOk(); $this->setRestrictionsForTestRoles($chapter, []); - $this->forceVisit($chapterUrl) - ->see('Chapter not found'); - $this->forceVisit($chapterPage->getUrl()) - ->see('Page not found'); + $this->followingRedirects()->get($chapterUrl)->assertSee('Chapter not found'); + $this->followingRedirects()->get($chapterPage->getUrl())->assertSee('Page not found'); $this->setRestrictionsForTestRoles($chapter, ['view']); - $this->visit($chapterUrl) - ->see($chapter->name); - $this->visit($chapterPage->getUrl()) - ->see($chapterPage->name); + $this->get($chapterUrl)->assertSee($chapter->name); + $this->get($chapterPage->getUrl())->assertSee($chapterPage->name); } public function test_chapter_create_restriction() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $chapterUrl = $chapter->getUrl(); $this->actingAs($this->user) - ->visit($chapterUrl) - ->seeInElement('.actions', 'New Page'); + ->get($chapterUrl) + ->assertElementContains('.actions', 'New Page'); $this->setRestrictionsForTestRoles($chapter, ['view', 'delete', 'update']); - $this->forceVisit($chapterUrl . '/create-page') - ->see('You do not have permission')->seePageIs('/'); - $this->visit($chapterUrl)->dontSeeInElement('.actions', 'New Page'); + $this->get($chapterUrl . '/create-page')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($chapterUrl)->assertElementNotContains('.actions', 'New Page'); $this->setRestrictionsForTestRoles($chapter, ['view', 'create']); - $this->visit($chapterUrl . '/create-page') - ->type('test page', 'name') - ->type('test content', 'html') - ->press('Save Page') - ->seePageIs($chapter->book->getUrl() . '/page/test-page'); + $this->get($chapter->getUrl('/create-page')); + /** @var Page $page */ + $page = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first(); + $resp = $this->post($page->getUrl(), [ + 'name' => 'test page', + 'html' => 'test content', + ]); + $resp->assertRedirect($chapter->book->getUrl('/page/test-page')); - $this->visit($chapterUrl)->seeInElement('.actions', 'New Page'); + $this->get($chapterUrl)->assertElementContains('.actions', 'New Page'); } public function test_chapter_update_restriction() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $chapterPage = $chapter->pages->first(); $chapterUrl = $chapter->getUrl(); - $this->actingAs($this->user) - ->visit($chapterUrl . '/edit') - ->see('Edit Chapter'); + $this->actingAs($this->user)->get($chapterUrl . '/edit') + ->assertSee('Edit Chapter'); $this->setRestrictionsForTestRoles($chapter, ['view', 'delete']); - $this->forceVisit($chapterUrl . '/edit') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($chapterPage->getUrl() . '/edit') - ->see('You do not have permission')->seePageIs('/'); + $this->get($chapterUrl . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($chapterPage->getUrl() . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($chapter, ['view', 'update']); - $this->visit($chapterUrl . '/edit') - ->seePageIs($chapterUrl . '/edit')->see('Edit Chapter'); - $this->visit($chapterPage->getUrl() . '/edit') - ->seePageIs($chapterPage->getUrl() . '/edit'); + $this->get($chapterUrl . '/edit')->assertOk()->assertSee('Edit Chapter'); + $this->get($chapterPage->getUrl() . '/edit')->assertOk(); } public function test_chapter_delete_restriction() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $chapterPage = $chapter->pages->first(); $chapterUrl = $chapter->getUrl(); $this->actingAs($this->user) - ->visit($chapterUrl . '/delete') - ->see('Delete Chapter'); + ->get($chapterUrl . '/delete') + ->assertSee('Delete Chapter'); $this->setRestrictionsForTestRoles($chapter, ['view', 'update']); - $this->forceVisit($chapterUrl . '/delete') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($chapterPage->getUrl() . '/delete') - ->see('You do not have permission')->seePageIs('/'); + $this->get($chapterUrl . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($chapterPage->getUrl() . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($chapter, ['view', 'delete']); - $this->visit($chapterUrl . '/delete') - ->seePageIs($chapterUrl . '/delete')->see('Delete Chapter'); - $this->visit($chapterPage->getUrl() . '/delete') - ->seePageIs($chapterPage->getUrl() . '/delete')->see('Delete Page'); + $this->get($chapterUrl . '/delete')->assertOk()->assertSee('Delete Chapter'); + $this->get($chapterPage->getUrl() . '/delete')->assertOk()->assertSee('Delete Page'); } public function test_page_view_restriction() { - $page = Page::first(); + /** @var Page $page */ + $page = Page::query()->first(); $pageUrl = $page->getUrl(); - $this->actingAs($this->user) - ->visit($pageUrl) - ->seePageIs($pageUrl); + $this->actingAs($this->user)->get($pageUrl)->assertOk(); $this->setRestrictionsForTestRoles($page, ['update', 'delete']); - $this->forceVisit($pageUrl) - ->see('Page not found'); + $this->get($pageUrl)->assertSee('Page not found'); $this->setRestrictionsForTestRoles($page, ['view']); - $this->visit($pageUrl) - ->see($page->name); + $this->get($pageUrl)->assertSee($page->name); } public function test_page_update_restriction() { - $page = Chapter::first(); + /** @var Page $page */ + $page = Page::query()->first(); $pageUrl = $page->getUrl(); $this->actingAs($this->user) - ->visit($pageUrl . '/edit') - ->seeInField('name', $page->name); + ->get($pageUrl . '/edit') + ->assertElementExists('input[name="name"][value="' . $page->name . '"]'); $this->setRestrictionsForTestRoles($page, ['view', 'delete']); - $this->forceVisit($pageUrl . '/edit') - ->see('You do not have permission')->seePageIs('/'); + $this->get($pageUrl . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($page, ['view', 'update']); - $this->visit($pageUrl . '/edit') - ->seePageIs($pageUrl . '/edit')->seeInField('name', $page->name); + $this->get($pageUrl . '/edit') + ->assertOk() + ->assertElementExists('input[name="name"][value="' . $page->name . '"]'); } public function test_page_delete_restriction() { - $page = Page::first(); + /** @var Page $page */ + $page = Page::query()->first(); $pageUrl = $page->getUrl(); $this->actingAs($this->user) - ->visit($pageUrl . '/delete') - ->see('Delete Page'); + ->get($pageUrl . '/delete') + ->assertSee('Delete Page'); $this->setRestrictionsForTestRoles($page, ['view', 'update']); - $this->forceVisit($pageUrl . '/delete') - ->see('You do not have permission')->seePageIs('/'); + $this->get($pageUrl . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($page, ['view', 'delete']); - $this->visit($pageUrl . '/delete') - ->seePageIs($pageUrl . '/delete')->see('Delete Page'); + $this->get($pageUrl . '/delete')->assertOk()->assertSee('Delete Page'); + } + + protected function entityRestrictionFormTest(string $model, string $title, string $permission, string $roleId) + { + /** @var Entity $modelInstance */ + $modelInstance = $model::query()->first(); + $this->asAdmin()->get($modelInstance->getUrl('/permissions')) + ->assertSee($title); + + $this->put($modelInstance->getUrl('/permissions'), [ + 'restricted' => 'true', + 'restrictions' => [ + $roleId => [ + $permission => 'true', + ], + ], + ]); + + $this->assertDatabaseHas($modelInstance->getTable(), ['id' => $modelInstance->id, 'restricted' => true]); + $this->assertDatabaseHas('entity_permissions', [ + 'restrictable_id' => $modelInstance->id, + 'restrictable_type' => $modelInstance->getMorphClass(), + 'role_id' => $roleId, + 'action' => $permission, + ]); } public function test_bookshelf_restriction_form() { - $shelf = Bookshelf::first(); - $this->asAdmin()->visit($shelf->getUrl('/permissions')) - ->see('Bookshelf Permissions') - ->check('restricted') - ->check('restrictions[2][view]') - ->press('Save Permissions') - ->seeInDatabase('bookshelves', ['id' => $shelf->id, 'restricted' => true]) - ->seeInDatabase('entity_permissions', [ - 'restrictable_id' => $shelf->id, - 'restrictable_type' => Bookshelf::newModelInstance()->getMorphClass(), - 'role_id' => '2', - 'action' => 'view', - ]); + $this->entityRestrictionFormTest(Bookshelf::class, 'Bookshelf Permissions', 'view', '2'); } public function test_book_restriction_form() { - $book = Book::first(); - $this->asAdmin()->visit($book->getUrl() . '/permissions') - ->see('Book Permissions') - ->check('restricted') - ->check('restrictions[2][view]') - ->press('Save Permissions') - ->seeInDatabase('books', ['id' => $book->id, 'restricted' => true]) - ->seeInDatabase('entity_permissions', [ - 'restrictable_id' => $book->id, - 'restrictable_type' => Book::newModelInstance()->getMorphClass(), - 'role_id' => '2', - 'action' => 'view', - ]); + $this->entityRestrictionFormTest(Book::class, 'Book Permissions', 'view', '2'); } public function test_chapter_restriction_form() { - $chapter = Chapter::first(); - $this->asAdmin()->visit($chapter->getUrl() . '/permissions') - ->see('Chapter Permissions') - ->check('restricted') - ->check('restrictions[2][update]') - ->press('Save Permissions') - ->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true]) - ->seeInDatabase('entity_permissions', [ - 'restrictable_id' => $chapter->id, - 'restrictable_type' => Chapter::newModelInstance()->getMorphClass(), - 'role_id' => '2', - 'action' => 'update', - ]); + $this->entityRestrictionFormTest(Chapter::class, 'Chapter Permissions', 'update', '2'); } public function test_page_restriction_form() { - $page = Page::first(); - $this->asAdmin()->visit($page->getUrl() . '/permissions') - ->see('Page Permissions') - ->check('restricted') - ->check('restrictions[2][delete]') - ->press('Save Permissions') - ->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true]) - ->seeInDatabase('entity_permissions', [ - 'restrictable_id' => $page->id, - 'restrictable_type' => Page::newModelInstance()->getMorphClass(), - 'role_id' => '2', - 'action' => 'delete', - ]); + $this->entityRestrictionFormTest(Page::class, 'Page Permissions', 'delete', '2'); } public function test_restricted_pages_not_visible_in_book_navigation_on_pages() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $page = $chapter->pages->first(); $page2 = $chapter->pages[2]; $this->setRestrictionsForTestRoles($page, []); $this->actingAs($this->user) - ->visit($page2->getUrl()) - ->dontSeeInElement('.sidebar-page-list', $page->name); + ->get($page2->getUrl()) + ->assertElementNotContains('.sidebar-page-list', $page->name); } public function test_restricted_pages_not_visible_in_book_navigation_on_chapters() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $page = $chapter->pages->first(); $this->setRestrictionsForTestRoles($page, []); $this->actingAs($this->user) - ->visit($chapter->getUrl()) - ->dontSeeInElement('.sidebar-page-list', $page->name); + ->get($chapter->getUrl()) + ->assertElementNotContains('.sidebar-page-list', $page->name); } public function test_restricted_pages_not_visible_on_chapter_pages() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $page = $chapter->pages->first(); $this->setRestrictionsForTestRoles($page, []); $this->actingAs($this->user) - ->visit($chapter->getUrl()) - ->dontSee($page->name); + ->get($chapter->getUrl()) + ->assertDontSee($page->name); } public function test_restricted_chapter_pages_not_visible_on_book_page() { + /** @var Chapter $chapter */ $chapter = Chapter::query()->first(); $this->actingAs($this->user) - ->visit($chapter->book->getUrl()) - ->see($chapter->pages->first()->name); + ->get($chapter->book->getUrl()) + ->assertSee($chapter->pages->first()->name); foreach ($chapter->pages as $page) { $this->setRestrictionsForTestRoles($page, []); } $this->actingAs($this->user) - ->visit($chapter->book->getUrl()) - ->dontSee($chapter->pages->first()->name); + ->get($chapter->book->getUrl()) + ->assertDontSee($chapter->pages->first()->name); } public function test_bookshelf_update_restriction_override() { - $shelf = Bookshelf::first(); + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); $this->actingAs($this->viewer) - ->visit($shelf->getUrl('/edit')) - ->dontSee('Edit Book'); + ->get($shelf->getUrl('/edit')) + ->assertDontSee('Edit Book'); $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']); - $this->forceVisit($shelf->getUrl('/edit')) - ->see('You do not have permission')->seePageIs('/'); + $this->get($shelf->getUrl('/edit'))->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($shelf, ['view', 'update']); - $this->visit($shelf->getUrl('/edit')) - ->seePageIs($shelf->getUrl('/edit')); + $this->get($shelf->getUrl('/edit'))->assertOk(); } public function test_bookshelf_delete_restriction_override() { - $shelf = Bookshelf::first(); + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); $this->actingAs($this->viewer) - ->visit($shelf->getUrl('/delete')) - ->dontSee('Delete Book'); + ->get($shelf->getUrl('/delete')) + ->assertDontSee('Delete Book'); $this->setRestrictionsForTestRoles($shelf, ['view', 'update']); - $this->forceVisit($shelf->getUrl('/delete')) - ->see('You do not have permission')->seePageIs('/'); + $this->get($shelf->getUrl('/delete'))->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']); - $this->visit($shelf->getUrl('/delete')) - ->seePageIs($shelf->getUrl('/delete'))->see('Delete Book'); + $this->get($shelf->getUrl('/delete'))->assertOk()->assertSee('Delete Book'); } public function test_book_create_restriction_override() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookUrl = $book->getUrl(); $this->actingAs($this->viewer) - ->visit($bookUrl) - ->dontSeeInElement('.actions', 'New Page') - ->dontSeeInElement('.actions', 'New Chapter'); + ->get($bookUrl) + ->assertElementNotContains('.actions', 'New Page') + ->assertElementNotContains('.actions', 'New Chapter'); $this->setRestrictionsForTestRoles($book, ['view', 'delete', 'update']); - $this->forceVisit($bookUrl . '/create-chapter') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookUrl . '/create-page') - ->see('You do not have permission')->seePageIs('/'); - $this->visit($bookUrl)->dontSeeInElement('.actions', 'New Page') - ->dontSeeInElement('.actions', 'New Chapter'); + $this->get($bookUrl . '/create-chapter')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookUrl . '/create-page')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookUrl)->assertElementNotContains('.actions', 'New Page') + ->assertElementNotContains('.actions', 'New Chapter'); $this->setRestrictionsForTestRoles($book, ['view', 'create']); - $this->visit($bookUrl . '/create-chapter') - ->type('test chapter', 'name') - ->type('test description for chapter', 'description') - ->press('Save Chapter') - ->seePageIs($bookUrl . '/chapter/test-chapter'); - $this->visit($bookUrl . '/create-page') - ->type('test page', 'name') - ->type('test content', 'html') - ->press('Save Page') - ->seePageIs($bookUrl . '/page/test-page'); - $this->visit($bookUrl)->seeInElement('.actions', 'New Page') - ->seeInElement('.actions', 'New Chapter'); + $resp = $this->post($book->getUrl('/create-chapter'), [ + 'name' => 'test chapter', + 'description' => 'test desc', + ]); + $resp->assertRedirect($book->getUrl('/chapter/test-chapter')); + + $this->get($book->getUrl('/create-page')); + /** @var Page $page */ + $page = Page::query()->where('draft', '=', true)->orderByDesc('id')->first(); + $resp = $this->post($page->getUrl(), [ + 'name' => 'test page', + 'html' => 'test desc', + ]); + $resp->assertRedirect($book->getUrl('/page/test-page')); + + $this->get($bookUrl) + ->assertElementContains('.actions', 'New Page') + ->assertElementContains('.actions', 'New Chapter'); } public function test_book_update_restriction_override() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); $bookUrl = $book->getUrl(); - $this->actingAs($this->viewer) - ->visit($bookUrl . '/edit') - ->dontSee('Edit Book'); + $this->actingAs($this->viewer)->get($bookUrl . '/edit') + ->assertDontSee('Edit Book'); $this->setRestrictionsForTestRoles($book, ['view', 'delete']); - $this->forceVisit($bookUrl . '/edit') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookPage->getUrl() . '/edit') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookChapter->getUrl() . '/edit') - ->see('You do not have permission')->seePageIs('/'); + $this->get($bookUrl . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookPage->getUrl() . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookChapter->getUrl() . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($book, ['view', 'update']); - $this->visit($bookUrl . '/edit') - ->seePageIs($bookUrl . '/edit'); - $this->visit($bookPage->getUrl() . '/edit') - ->seePageIs($bookPage->getUrl() . '/edit'); - $this->visit($bookChapter->getUrl() . '/edit') - ->see('Edit Chapter'); + $this->get($bookUrl . '/edit')->assertOk(); + $this->get($bookPage->getUrl() . '/edit')->assertOk(); + $this->get($bookChapter->getUrl() . '/edit')->assertSee('Edit Chapter'); } public function test_book_delete_restriction_override() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); $bookUrl = $book->getUrl(); $this->actingAs($this->viewer) - ->visit($bookUrl . '/delete') - ->dontSee('Delete Book'); + ->get($bookUrl . '/delete') + ->assertDontSee('Delete Book'); $this->setRestrictionsForTestRoles($book, ['view', 'update']); - $this->forceVisit($bookUrl . '/delete') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookPage->getUrl() . '/delete') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookChapter->getUrl() . '/delete') - ->see('You do not have permission')->seePageIs('/'); + $this->get($bookUrl . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookPage->getUrl() . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookChapter->getUrl() . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($book, ['view', 'delete']); - $this->visit($bookUrl . '/delete') - ->seePageIs($bookUrl . '/delete')->see('Delete Book'); - $this->visit($bookPage->getUrl() . '/delete') - ->seePageIs($bookPage->getUrl() . '/delete')->see('Delete Page'); - $this->visit($bookChapter->getUrl() . '/delete') - ->see('Delete Chapter'); + $this->get($bookUrl . '/delete')->assertOk()->assertSee('Delete Book'); + $this->get($bookPage->getUrl() . '/delete')->assertOk()->assertSee('Delete Page'); + $this->get($bookChapter->getUrl() . '/delete')->assertSee('Delete Chapter'); } public function test_page_visible_if_has_permissions_when_book_not_visible() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookChapter = $book->chapters->first(); $bookPage = $bookChapter->pages->first(); @@ -655,34 +645,37 @@ class EntityPermissionsTest extends BrowserKitTest $this->setRestrictionsForTestRoles($bookPage, ['view']); $this->actingAs($this->viewer); - $this->get($bookPage->getUrl()); - $this->assertResponseOk(); - $this->see($bookPage->name); - $this->dontSee(substr($book->name, 0, 15)); - $this->dontSee(substr($bookChapter->name, 0, 15)); + $resp = $this->get($bookPage->getUrl()); + $resp->assertOk(); + $resp->assertSee($bookPage->name); + $resp->assertDontSee(substr($book->name, 0, 15)); + $resp->assertDontSee(substr($bookChapter->name, 0, 15)); } public function test_book_sort_view_permission() { - $firstBook = Book::first(); - $secondBook = Book::find(2); + /** @var Book $firstBook */ + $firstBook = Book::query()->first(); + /** @var Book $secondBook */ + $secondBook = Book::query()->find(2); $this->setRestrictionsForTestRoles($firstBook, ['view', 'update']); $this->setRestrictionsForTestRoles($secondBook, ['view']); // Test sort page visibility - $this->actingAs($this->user)->visit($secondBook->getUrl() . '/sort') - ->see('You do not have permission') - ->seePageIs('/'); + $this->actingAs($this->user)->get($secondBook->getUrl('/sort'))->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); // Check sort page on first book - $this->actingAs($this->user)->visit($firstBook->getUrl() . '/sort'); + $this->actingAs($this->user)->get($firstBook->getUrl('/sort')); } public function test_book_sort_permission() { - $firstBook = Book::first(); - $secondBook = Book::find(2); + /** @var Book $firstBook */ + $firstBook = Book::query()->first(); + /** @var Book $secondBook */ + $secondBook = Book::query()->find(2); $this->setRestrictionsForTestRoles($firstBook, ['view', 'update']); $this->setRestrictionsForTestRoles($secondBook, ['view']); @@ -703,9 +696,8 @@ class EntityPermissionsTest extends BrowserKitTest // Move chapter from first book to a second book $this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)]) - ->followRedirects() - ->see('You do not have permission') - ->seePageIs('/'); + ->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $reqData = [ [ @@ -719,30 +711,30 @@ class EntityPermissionsTest extends BrowserKitTest // Move chapter from second book to first book $this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)]) - ->followRedirects() - ->see('You do not have permission') - ->seePageIs('/'); + ->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); } public function test_can_create_page_if_chapter_has_permissions_when_book_not_visible() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $this->setRestrictionsForTestRoles($book, []); $bookChapter = $book->chapters->first(); $this->setRestrictionsForTestRoles($bookChapter, ['view']); - $this->actingAs($this->user)->visit($bookChapter->getUrl()) - ->dontSee('New Page'); + $this->actingAs($this->user)->get($bookChapter->getUrl()) + ->assertDontSee('New Page'); $this->setRestrictionsForTestRoles($bookChapter, ['view', 'create']); - $this->actingAs($this->user)->visit($bookChapter->getUrl()) - ->click('New Page') - ->seeStatusCode(200) - ->type('test page', 'name') - ->type('test content', 'html') - ->press('Save Page') - ->seePageIs($book->getUrl('/page/test-page')) - ->seeStatusCode(200); + $this->get($bookChapter->getUrl('/create-page')); + /** @var Page $page */ + $page = Page::query()->where('draft', '=', true)->orderByDesc('id')->first(); + $resp = $this->post($page->getUrl(), [ + 'name' => 'test page', + 'html' => 'test content', + ]); + $resp->assertRedirect($book->getUrl('/page/test-page')); } } diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php index b9b1805b6..5248ae152 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolesTest.php @@ -2,18 +2,20 @@ namespace Tests\Permissions; +use BookStack\Actions\ActivityType; use BookStack\Actions\Comment; use BookStack\Auth\Role; use BookStack\Auth\User; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; +use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use BookStack\Uploads\Image; -use Laravel\BrowserKitTesting\HttpException; -use Tests\BrowserKitTest; +use Tests\TestCase; +use Tests\TestResponse; -class RolesTest extends BrowserKitTest +class RolesTest extends TestCase { protected $user; @@ -25,17 +27,17 @@ class RolesTest extends BrowserKitTest public function test_admin_can_see_settings() { - $this->asAdmin()->visit('/settings')->see('Settings'); + $this->asAdmin()->get('/settings')->assertSee('Settings'); } public function test_cannot_delete_admin_role() { $adminRole = Role::getRole('admin'); $deletePageUrl = '/settings/roles/delete/' . $adminRole->id; - $this->asAdmin()->visit($deletePageUrl) - ->press('Confirm') - ->seePageIs($deletePageUrl) - ->see('cannot be deleted'); + + $this->asAdmin()->get($deletePageUrl); + $this->delete($deletePageUrl)->assertRedirect($deletePageUrl); + $this->get($deletePageUrl)->assertSee('cannot be deleted'); } public function test_role_cannot_be_deleted_if_default() @@ -44,10 +46,9 @@ class RolesTest extends BrowserKitTest $this->setSettings(['registration-role' => $newRole->id]); $deletePageUrl = '/settings/roles/delete/' . $newRole->id; - $this->asAdmin()->visit($deletePageUrl) - ->press('Confirm') - ->seePageIs($deletePageUrl) - ->see('cannot be deleted'); + $this->asAdmin()->get($deletePageUrl); + $this->delete($deletePageUrl)->assertRedirect($deletePageUrl); + $this->get($deletePageUrl)->assertSee('cannot be deleted'); } public function test_role_create_update_delete_flow() @@ -57,68 +58,104 @@ class RolesTest extends BrowserKitTest $testRoleUpdateName = 'An Super Updated role'; // Creation - $this->asAdmin()->visit('/settings') - ->click('Roles') - ->seePageIs('/settings/roles') - ->click('Create New Role') - ->type('Test Role', 'display_name') - ->type('A little test description', 'description') - ->press('Save Role') - ->seeInDatabase('roles', ['display_name' => $testRoleName, 'description' => $testRoleDesc, 'mfa_enforced' => false]) - ->seePageIs('/settings/roles'); + $resp = $this->asAdmin()->get('/settings'); + $resp->assertElementContains('a[href="' . url('/settings/roles') . '"]', 'Roles'); + + $resp = $this->get('/settings/roles'); + $resp->assertElementContains('a[href="' . url('/settings/roles/new') . '"]', 'Create New Role'); + + $resp = $this->get('/settings/roles/new'); + $resp->assertElementContains('form[action="' . url('/settings/roles/new') . '"]', 'Save Role'); + + $resp = $this->post('/settings/roles/new', [ + 'display_name' => $testRoleName, + 'description' => $testRoleDesc, + ]); + $resp->assertRedirect('/settings/roles'); + + $resp = $this->get('/settings/roles'); + $resp->assertSee($testRoleName); + $resp->assertSee($testRoleDesc); + $this->assertDatabaseHas('roles', [ + 'display_name' => $testRoleName, + 'description' => $testRoleDesc, + 'mfa_enforced' => false, + ]); + + /** @var Role $role */ + $role = Role::query()->where('display_name', '=', $testRoleName)->first(); + // Updating - $this->asAdmin()->visit('/settings/roles') - ->see($testRoleDesc) - ->click($testRoleName) - ->type($testRoleUpdateName, '#display_name') - ->check('#mfa_enforced') - ->press('Save Role') - ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'description' => $testRoleDesc, 'mfa_enforced' => true]) - ->seePageIs('/settings/roles'); + $resp = $this->get('/settings/roles/' . $role->id); + $resp->assertSee($testRoleName); + $resp->assertSee($testRoleDesc); + $resp->assertElementContains('form[action="' . url('/settings/roles/' . $role->id) . '"]', 'Save Role'); + + $resp = $this->put('/settings/roles/' . $role->id, [ + 'display_name' => $testRoleUpdateName, + 'description' => $testRoleDesc, + 'mfa_enforced' => 'true', + ]); + $resp->assertRedirect('/settings/roles'); + $this->assertDatabaseHas('roles', [ + 'display_name' => $testRoleUpdateName, + 'description' => $testRoleDesc, + 'mfa_enforced' => true, + ]); + // Deleting - $this->asAdmin()->visit('/settings/roles') - ->click($testRoleUpdateName) - ->click('Delete Role') - ->see($testRoleUpdateName) - ->press('Confirm') - ->seePageIs('/settings/roles') - ->dontSee($testRoleUpdateName); + $resp = $this->get('/settings/roles/' . $role->id); + $resp->assertElementContains('a[href="' . url("/settings/roles/delete/$role->id") . '"]', 'Delete Role'); + + $resp = $this->get("/settings/roles/delete/$role->id"); + $resp->assertSee($testRoleUpdateName); + $resp->assertElementContains('form[action="' . url("/settings/roles/delete/$role->id") . '"]', 'Confirm'); + + $resp = $this->delete("/settings/roles/delete/$role->id"); + $resp->assertRedirect('/settings/roles'); + $this->get('/settings/roles')->assertSee('Role successfully deleted'); + $this->assertActivityExists(ActivityType::ROLE_DELETE); } - public function test_admin_role_cannot_be_removed_if_last_admin() + public function test_admin_role_cannot_be_removed_if_user_last_admin() { - $adminRole = Role::where('system_name', '=', 'admin')->first(); + /** @var Role $adminRole */ + $adminRole = Role::query()->where('system_name', '=', 'admin')->first(); $adminUser = $this->getAdmin(); $adminRole->users()->where('id', '!=', $adminUser->id)->delete(); - $this->assertEquals($adminRole->users()->count(), 1); + $this->assertEquals(1, $adminRole->users()->count()); $viewerRole = $this->getViewer()->roles()->first(); $editUrl = '/settings/users/' . $adminUser->id; - $this->actingAs($adminUser)->put($editUrl, [ + $resp = $this->actingAs($adminUser)->put($editUrl, [ 'name' => $adminUser->name, 'email' => $adminUser->email, 'roles' => [ 'viewer' => strval($viewerRole->id), ], - ])->followRedirects(); + ]); - $this->seePageIs($editUrl); - $this->see('This user is the only user assigned to the administrator role'); + $resp->assertRedirect($editUrl); + + $resp = $this->get($editUrl); + $resp->assertSee('This user is the only user assigned to the administrator role'); } public function test_migrate_users_on_delete_works() { + /** @var Role $roleA */ $roleA = Role::query()->create(['display_name' => 'Delete Test A']); + /** @var Role $roleB */ $roleB = Role::query()->create(['display_name' => 'Delete Test B']); $this->user->attachRole($roleB); $this->assertCount(0, $roleA->users()->get()); $this->assertCount(1, $roleB->users()->get()); - $deletePage = $this->asAdmin()->get("/settings/roles/delete/{$roleB->id}"); - $deletePage->seeElement('select[name=migrate_role_id]'); - $this->asAdmin()->delete("/settings/roles/delete/{$roleB->id}", [ + $deletePage = $this->asAdmin()->get("/settings/roles/delete/$roleB->id"); + $deletePage->assertElementExists('select[name=migrate_role_id]'); + $this->asAdmin()->delete("/settings/roles/delete/$roleB->id", [ 'migrate_role_id' => $roleA->id, ]); @@ -128,21 +165,19 @@ class RolesTest extends BrowserKitTest public function test_manage_user_permission() { - $this->actingAs($this->user)->visit('/settings/users') - ->seePageIs('/'); + $this->actingAs($this->user)->get('/settings/users')->assertRedirect('/'); $this->giveUserPermissions($this->user, ['users-manage']); - $this->actingAs($this->user)->visit('/settings/users') - ->seePageIs('/settings/users'); + $this->actingAs($this->user)->get('/settings/users')->assertOk(); } public function test_manage_users_permission_shows_link_in_header_if_does_not_have_settings_manage_permision() { $usersLink = 'href="' . url('/settings/users') . '"'; - $this->actingAs($this->user)->visit('/')->dontSee($usersLink); + $this->actingAs($this->user)->get('/')->assertDontSee($usersLink); $this->giveUserPermissions($this->user, ['users-manage']); - $this->actingAs($this->user)->visit('/')->see($usersLink); + $this->actingAs($this->user)->get('/')->assertSee($usersLink); $this->giveUserPermissions($this->user, ['settings-manage', 'users-manage']); - $this->actingAs($this->user)->visit('/')->dontSee($usersLink); + $this->actingAs($this->user)->get('/')->assertDontSee($usersLink); } public function test_user_cannot_change_email_unless_they_have_manage_users_permission() @@ -151,14 +186,14 @@ class RolesTest extends BrowserKitTest $originalEmail = $this->user->email; $this->actingAs($this->user); - $this->visit($userProfileUrl) - ->assertResponseOk() - ->seeElement('input[name=email][disabled]'); + $this->get($userProfileUrl) + ->assertOk() + ->assertElementExists('input[name=email][disabled]'); $this->put($userProfileUrl, [ 'name' => 'my_new_name', 'email' => 'new_email@example.com', ]); - $this->seeInDatabase('users', [ + $this->assertDatabaseHas('users', [ 'id' => $this->user->id, 'email' => $originalEmail, 'name' => 'my_new_name', @@ -166,16 +201,16 @@ class RolesTest extends BrowserKitTest $this->giveUserPermissions($this->user, ['users-manage']); - $this->visit($userProfileUrl) - ->assertResponseOk() - ->dontSeeElement('input[name=email][disabled]') - ->seeElement('input[name=email]'); + $this->get($userProfileUrl) + ->assertOk() + ->assertElementNotExists('input[name=email][disabled]') + ->assertElementExists('input[name=email]'); $this->put($userProfileUrl, [ 'name' => 'my_new_name_2', 'email' => 'new_email@example.com', ]); - $this->seeInDatabase('users', [ + $this->assertDatabaseHas('users', [ 'id' => $this->user->id, 'email' => 'new_email@example.com', 'name' => 'my_new_name_2', @@ -184,40 +219,47 @@ class RolesTest extends BrowserKitTest public function test_user_roles_manage_permission() { - $this->actingAs($this->user)->visit('/settings/roles') - ->seePageIs('/')->visit('/settings/roles/1')->seePageIs('/'); + $this->actingAs($this->user)->get('/settings/roles')->assertRedirect('/'); + $this->get('/settings/roles/1')->assertRedirect('/'); $this->giveUserPermissions($this->user, ['user-roles-manage']); - $this->actingAs($this->user)->visit('/settings/roles') - ->seePageIs('/settings/roles')->click('Admin') - ->see('Edit Role'); + $this->actingAs($this->user)->get('/settings/roles')->assertOk(); + $this->get('/settings/roles/1') + ->assertOk() + ->assertSee('Admin'); } public function test_settings_manage_permission() { - $this->actingAs($this->user)->visit('/settings') - ->seePageIs('/'); + $this->actingAs($this->user)->get('/settings')->assertRedirect('/'); $this->giveUserPermissions($this->user, ['settings-manage']); - $this->actingAs($this->user)->visit('/settings') - ->seePageIs('/settings')->press('Save Settings')->see('Settings Saved'); + $this->get('/settings')->assertOk(); + + $resp = $this->post('/settings', []); + $resp->assertRedirect('/settings'); + $resp = $this->get('/settings'); + $resp->assertSee('Settings saved'); } public function test_restrictions_manage_all_permission() { - $page = Page::take(1)->get()->first(); - $this->actingAs($this->user)->visit($page->getUrl()) - ->dontSee('Permissions') - ->visit($page->getUrl() . '/permissions') - ->seePageIs('/'); + $page = Page::query()->get()->first(); + + $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions'); + $this->get($page->getUrl('/permissions'))->assertRedirect('/'); + $this->giveUserPermissions($this->user, ['restrictions-manage-all']); - $this->actingAs($this->user)->visit($page->getUrl()) - ->see('Permissions') - ->click('Permissions') - ->see('Page Permissions')->seePageIs($page->getUrl() . '/permissions'); + + $this->actingAs($this->user)->get($page->getUrl())->assertSee('Permissions'); + + $this->get($page->getUrl('/permissions')) + ->assertOk() + ->assertSee('Page Permissions'); } public function test_restrictions_manage_own_permission() { - $otherUsersPage = Page::first(); + /** @var Page $otherUsersPage */ + $otherUsersPage = Page::query()->first(); $content = $this->createEntityChainBelongingToUser($this->user); // Set a different creator on the page we're checking to ensure @@ -228,57 +270,45 @@ class RolesTest extends BrowserKitTest $page->save(); // Check can't restrict other's content - $this->actingAs($this->user)->visit($otherUsersPage->getUrl()) - ->dontSee('Permissions') - ->visit($otherUsersPage->getUrl() . '/permissions') - ->seePageIs('/'); + $this->actingAs($this->user)->get($otherUsersPage->getUrl())->assertDontSee('Permissions'); + $this->get($otherUsersPage->getUrl('/permissions'))->assertRedirect('/'); + // Check can't restrict own content - $this->actingAs($this->user)->visit($page->getUrl()) - ->dontSee('Permissions') - ->visit($page->getUrl() . '/permissions') - ->seePageIs('/'); + $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions'); + $this->get($page->getUrl('/permissions'))->assertRedirect('/'); $this->giveUserPermissions($this->user, ['restrictions-manage-own']); // Check can't restrict other's content - $this->actingAs($this->user)->visit($otherUsersPage->getUrl()) - ->dontSee('Permissions') - ->visit($otherUsersPage->getUrl() . '/permissions') - ->seePageIs('/'); + $this->actingAs($this->user)->get($otherUsersPage->getUrl())->assertDontSee('Permissions'); + $this->get($otherUsersPage->getUrl('/permissions'))->assertRedirect(); + // Check can restrict own content - $this->actingAs($this->user)->visit($page->getUrl()) - ->see('Permissions') - ->click('Permissions') - ->seePageIs($page->getUrl() . '/permissions'); + $this->actingAs($this->user)->get($page->getUrl())->assertSee('Permissions'); + $this->get($page->getUrl('/permissions'))->assertOk(); } /** * Check a standard entity access permission. - * - * @param string $permission - * @param array $accessUrls Urls that are only accessible after having the permission - * @param array $visibles Check this text, In the buttons toolbar, is only visible with the permission */ - private function checkAccessPermission($permission, $accessUrls = [], $visibles = []) + private function checkAccessPermission(string $permission, array $accessUrls = [], array $visibles = []) { foreach ($accessUrls as $url) { - $this->actingAs($this->user)->visit($url) - ->seePageIs('/'); + $this->actingAs($this->user)->get($url)->assertRedirect('/'); } + foreach ($visibles as $url => $text) { - $this->actingAs($this->user)->visit($url) - ->dontSeeInElement('.action-buttons', $text); + $this->actingAs($this->user)->get($url) + ->assertElementNotContains('.action-buttons', $text); } $this->giveUserPermissions($this->user, [$permission]); foreach ($accessUrls as $url) { - $this->actingAs($this->user)->visit($url) - ->seePageIs($url); + $this->actingAs($this->user)->get($url)->assertOk(); } foreach ($visibles as $url => $text) { - $this->actingAs($this->user)->visit($url) - ->see($text); + $this->actingAs($this->user)->get($url)->assertSee($text); } } @@ -290,16 +320,16 @@ class RolesTest extends BrowserKitTest '/shelves' => 'New Shelf', ]); - $this->visit('/create-shelf') - ->type('test shelf', 'name') - ->type('shelf desc', 'description') - ->press('Save Shelf') - ->seePageIs('/shelves/test-shelf'); + $this->post('/shelves', [ + 'name' => 'test shelf', + 'description' => 'shelf desc', + ])->assertRedirect('/shelves/test-shelf'); } public function test_bookshelves_edit_own_permission() { - $otherShelf = Bookshelf::first(); + /** @var Bookshelf $otherShelf */ + $otherShelf = Bookshelf::query()->first(); $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); $this->regenEntityPermissions($ownShelf); @@ -310,15 +340,14 @@ class RolesTest extends BrowserKitTest $ownShelf->getUrl() => 'Edit', ]); - $this->visit($otherShelf->getUrl()) - ->dontSeeInElement('.action-buttons', 'Edit') - ->visit($otherShelf->getUrl('/edit')) - ->seePageIs('/'); + $this->get($otherShelf->getUrl())->assertElementNotContains('.action-buttons', 'Edit'); + $this->get($otherShelf->getUrl('/edit'))->assertRedirect('/'); } public function test_bookshelves_edit_all_permission() { - $otherShelf = Bookshelf::first(); + /** @var Bookshelf $otherShelf */ + $otherShelf = Bookshelf::query()->first(); $this->checkAccessPermission('bookshelf-update-all', [ $otherShelf->getUrl('/edit'), ], [ @@ -329,7 +358,8 @@ class RolesTest extends BrowserKitTest public function test_bookshelves_delete_own_permission() { $this->giveUserPermissions($this->user, ['bookshelf-update-all']); - $otherShelf = Bookshelf::first(); + /** @var Bookshelf $otherShelf */ + $otherShelf = Bookshelf::query()->first(); $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); $this->regenEntityPermissions($ownShelf); @@ -340,30 +370,27 @@ class RolesTest extends BrowserKitTest $ownShelf->getUrl() => 'Delete', ]); - $this->visit($otherShelf->getUrl()) - ->dontSeeInElement('.action-buttons', 'Delete') - ->visit($otherShelf->getUrl('/delete')) - ->seePageIs('/'); - $this->visit($ownShelf->getUrl())->visit($ownShelf->getUrl('/delete')) - ->press('Confirm') - ->seePageIs('/shelves') - ->dontSee($ownShelf->name); + $this->get($otherShelf->getUrl())->assertElementNotContains('.action-buttons', 'Delete'); + $this->get($otherShelf->getUrl('/delete'))->assertRedirect('/'); + + $this->get($ownShelf->getUrl()); + $this->delete($ownShelf->getUrl())->assertRedirect('/shelves'); + $this->get('/shelves')->assertDontSee($ownShelf->name); } public function test_bookshelves_delete_all_permission() { $this->giveUserPermissions($this->user, ['bookshelf-update-all']); - $otherShelf = Bookshelf::first(); + /** @var Bookshelf $otherShelf */ + $otherShelf = Bookshelf::query()->first(); $this->checkAccessPermission('bookshelf-delete-all', [ $otherShelf->getUrl('/delete'), ], [ $otherShelf->getUrl() => 'Delete', ]); - $this->visit($otherShelf->getUrl())->visit($otherShelf->getUrl('/delete')) - ->press('Confirm') - ->seePageIs('/shelves') - ->dontSee($otherShelf->name); + $this->delete($otherShelf->getUrl())->assertRedirect('/shelves'); + $this->get('/shelves')->assertDontSee($otherShelf->name); } public function test_books_create_all_permissions() @@ -374,16 +401,16 @@ class RolesTest extends BrowserKitTest '/books' => 'Create New Book', ]); - $this->visit('/create-book') - ->type('test book', 'name') - ->type('book desc', 'description') - ->press('Save Book') - ->seePageIs('/books/test-book'); + $this->post('/books', [ + 'name' => 'test book', + 'description' => 'book desc', + ])->assertRedirect('/books/test-book'); } public function test_books_edit_own_permission() { - $otherBook = Book::take(1)->get()->first(); + /** @var Book $otherBook */ + $otherBook = Book::query()->take(1)->get()->first(); $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('book-update-own', [ $ownBook->getUrl() . '/edit', @@ -391,15 +418,14 @@ class RolesTest extends BrowserKitTest $ownBook->getUrl() => 'Edit', ]); - $this->visit($otherBook->getUrl()) - ->dontSeeInElement('.action-buttons', 'Edit') - ->visit($otherBook->getUrl() . '/edit') - ->seePageIs('/'); + $this->get($otherBook->getUrl())->assertElementNotContains('.action-buttons', 'Edit'); + $this->get($otherBook->getUrl('/edit'))->assertRedirect('/'); } public function test_books_edit_all_permission() { - $otherBook = Book::take(1)->get()->first(); + /** @var Book $otherBook */ + $otherBook = Book::query()->take(1)->get()->first(); $this->checkAccessPermission('book-update-all', [ $otherBook->getUrl() . '/edit', ], [ @@ -410,7 +436,8 @@ class RolesTest extends BrowserKitTest public function test_books_delete_own_permission() { $this->giveUserPermissions($this->user, ['book-update-all']); - $otherBook = Book::take(1)->get()->first(); + /** @var Book $otherBook */ + $otherBook = Book::query()->take(1)->get()->first(); $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('book-delete-own', [ $ownBook->getUrl() . '/delete', @@ -418,35 +445,33 @@ class RolesTest extends BrowserKitTest $ownBook->getUrl() => 'Delete', ]); - $this->visit($otherBook->getUrl()) - ->dontSeeInElement('.action-buttons', 'Delete') - ->visit($otherBook->getUrl() . '/delete') - ->seePageIs('/'); - $this->visit($ownBook->getUrl())->visit($ownBook->getUrl() . '/delete') - ->press('Confirm') - ->seePageIs('/books') - ->dontSee($ownBook->name); + $this->get($otherBook->getUrl())->assertElementNotContains('.action-buttons', 'Delete'); + $this->get($otherBook->getUrl('/delete'))->assertRedirect('/'); + $this->get($ownBook->getUrl()); + $this->delete($ownBook->getUrl())->assertRedirect('/books'); + $this->get('/books')->assertDontSee($ownBook->name); } public function test_books_delete_all_permission() { $this->giveUserPermissions($this->user, ['book-update-all']); - $otherBook = Book::take(1)->get()->first(); + /** @var Book $otherBook */ + $otherBook = Book::query()->take(1)->get()->first(); $this->checkAccessPermission('book-delete-all', [ $otherBook->getUrl() . '/delete', ], [ $otherBook->getUrl() => 'Delete', ]); - $this->visit($otherBook->getUrl())->visit($otherBook->getUrl() . '/delete') - ->press('Confirm') - ->seePageIs('/books') - ->dontSee($otherBook->name); + $this->get($otherBook->getUrl()); + $this->delete($otherBook->getUrl())->assertRedirect('/books'); + $this->get('/books')->assertDontSee($otherBook->name); } public function test_chapter_create_own_permissions() { - $book = Book::take(1)->get()->first(); + /** @var Book $book */ + $book = Book::query()->take(1)->get()->first(); $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('chapter-create-own', [ $ownBook->getUrl('/create-chapter'), @@ -454,37 +479,35 @@ class RolesTest extends BrowserKitTest $ownBook->getUrl() => 'New Chapter', ]); - $this->visit($ownBook->getUrl('/create-chapter')) - ->type('test chapter', 'name') - ->type('chapter desc', 'description') - ->press('Save Chapter') - ->seePageIs($ownBook->getUrl('/chapter/test-chapter')); + $this->post($ownBook->getUrl('/create-chapter'), [ + 'name' => 'test chapter', + 'description' => 'chapter desc', + ])->assertRedirect($ownBook->getUrl('/chapter/test-chapter')); - $this->visit($book->getUrl()) - ->dontSeeInElement('.action-buttons', 'New Chapter') - ->visit($book->getUrl('/create-chapter')) - ->seePageIs('/'); + $this->get($book->getUrl())->assertElementNotContains('.action-buttons', 'New Chapter'); + $this->get($book->getUrl('/create-chapter'))->assertRedirect('/'); } public function test_chapter_create_all_permissions() { - $book = Book::take(1)->get()->first(); + /** @var Book $book */ + $book = Book::query()->first(); $this->checkAccessPermission('chapter-create-all', [ $book->getUrl('/create-chapter'), ], [ $book->getUrl() => 'New Chapter', ]); - $this->visit($book->getUrl('/create-chapter')) - ->type('test chapter', 'name') - ->type('chapter desc', 'description') - ->press('Save Chapter') - ->seePageIs($book->getUrl('/chapter/test-chapter')); + $this->post($book->getUrl('/create-chapter'), [ + 'name' => 'test chapter', + 'description' => 'chapter desc', + ])->assertRedirect($book->getUrl('/chapter/test-chapter')); } public function test_chapter_edit_own_permission() { - $otherChapter = Chapter::take(1)->get()->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->first(); $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter']; $this->checkAccessPermission('chapter-update-own', [ $ownChapter->getUrl() . '/edit', @@ -492,15 +515,14 @@ class RolesTest extends BrowserKitTest $ownChapter->getUrl() => 'Edit', ]); - $this->visit($otherChapter->getUrl()) - ->dontSeeInElement('.action-buttons', 'Edit') - ->visit($otherChapter->getUrl() . '/edit') - ->seePageIs('/'); + $this->get($otherChapter->getUrl())->assertElementNotContains('.action-buttons', 'Edit'); + $this->get($otherChapter->getUrl('/edit'))->assertRedirect('/'); } public function test_chapter_edit_all_permission() { - $otherChapter = Chapter::take(1)->get()->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->take(1)->get()->first(); $this->checkAccessPermission('chapter-update-all', [ $otherChapter->getUrl() . '/edit', ], [ @@ -511,7 +533,8 @@ class RolesTest extends BrowserKitTest public function test_chapter_delete_own_permission() { $this->giveUserPermissions($this->user, ['chapter-update-all']); - $otherChapter = Chapter::take(1)->get()->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->first(); $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter']; $this->checkAccessPermission('chapter-delete-own', [ $ownChapter->getUrl() . '/delete', @@ -520,20 +543,18 @@ class RolesTest extends BrowserKitTest ]); $bookUrl = $ownChapter->book->getUrl(); - $this->visit($otherChapter->getUrl()) - ->dontSeeInElement('.action-buttons', 'Delete') - ->visit($otherChapter->getUrl() . '/delete') - ->seePageIs('/'); - $this->visit($ownChapter->getUrl())->visit($ownChapter->getUrl() . '/delete') - ->press('Confirm') - ->seePageIs($bookUrl) - ->dontSeeInElement('.book-content', $ownChapter->name); + $this->get($otherChapter->getUrl())->assertElementNotContains('.action-buttons', 'Delete'); + $this->get($otherChapter->getUrl('/delete'))->assertRedirect('/'); + $this->get($ownChapter->getUrl()); + $this->delete($ownChapter->getUrl())->assertRedirect($bookUrl); + $this->get($bookUrl)->assertElementNotContains('.book-content', $ownChapter->name); } public function test_chapter_delete_all_permission() { $this->giveUserPermissions($this->user, ['chapter-update-all']); - $otherChapter = Chapter::take(1)->get()->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->first(); $this->checkAccessPermission('chapter-delete-all', [ $otherChapter->getUrl() . '/delete', ], [ @@ -541,16 +562,17 @@ class RolesTest extends BrowserKitTest ]); $bookUrl = $otherChapter->book->getUrl(); - $this->visit($otherChapter->getUrl())->visit($otherChapter->getUrl() . '/delete') - ->press('Confirm') - ->seePageIs($bookUrl) - ->dontSeeInElement('.book-content', $otherChapter->name); + $this->get($otherChapter->getUrl()); + $this->delete($otherChapter->getUrl())->assertRedirect($bookUrl); + $this->get($bookUrl)->assertElementNotContains('.book-content', $otherChapter->name); } public function test_page_create_own_permissions() { - $book = Book::first(); - $chapter = Chapter::first(); + /** @var Book $book */ + $book = Book::query()->first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $entities = $this->createEntityChainBelongingToUser($this->user); $ownBook = $entities['book']; @@ -561,8 +583,7 @@ class RolesTest extends BrowserKitTest $accessUrls = [$createUrl, $createUrlChapter]; foreach ($accessUrls as $url) { - $this->actingAs($this->user)->visit($url) - ->seePageIs('/'); + $this->actingAs($this->user)->get($url)->assertRedirect('/'); } $this->checkAccessPermission('page-create-own', [], [ @@ -573,40 +594,39 @@ class RolesTest extends BrowserKitTest $this->giveUserPermissions($this->user, ['page-create-own']); foreach ($accessUrls as $index => $url) { - $this->actingAs($this->user)->visit($url); - $expectedUrl = Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); - $this->seePageIs($expectedUrl); + $resp = $this->actingAs($this->user)->get($url); + $expectedUrl = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); + $resp->assertRedirect($expectedUrl); } - $this->visit($createUrl) - ->type('test page', 'name') - ->type('page desc', 'html') - ->press('Save Page') - ->seePageIs($ownBook->getUrl('/page/test-page')); + $this->get($createUrl); + /** @var Page $draft */ + $draft = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first(); + $this->post($draft->getUrl(), [ + 'name' => 'test page', + 'html' => 'page desc', + ])->assertRedirect($ownBook->getUrl('/page/test-page')); - $this->visit($book->getUrl()) - ->dontSeeInElement('.action-buttons', 'New Page') - ->visit($book->getUrl() . '/create-page') - ->seePageIs('/'); - $this->visit($chapter->getUrl()) - ->dontSeeInElement('.action-buttons', 'New Page') - ->visit($chapter->getUrl() . '/create-page') - ->seePageIs('/'); + $this->get($book->getUrl())->assertElementNotContains('.action-buttons', 'New Page'); + $this->get($book->getUrl('/create-page'))->assertRedirect('/'); + + $this->get($chapter->getUrl())->assertElementNotContains('.action-buttons', 'New Page'); + $this->get($chapter->getUrl('/create-page'))->assertRedirect('/'); } public function test_page_create_all_permissions() { - $book = Book::take(1)->get()->first(); - $chapter = Chapter::take(1)->get()->first(); - $baseUrl = $book->getUrl() . '/page'; + /** @var Book $book */ + $book = Book::query()->first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $createUrl = $book->getUrl('/create-page'); $createUrlChapter = $chapter->getUrl('/create-page'); $accessUrls = [$createUrl, $createUrlChapter]; foreach ($accessUrls as $url) { - $this->actingAs($this->user)->visit($url) - ->seePageIs('/'); + $this->actingAs($this->user)->get($url)->assertRedirect('/'); } $this->checkAccessPermission('page-create-all', [], [ @@ -617,27 +637,32 @@ class RolesTest extends BrowserKitTest $this->giveUserPermissions($this->user, ['page-create-all']); foreach ($accessUrls as $index => $url) { - $this->actingAs($this->user)->visit($url); - $expectedUrl = Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); - $this->seePageIs($expectedUrl); + $resp = $this->actingAs($this->user)->get($url); + $expectedUrl = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); + $resp->assertRedirect($expectedUrl); } - $this->visit($createUrl) - ->type('test page', 'name') - ->type('page desc', 'html') - ->press('Save Page') - ->seePageIs($book->getUrl('/page/test-page')); + $this->get($createUrl); + /** @var Page $draft */ + $draft = Page::query()->where('draft', '=', true)->orderByDesc('id')->first(); + $this->post($draft->getUrl(), [ + 'name' => 'test page', + 'html' => 'page desc', + ])->assertRedirect($book->getUrl('/page/test-page')); - $this->visit($chapter->getUrl('/create-page')) - ->type('new test page', 'name') - ->type('page desc', 'html') - ->press('Save Page') - ->seePageIs($book->getUrl('/page/new-test-page')); + $this->get($chapter->getUrl('/create-page')); + /** @var Page $draft */ + $draft = Page::query()->where('draft', '=', true)->orderByDesc('id')->first(); + $this->post($draft->getUrl(), [ + 'name' => 'new test page', + 'html' => 'page desc', + ])->assertRedirect($book->getUrl('/page/new-test-page')); } public function test_page_edit_own_permission() { - $otherPage = Page::take(1)->get()->first(); + /** @var Page $otherPage */ + $otherPage = Page::query()->first(); $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->checkAccessPermission('page-update-own', [ $ownPage->getUrl() . '/edit', @@ -645,17 +670,16 @@ class RolesTest extends BrowserKitTest $ownPage->getUrl() => 'Edit', ]); - $this->visit($otherPage->getUrl()) - ->dontSeeInElement('.action-buttons', 'Edit') - ->visit($otherPage->getUrl() . '/edit') - ->seePageIs('/'); + $this->get($otherPage->getUrl())->assertElementNotContains('.action-buttons', 'Edit'); + $this->get($otherPage->getUrl() . '/edit')->assertRedirect('/'); } public function test_page_edit_all_permission() { - $otherPage = Page::take(1)->get()->first(); + /** @var Page $otherPage */ + $otherPage = Page::query()->first(); $this->checkAccessPermission('page-update-all', [ - $otherPage->getUrl() . '/edit', + $otherPage->getUrl('/edit'), ], [ $otherPage->getUrl() => 'Edit', ]); @@ -664,7 +688,8 @@ class RolesTest extends BrowserKitTest public function test_page_delete_own_permission() { $this->giveUserPermissions($this->user, ['page-update-all']); - $otherPage = Page::take(1)->get()->first(); + /** @var Page $otherPage */ + $otherPage = Page::query()->first(); $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->checkAccessPermission('page-delete-own', [ $ownPage->getUrl() . '/delete', @@ -673,122 +698,127 @@ class RolesTest extends BrowserKitTest ]); $parent = $ownPage->chapter ?? $ownPage->book; - $this->visit($otherPage->getUrl()) - ->dontSeeInElement('.action-buttons', 'Delete') - ->visit($otherPage->getUrl() . '/delete') - ->seePageIs('/'); - $this->visit($ownPage->getUrl())->visit($ownPage->getUrl() . '/delete') - ->press('Confirm') - ->seePageIs($parent->getUrl()) - ->dontSeeInElement('.book-content', $ownPage->name); + $this->get($otherPage->getUrl())->assertElementNotContains('.action-buttons', 'Delete'); + $this->get($otherPage->getUrl('/delete'))->assertRedirect('/'); + $this->get($ownPage->getUrl()); + $this->delete($ownPage->getUrl())->assertRedirect($parent->getUrl()); + $this->get($parent->getUrl())->assertElementNotContains('.book-content', $ownPage->name); } public function test_page_delete_all_permission() { $this->giveUserPermissions($this->user, ['page-update-all']); - $otherPage = Page::take(1)->get()->first(); + /** @var Page $otherPage */ + $otherPage = Page::query()->first(); + $this->checkAccessPermission('page-delete-all', [ $otherPage->getUrl() . '/delete', ], [ $otherPage->getUrl() => 'Delete', ]); + /** @var Entity $parent */ $parent = $otherPage->chapter ?? $otherPage->book; - $this->visit($otherPage->getUrl())->visit($otherPage->getUrl() . '/delete') - ->press('Confirm') - ->seePageIs($parent->getUrl()) - ->dontSeeInElement('.book-content', $otherPage->name); + $this->get($otherPage->getUrl()); + + $this->delete($otherPage->getUrl())->assertRedirect($parent->getUrl()); + $this->get($parent->getUrl())->assertDontSee($otherPage->name); } public function test_public_role_visible_in_user_edit_screen() { - $user = User::first(); + /** @var User $user */ + $user = User::query()->first(); $adminRole = Role::getSystemRole('admin'); $publicRole = Role::getSystemRole('public'); - $this->asAdmin()->visit('/settings/users/' . $user->id) - ->seeElement('[name="roles[' . $adminRole->id . ']"]') - ->seeElement('[name="roles[' . $publicRole->id . ']"]'); + $this->asAdmin()->get('/settings/users/' . $user->id) + ->assertElementExists('[name="roles[' . $adminRole->id . ']"]') + ->assertElementExists('[name="roles[' . $publicRole->id . ']"]'); } public function test_public_role_visible_in_role_listing() { - $this->asAdmin()->visit('/settings/roles') - ->see('Admin') - ->see('Public'); + $this->asAdmin()->get('/settings/roles') + ->assertSee('Admin') + ->assertSee('Public'); } public function test_public_role_visible_in_default_role_setting() { - $this->asAdmin()->visit('/settings') - ->seeElement('[data-system-role-name="admin"]') - ->seeElement('[data-system-role-name="public"]'); + $this->asAdmin()->get('/settings') + ->assertElementExists('[data-system-role-name="admin"]') + ->assertElementExists('[data-system-role-name="public"]'); } - public function test_public_role_not_deleteable() + public function test_public_role_not_deletable() { - $this->asAdmin()->visit('/settings/roles') - ->click('Public') - ->see('Edit Role') - ->click('Delete Role') - ->press('Confirm') - ->see('Delete Role') - ->see('Cannot be deleted'); + /** @var Role $publicRole */ + $publicRole = Role::getSystemRole('public'); + $resp = $this->asAdmin()->delete('/settings/roles/delete/' . $publicRole->id); + $resp->assertRedirect('/'); + + $this->get('/settings/roles/delete/' . $publicRole->id); + $resp = $this->delete('/settings/roles/delete/' . $publicRole->id); + $resp->assertRedirect('/settings/roles/delete/' . $publicRole->id); + $resp = $this->get('/settings/roles/delete/' . $publicRole->id); + $resp->assertSee('This role is a system role and cannot be deleted'); } public function test_image_delete_own_permission() { $this->giveUserPermissions($this->user, ['image-update-all']); - $page = Page::first(); - $image = factory(Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $this->user->id, 'updated_by' => $this->user->id]); + /** @var Page $page */ + $page = Page::query()->first(); + $image = factory(Image::class)->create([ + 'uploaded_to' => $page->id, + 'created_by' => $this->user->id, + 'updated_by' => $this->user->id, + ]); - $this->actingAs($this->user)->json('delete', '/images/' . $image->id) - ->seeStatusCode(403); + $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); $this->giveUserPermissions($this->user, ['image-delete-own']); - $this->actingAs($this->user)->json('delete', '/images/' . $image->id) - ->seeStatusCode(200) - ->dontSeeInDatabase('images', ['id' => $image->id]); + $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk(); + $this->assertDatabaseMissing('images', ['id' => $image->id]); } public function test_image_delete_all_permission() { $this->giveUserPermissions($this->user, ['image-update-all']); $admin = $this->getAdmin(); - $page = Page::first(); + /** @var Page $page */ + $page = Page::query()->first(); $image = factory(Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]); - $this->actingAs($this->user)->json('delete', '/images/' . $image->id) - ->seeStatusCode(403); + $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); $this->giveUserPermissions($this->user, ['image-delete-own']); - $this->actingAs($this->user)->json('delete', '/images/' . $image->id) - ->seeStatusCode(403); + $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); $this->giveUserPermissions($this->user, ['image-delete-all']); - $this->actingAs($this->user)->json('delete', '/images/' . $image->id) - ->seeStatusCode(200) - ->dontSeeInDatabase('images', ['id' => $image->id]); + $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk(); + $this->assertDatabaseMissing('images', ['id' => $image->id]); } public function test_role_permission_removal() { // To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a. - $page = Page::first(); + /** @var Page $page */ + $page = Page::query()->first(); $viewerRole = Role::getRole('viewer'); $viewer = $this->getViewer(); - $this->actingAs($viewer)->visit($page->getUrl())->assertResponseStatus(200); + $this->actingAs($viewer)->get($page->getUrl())->assertOk(); $this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [ 'display_name' => $viewerRole->display_name, 'description' => $viewerRole->description, 'permission' => [], - ])->assertResponseStatus(302); + ])->assertStatus(302); - $this->expectException(HttpException::class); - $this->actingAs($viewer)->visit($page->getUrl())->assertResponseStatus(404); + $this->actingAs($viewer)->get($page->getUrl())->assertStatus(404); } public function test_empty_state_actions_not_visible_without_permission() @@ -796,130 +826,120 @@ class RolesTest extends BrowserKitTest $admin = $this->getAdmin(); // Book links $book = factory(Book::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id]); - $this->updateEntityPermissions($book); - $this->actingAs($this->getViewer())->visit($book->getUrl()) - ->dontSee('Create a new page') - ->dontSee('Add a chapter'); + $this->regenEntityPermissions($book); + $this->actingAs($this->getViewer())->get($book->getUrl()) + ->assertDontSee('Create a new page') + ->assertDontSee('Add a chapter'); // Chapter links $chapter = factory(Chapter::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]); - $this->updateEntityPermissions($chapter); - $this->actingAs($this->getViewer())->visit($chapter->getUrl()) - ->dontSee('Create a new page') - ->dontSee('Sort the current book'); + $this->regenEntityPermissions($chapter); + $this->actingAs($this->getViewer())->get($chapter->getUrl()) + ->assertDontSee('Create a new page') + ->assertDontSee('Sort the current book'); } public function test_comment_create_permission() { $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; - $this->actingAs($this->user)->addComment($ownPage); - - $this->assertResponseStatus(403); + $this->actingAs($this->user) + ->addComment($ownPage) + ->assertStatus(403); $this->giveUserPermissions($this->user, ['comment-create-all']); - $this->actingAs($this->user)->addComment($ownPage); - $this->assertResponseStatus(200); + $this->actingAs($this->user) + ->addComment($ownPage) + ->assertOk(); } public function test_comment_update_own_permission() { $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->giveUserPermissions($this->user, ['comment-create-all']); - $commentId = $this->actingAs($this->user)->addComment($ownPage); + $this->actingAs($this->user)->addComment($ownPage); + /** @var Comment $comment */ + $comment = $ownPage->comments()->latest()->first(); // no comment-update-own - $this->actingAs($this->user)->updateComment($commentId); - $this->assertResponseStatus(403); + $this->actingAs($this->user)->updateComment($comment)->assertStatus(403); $this->giveUserPermissions($this->user, ['comment-update-own']); // now has comment-update-own - $this->actingAs($this->user)->updateComment($commentId); - $this->assertResponseStatus(200); + $this->actingAs($this->user)->updateComment($comment)->assertOk(); } public function test_comment_update_all_permission() { + /** @var Page $ownPage */ $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; - $commentId = $this->asAdmin()->addComment($ownPage); + $this->asAdmin()->addComment($ownPage); + /** @var Comment $comment */ + $comment = $ownPage->comments()->latest()->first(); // no comment-update-all - $this->actingAs($this->user)->updateComment($commentId); - $this->assertResponseStatus(403); + $this->actingAs($this->user)->updateComment($comment)->assertStatus(403); $this->giveUserPermissions($this->user, ['comment-update-all']); // now has comment-update-all - $this->actingAs($this->user)->updateComment($commentId); - $this->assertResponseStatus(200); + $this->actingAs($this->user)->updateComment($comment)->assertOk(); } public function test_comment_delete_own_permission() { + /** @var Page $ownPage */ $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->giveUserPermissions($this->user, ['comment-create-all']); - $commentId = $this->actingAs($this->user)->addComment($ownPage); + $this->actingAs($this->user)->addComment($ownPage); + + /** @var Comment $comment */ + $comment = $ownPage->comments()->latest()->first(); // no comment-delete-own - $this->actingAs($this->user)->deleteComment($commentId); - $this->assertResponseStatus(403); + $this->actingAs($this->user)->deleteComment($comment)->assertStatus(403); $this->giveUserPermissions($this->user, ['comment-delete-own']); // now has comment-update-own - $this->actingAs($this->user)->deleteComment($commentId); - $this->assertResponseStatus(200); + $this->actingAs($this->user)->deleteComment($comment)->assertOk(); } public function test_comment_delete_all_permission() { + /** @var Page $ownPage */ $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; - $commentId = $this->asAdmin()->addComment($ownPage); + $this->asAdmin()->addComment($ownPage); + /** @var Comment $comment */ + $comment = $ownPage->comments()->latest()->first(); // no comment-delete-all - $this->actingAs($this->user)->deleteComment($commentId); - $this->assertResponseStatus(403); + $this->actingAs($this->user)->deleteComment($comment)->assertStatus(403); $this->giveUserPermissions($this->user, ['comment-delete-all']); // now has comment-delete-all - $this->actingAs($this->user)->deleteComment($commentId); - $this->assertResponseStatus(200); + $this->actingAs($this->user)->deleteComment($comment)->assertOk(); } - private function addComment($page) + private function addComment(Page $page): TestResponse { $comment = factory(Comment::class)->make(); - $url = "/comment/$page->id"; - $request = [ - 'text' => $comment->text, - 'html' => $comment->html, - ]; - $this->postJson($url, $request); - $comment = $page->comments()->first(); - - return $comment === null ? null : $comment->id; + return $this->postJson("/comment/$page->id", $comment->only('text', 'html')); } - private function updateComment($commentId) + private function updateComment(Comment $comment): TestResponse { - $comment = factory(Comment::class)->make(); - $url = "/comment/$commentId"; - $request = [ - 'text' => $comment->text, - 'html' => $comment->html, - ]; + $commentData = factory(Comment::class)->make(); - return $this->putJson($url, $request); + return $this->putJson("/comment/{$comment->id}", $commentData->only('text', 'html')); } - private function deleteComment($commentId) + private function deleteComment(Comment $comment): TestResponse { - $url = '/comment/' . $commentId; - - return $this->json('DELETE', $url); + return $this->json('DELETE', '/comment/' . $comment->id); } } diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php index ae0c0ff95..499c0c9f9 100644 --- a/tests/PublicActionTest.php +++ b/tests/PublicActionTest.php @@ -2,7 +2,6 @@ namespace Tests; -use Auth; use BookStack\Auth\Permissions\PermissionService; use BookStack\Auth\Permissions\RolePermission; use BookStack\Auth\Role; @@ -10,6 +9,7 @@ use BookStack\Auth\User; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\View; class PublicActionTest extends TestCase diff --git a/tests/RecycleBinTest.php b/tests/RecycleBinTest.php index 1c5445212..f3e30c0d0 100644 --- a/tests/RecycleBinTest.php +++ b/tests/RecycleBinTest.php @@ -8,8 +8,8 @@ use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Deletion; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; -use DB; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\DB; class RecycleBinTest extends TestCase { diff --git a/tests/SharedTestHelpers.php b/tests/SharedTestHelpers.php index df6c613df..e4d27c849 100644 --- a/tests/SharedTestHelpers.php +++ b/tests/SharedTestHelpers.php @@ -89,7 +89,7 @@ trait SharedTestHelpers /** * Get a user that's not a system user such as the guest user. */ - public function getNormalUser() + public function getNormalUser(): User { return User::query()->where('system_name', '=', null)->get()->last(); } @@ -211,6 +211,27 @@ trait SharedTestHelpers return $permissionRepo->saveNewRole($roleData); } + /** + * Create a group of entities that belong to a specific user. + * + * @return array{book: Book, chapter: Chapter, page: Page} + */ + protected function createEntityChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array + { + if (empty($updaterUser)) { + $updaterUser = $creatorUser; + } + + $userAttrs = ['created_by' => $creatorUser->id, 'owned_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]; + $book = factory(Book::class)->create($userAttrs); + $chapter = factory(Chapter::class)->create(array_merge(['book_id' => $book->id], $userAttrs)); + $page = factory(Page::class)->create(array_merge(['book_id' => $book->id, 'chapter_id' => $chapter->id], $userAttrs)); + $restrictionService = $this->app[PermissionService::class]; + $restrictionService->buildJointPermissionsForEntity($book); + + return compact('book', 'chapter', 'page'); + } + /** * Mock the HttpFetcher service and return the given data on fetch. */ diff --git a/tests/TestCase.php b/tests/TestCase.php index 080515173..98e0dfbac 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -62,7 +62,7 @@ abstract class TestCase extends BaseTestCase * Assert that an activity entry exists of the given key. * Checks the activity belongs to the given entity if provided. */ - protected function assertActivityExists(string $type, Entity $entity = null) + protected function assertActivityExists(string $type, ?Entity $entity = null, string $detail = '') { $detailsToCheck = ['type' => $type]; @@ -71,6 +71,10 @@ abstract class TestCase extends BaseTestCase $detailsToCheck['entity_id'] = $entity->id; } + if ($detail) { + $detailsToCheck['detail'] = $detail; + } + $this->assertDatabaseHas('activities', $detailsToCheck); } } diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index bab85be7a..2cab765ae 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -7,9 +7,9 @@ use BookStack\Entities\Models\Page; use BookStack\Entities\Tools\PageContent; use BookStack\Facades\Theme; use BookStack\Theming\ThemeEvents; -use File; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Illuminate\Support\Facades\File; use League\CommonMark\ConfigurableEnvironmentInterface; class ThemeTest extends TestCase diff --git a/tests/User/UserManagementTest.php b/tests/User/UserManagementTest.php index 4fd7bacc7..ed2fb5f04 100644 --- a/tests/User/UserManagementTest.php +++ b/tests/User/UserManagementTest.php @@ -3,12 +3,112 @@ namespace Tests\User; use BookStack\Actions\ActivityType; +use BookStack\Auth\Role; use BookStack\Auth\User; use BookStack\Entities\Models\Page; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Str; use Tests\TestCase; class UserManagementTest extends TestCase { + public function test_user_creation() + { + /** @var User $user */ + $user = factory(User::class)->make(); + $adminRole = Role::getRole('admin'); + + $resp = $this->asAdmin()->get('/settings/users'); + $resp->assertElementContains('a[href="' . url('/settings/users/create') . '"]', 'Add New User'); + + $this->get('/settings/users/create') + ->assertElementContains('form[action="' . url('/settings/users/create') . '"]', 'Save'); + + $resp = $this->post('/settings/users/create', [ + 'name' => $user->name, + 'email' => $user->email, + 'password' => $user->password, + 'password-confirm' => $user->password, + 'roles[' . $adminRole->id . ']' => 'true', + ]); + $resp->assertRedirect('/settings/users'); + + $resp = $this->get('/settings/users'); + $resp->assertSee($user->name); + + $this->assertDatabaseHas('users', $user->only('name', 'email')); + + $user->refresh(); + $this->assertStringStartsWith(Str::slug($user->name), $user->slug); + } + + public function test_user_updating() + { + $user = $this->getNormalUser(); + $password = $user->password; + + $resp = $this->asAdmin()->get('/settings/users/' . $user->id); + $resp->assertSee($user->email); + + $this->put($user->getEditUrl(), [ + 'name' => 'Barry Scott', + ])->assertRedirect('/settings/users'); + + $this->assertDatabaseHas('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password]); + $this->assertDatabaseMissing('users', ['name' => $user->name]); + + $user->refresh(); + $this->assertStringStartsWith(Str::slug($user->name), $user->slug); + } + + public function test_user_password_update() + { + $user = $this->getNormalUser(); + $userProfilePage = '/settings/users/' . $user->id; + + $this->asAdmin()->get($userProfilePage); + $this->put($userProfilePage, [ + 'password' => 'newpassword', + ])->assertRedirect($userProfilePage); + + $this->get($userProfilePage)->assertSee('Password confirmation required'); + + $this->put($userProfilePage, [ + 'password' => 'newpassword', + 'password-confirm' => 'newpassword', + ])->assertRedirect('/settings/users'); + + $userPassword = User::query()->find($user->id)->password; + $this->assertTrue(Hash::check('newpassword', $userPassword)); + } + + public function test_user_cannot_be_deleted_if_last_admin() + { + $adminRole = Role::getRole('admin'); + + // Delete all but one admin user if there are more than one + $adminUsers = $adminRole->users; + if (count($adminUsers) > 1) { + /** @var User $user */ + foreach ($adminUsers->splice(1) as $user) { + $user->delete(); + } + } + + // Ensure we currently only have 1 admin user + $this->assertEquals(1, $adminRole->users()->count()); + /** @var User $user */ + $user = $adminRole->users->first(); + + $resp = $this->asAdmin()->delete('/settings/users/' . $user->id); + $resp->assertRedirect('/settings/users/' . $user->id); + + $resp = $this->get('/settings/users/' . $user->id); + $resp->assertSee('You cannot delete the only admin'); + + $this->assertDatabaseHas('users', ['id' => $user->id]); + } + public function test_delete() { $editor = $this->getEditor(); @@ -42,4 +142,26 @@ class UserManagementTest extends TestCase 'owned_by' => $newOwner->id, ]); } + + public function test_guest_profile_shows_limited_form() + { + $guest = User::getDefault(); + $resp = $this->asAdmin()->get('/settings/users/' . $guest->id); + $resp->assertSee('Guest'); + $resp->assertElementNotExists('#password'); + } + + public function test_guest_profile_cannot_be_deleted() + { + $guestUser = User::getDefault(); + $resp = $this->asAdmin()->get('/settings/users/' . $guestUser->id . '/delete'); + $resp->assertSee('Delete User'); + $resp->assertSee('Guest'); + $resp->assertElementContains('form[action$="/settings/users/' . $guestUser->id . '"] button', 'Confirm'); + + $resp = $this->delete('/settings/users/' . $guestUser->id); + $resp->assertRedirect('/settings/users/' . $guestUser->id); + $resp = $this->followRedirects($resp); + $resp->assertSee('cannot delete the guest user'); + } } diff --git a/tests/User/UserPreferencesTest.php b/tests/User/UserPreferencesTest.php index 1d5d3e729..b39c2c47c 100644 --- a/tests/User/UserPreferencesTest.php +++ b/tests/User/UserPreferencesTest.php @@ -2,6 +2,7 @@ namespace Tests\User; +use BookStack\Entities\Models\Bookshelf; use Tests\TestCase; class UserPreferencesTest extends TestCase @@ -106,4 +107,44 @@ class UserPreferencesTest extends TestCase $home = $this->get('/login'); $home->assertElementExists('.dark-mode'); } + + public function test_books_view_type_preferences_when_list() + { + $editor = $this->getEditor(); + setting()->putUser($editor, 'books_view_type', 'list'); + + $this->actingAs($editor)->get('/books') + ->assertElementNotExists('.featured-image-container') + ->assertElementExists('.content-wrap .entity-list-item'); + } + + public function test_books_view_type_preferences_when_grid() + { + $editor = $this->getEditor(); + setting()->putUser($editor, 'books_view_type', 'grid'); + + $this->actingAs($editor)->get('/books') + ->assertElementExists('.featured-image-container'); + } + + public function test_shelf_view_type_change() + { + $editor = $this->getEditor(); + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); + setting()->putUser($editor, 'bookshelf_view_type', 'list'); + + $this->actingAs($editor)->get($shelf->getUrl()) + ->assertElementNotExists('.featured-image-container') + ->assertElementExists('.content-wrap .entity-list-item') + ->assertSee('Grid View'); + + $req = $this->patch("/settings/users/{$editor->id}/switch-shelf-view", ['view_type' => 'grid']); + $req->assertRedirect($shelf->getUrl()); + + $this->actingAs($editor)->get($shelf->getUrl()) + ->assertElementExists('.featured-image-container') + ->assertElementNotExists('.content-wrap .entity-list-item') + ->assertSee('List View'); + } } diff --git a/tests/User/UserProfileTest.php b/tests/User/UserProfileTest.php index 859a036e0..3942efa8e 100644 --- a/tests/User/UserProfileTest.php +++ b/tests/User/UserProfileTest.php @@ -5,11 +5,13 @@ namespace Tests\User; use Activity; use BookStack\Actions\ActivityType; use BookStack\Auth\User; -use BookStack\Entities\Models\Bookshelf; -use Tests\BrowserKitTest; +use Tests\TestCase; -class UserProfileTest extends BrowserKitTest +class UserProfileTest extends TestCase { + /** + * @var User + */ protected $user; public function setUp(): void @@ -21,74 +23,73 @@ class UserProfileTest extends BrowserKitTest public function test_profile_page_shows_name() { $this->asAdmin() - ->visit('/user/' . $this->user->slug) - ->see($this->user->name); + ->get('/user/' . $this->user->slug) + ->assertSee($this->user->name); } public function test_profile_page_shows_recent_entities() { $content = $this->createEntityChainBelongingToUser($this->user, $this->user); - $this->asAdmin() - ->visit('/user/' . $this->user->slug) - // Check the recently created page is shown - ->see($content['page']->name) - // Check the recently created chapter is shown - ->see($content['chapter']->name) - // Check the recently created book is shown - ->see($content['book']->name); + $resp = $this->asAdmin()->get('/user/' . $this->user->slug); + // Check the recently created page is shown + $resp->assertSee($content['page']->name); + // Check the recently created chapter is shown + $resp->assertSee($content['chapter']->name); + // Check the recently created book is shown + $resp->assertSee($content['book']->name); } public function test_profile_page_shows_created_content_counts() { - $newUser = $this->getNewBlankUser(); + $newUser = factory(User::class)->create(); - $this->asAdmin()->visit('/user/' . $newUser->slug) - ->see($newUser->name) - ->seeInElement('#content-counts', '0 Books') - ->seeInElement('#content-counts', '0 Chapters') - ->seeInElement('#content-counts', '0 Pages'); + $this->asAdmin()->get('/user/' . $newUser->slug) + ->assertSee($newUser->name) + ->assertElementContains('#content-counts', '0 Books') + ->assertElementContains('#content-counts', '0 Chapters') + ->assertElementContains('#content-counts', '0 Pages'); $this->createEntityChainBelongingToUser($newUser, $newUser); - $this->asAdmin()->visit('/user/' . $newUser->slug) - ->see($newUser->name) - ->seeInElement('#content-counts', '1 Book') - ->seeInElement('#content-counts', '1 Chapter') - ->seeInElement('#content-counts', '1 Page'); + $this->asAdmin()->get('/user/' . $newUser->slug) + ->assertSee($newUser->name) + ->assertElementContains('#content-counts', '1 Book') + ->assertElementContains('#content-counts', '1 Chapter') + ->assertElementContains('#content-counts', '1 Page'); } public function test_profile_page_shows_recent_activity() { - $newUser = $this->getNewBlankUser(); + $newUser = factory(User::class)->create(); $this->actingAs($newUser); $entities = $this->createEntityChainBelongingToUser($newUser, $newUser); Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE); Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE); - $this->asAdmin()->visit('/user/' . $newUser->slug) - ->seeInElement('#recent-user-activity', 'updated book') - ->seeInElement('#recent-user-activity', 'created page') - ->seeInElement('#recent-user-activity', $entities['page']->name); + $this->asAdmin()->get('/user/' . $newUser->slug) + ->assertElementContains('#recent-user-activity', 'updated book') + ->assertElementContains('#recent-user-activity', 'created page') + ->assertElementContains('#recent-user-activity', $entities['page']->name); } - public function test_clicking_user_name_in_activity_leads_to_profile_page() + public function test_user_activity_has_link_leading_to_profile() { - $newUser = $this->getNewBlankUser(); + $newUser = factory(User::class)->create(); $this->actingAs($newUser); $entities = $this->createEntityChainBelongingToUser($newUser, $newUser); Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE); Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE); - $this->asAdmin()->visit('/')->clickInElement('#recent-activity', $newUser->name) - ->seePageIs('/user/' . $newUser->slug) - ->see($newUser->name); + $linkSelector = '#recent-activity a[href$="/user/' . $newUser->slug . '"]'; + $this->asAdmin()->get('/') + ->assertElementContains($linkSelector, $newUser->name); } public function test_profile_has_search_links_in_created_entity_lists() { $user = $this->getEditor(); - $resp = $this->actingAs($this->getAdmin())->visit('/user/' . $user->slug); + $resp = $this->actingAs($this->getAdmin())->get('/user/' . $user->slug); $expectedLinks = [ '/search?term=%7Bcreated_by%3A' . $user->slug . '%7D+%7Btype%3Apage%7D', @@ -98,66 +99,7 @@ class UserProfileTest extends BrowserKitTest ]; foreach ($expectedLinks as $link) { - $resp->seeInElement('[href$="' . $link . '"]', 'View All'); + $resp->assertElementContains('[href$="' . $link . '"]', 'View All'); } } - - public function test_guest_profile_shows_limited_form() - { - $this->asAdmin() - ->visit('/settings/users') - ->click('Guest') - ->dontSeeElement('#password'); - } - - public function test_guest_profile_cannot_be_deleted() - { - $guestUser = User::getDefault(); - $this->asAdmin()->visit('/settings/users/' . $guestUser->id . '/delete') - ->see('Delete User')->see('Guest') - ->press('Confirm') - ->seePageIs('/settings/users/' . $guestUser->id) - ->see('cannot delete the guest user'); - } - - public function test_books_view_is_list() - { - $editor = $this->getEditor(); - setting()->putUser($editor, 'books_view_type', 'list'); - - $this->actingAs($editor) - ->visit('/books') - ->pageNotHasElement('.featured-image-container') - ->pageHasElement('.content-wrap .entity-list-item'); - } - - public function test_books_view_is_grid() - { - $editor = $this->getEditor(); - setting()->putUser($editor, 'books_view_type', 'grid'); - - $this->actingAs($editor) - ->visit('/books') - ->pageHasElement('.featured-image-container'); - } - - public function test_shelf_view_type_change() - { - $editor = $this->getEditor(); - $shelf = Bookshelf::query()->first(); - setting()->putUser($editor, 'bookshelf_view_type', 'list'); - - $this->actingAs($editor)->visit($shelf->getUrl()) - ->pageNotHasElement('.featured-image-container') - ->pageHasElement('.content-wrap .entity-list-item') - ->see('Grid View'); - - $req = $this->patch("/settings/users/{$editor->id}/switch-shelf-view", ['view_type' => 'grid']); - $req->assertRedirectedTo($shelf->getUrl()); - - $this->actingAs($editor)->visit($shelf->getUrl()) - ->pageHasElement('.featured-image-container') - ->pageNotHasElement('.content-wrap .entity-list-item') - ->see('List View'); - } }