diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index ea5d492..3b9b30b 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -32,7 +32,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout code" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" - name: "Check file permissions" run: | @@ -57,7 +57,7 @@ jobs: uses: "actions/checkout@v4" - name: "Install dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" with: dependency-versions: "highest" @@ -88,24 +88,24 @@ jobs: extensions: "mbstring" - name: "Checkout code" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" - name: "Install dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" with: dependency-versions: "${{ matrix.dependencies }}" - name: "Raise constraint for antecedent/patchwork" if: "${{ matrix.dependencies == 'lowest' }}" - run: "composer require --dev --prefer-lowest --update-with-all-dependencies 'antecedent/patchwork:^2.0.8'" + run: "composer require --dev --prefer-lowest --update-with-all-dependencies 'antecedent/patchwork:^2.1.26'" - name: "Execute unit tests" if: "${{ ! (matrix.php-version == '8.1' && matrix.dependencies == 'highest') }}" - run: "composer run-script unit-tests -- --no-coverage" + run: "composer run-script unit-tests" - name: "Execute unit tests with coverage" if: "${{ matrix.php-version == '8.1' && matrix.dependencies == 'highest' }}" - run: "composer run-script unit-tests" + run: "composer run-script unit-tests-with-coverage" - name: "Send coverage to Coveralls" env: @@ -130,13 +130,13 @@ jobs: coverage: "none" - name: "Checkout code" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" - name: "Validate Composer configuration" run: "composer validate --strict" - name: "Install dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" with: dependency-versions: "highest" @@ -161,7 +161,7 @@ jobs: coverage: "none" - name: "Checkout code" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" - name: "Check EditorConfig configuration" run: "test -f .editorconfig" @@ -170,7 +170,7 @@ jobs: uses: "greut/eclint-action@v0" - name: "Install dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" with: dependency-versions: "highest" @@ -185,7 +185,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout code" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" - name: "Check exported files" run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 45652ed..fc13de9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # BC Security Changelog +## Version 0.23.0 (2024-04-04) + +**Important**: either deactivate and reactivate plugin after update or install new cron job manually via WP-CLI: `wp cron event schedule bc-security/failed-logins-clean-up now daily`. + +### Added + +* New built-in rule for bad request banner module that triggers when non-existing `.tgz` or `.zip` file is accessed [#155](https://github.com/chesio/bc-security/issues/155). +* Plugin has been tested with WordPress 6.5 [#152](https://github.com/chesio/bc-security/issues/152). + +### Changed + +* List of supported PHP versions for PHP version check has been updated to include PHP 8.3 [#151](https://github.com/chesio/bc-security/issues/151). + +### Fixed + +* Fix SQL syntax error when bulk unlocking entries in internal blocklist [#154](https://github.com/chesio/bc-security/pull/154) - thanks to @szepeviktor. +* Table storing failed logins data is now pruned automatically [#156](https://github.com/chesio/bc-security/issues/156). + ## Version 0.22.1 (2024-02-07) ### Fixed @@ -12,7 +30,7 @@ This release has been tested with PHP 8.3 and WordPress 6.4. PHP 8.1 or newer an ### Added -* New built-in rule to bad request banner module that triggers when non-existing `readme.txt` file is accessed [#149](https://github.com/chesio/bc-security/issues/149). +* New built-in rule for bad request banner module that triggers when non-existing `readme.txt` file is accessed [#149](https://github.com/chesio/bc-security/issues/149). * Plugin has been tested with PHP 8.3 [#145](https://github.com/chesio/bc-security/issues/145). * Plugin has been tested with WordPress 6.4 [#144](https://github.com/chesio/bc-security/issues/144). @@ -92,7 +110,7 @@ These adjustments led to some breaking changes, therefore during update it is re * PHP 8.0 is supported [#104](https://github.com/chesio/bc-security/issues/104). * Alert about "No removed plugins installed" has more information [#107](https://github.com/chesio/bc-security/issues/107). * Detection of plugins installed from WordPress Directory has been improved [#112](https://github.com/chesio/bc-security/issues/112). -* On WordPress 5.8 and newer the plugin cannot be accidentally overriden from WordPress.org Plugins Directory [#111](https://github.com/chesio/bc-security/issues/111). +* On WordPress 5.8 and newer the plugin cannot be accidentally overridden from WordPress.org Plugins Directory [#111](https://github.com/chesio/bc-security/issues/111). ## Older releases diff --git a/README.md b/README.md index 4a31137..caeb137 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ You may also optionally provide Google API key if you want to check your website BC Security can help you find potential security issues or even signs of breach. -Since security measures for development instalations do not have to be as strict as for live installations, some checks are run only in *live environment*. A *live environment* is determined as one where [`wp_get_environment_type()`](https://developer.wordpress.org/reference/functions/wp_get_environment_type/) returns either `production` or `staging`, but there is a [dedicated filter](#customization) that can be used to override *live environment* detection. +Since security measures for development installations do not have to be as strict as for live installations, some checks are run only in *live environment*. A *live environment* is determined as one where [`wp_get_environment_type()`](https://developer.wordpress.org/reference/functions/wp_get_environment_type/) returns either `production` or `staging`, but there is a [dedicated filter](#customization) that can be used to override *live environment* detection. #### Basic checks @@ -144,10 +144,11 @@ Passwords are validated on user creation, password change or password reset. If Remote IP addresses that are scanning your website for weaknesses can be automatically [blocked](#internal-blocklist) for configured amount of time. Such scanners can be usually quite easily detected because while scanning a website they trigger a lot of 404 errors and URLs they try to access differ from "valid" 404 errors: usually they try to find a known vulnerable plugin, forgotten backup file or PHP script used for administrative purposes. -There are three built-in rules available (they are not active by default): +There are four built-in rules available (they are not active by default): 1. ban when non-existent PHP file is requested (any URL ending with `.php`) -2. ban when non-existent backup file is requested (any URL targeting file with `backup` in basename or with `.back`, `.old` or `.tmp` extension) -3. ban when non-existent `readme.txt` file is accessed +2. ban when non-existent archive file is requested (any URL ending with `.tgz` or `.zip`) +3. ban when non-existent backup file is requested (any URL targeting file with `backup` in basename or with `.back`, `.old` or `.tmp` extension) +4. ban when non-existent `readme.txt` file is accessed You may define custom rules as well (in form of regular expression). @@ -198,7 +199,10 @@ You can mute all email notifications by setting constant `BC_SECURITY_MUTE_NOTIF Following events triggered by BC Security are logged: 1. Short and long lockout events (see [Login Security](#login-security) feature) -2. Requests blocked by [external](#external-blocklist) or [internal](#internal-blocklist) blocklist +2. Requests blocked by [external](#external-blocklist) or [internal](#internal-blocklist) blocklist _(* see note below)_ +3. Requests that match any of configured [bad request rules](#bad-requests-banner) + +_(*) Note: in case internal blocklist is synchronized with `.htaccess` file, HTTP requests are blocked by webserver before being handled to WordPress, therefore they cannot be logged by the plugin._ Following events triggered by WordPress core are logged: diff --git a/bc-security.php b/bc-security.php index a5bb4b5..5f0e26d 100644 --- a/bc-security.php +++ b/bc-security.php @@ -4,12 +4,12 @@ * Plugin Name: BC Security * Plugin URI: https://github.com/chesio/bc-security * Description: Helps keeping WordPress websites secure. - * Version: 0.22.1 + * Version: 0.23.0 * Author: Česlav Przywara * Author URI: https://www.chesio.com * Requires PHP: 8.1 * Requires at least: 6.2 - * Tested up to: 6.4 + * Tested up to: 6.5 * Text Domain: bc-security * GitHub Plugin URI: https://github.com/chesio/bc-security * Update URI: https://github.com/chesio/bc-security diff --git a/classes/BlueChip/Security/Admin.php b/classes/BlueChip/Security/Admin.php index 5e58463..8cf3132 100644 --- a/classes/BlueChip/Security/Admin.php +++ b/classes/BlueChip/Security/Admin.php @@ -35,9 +35,9 @@ class Admin */ public function init(string $plugin_filename): self { - add_action('admin_menu', [$this, 'makeAdminMenu']); - add_action('admin_init', [$this, 'initAdminPages']); - add_filter('plugin_action_links_' . plugin_basename($plugin_filename), [$this, 'filterActionLinks']); + add_action('admin_menu', $this->makeAdminMenu(...)); + add_action('admin_init', $this->initAdminPages(...)); + add_filter('plugin_action_links_' . plugin_basename($plugin_filename), $this->filterActionLinks(...)); return $this; } @@ -59,7 +59,7 @@ public function addPage(Core\Admin\AbstractPage $page): self /** * @action https://developer.wordpress.org/reference/hooks/admin_init/ */ - public function initAdminPages(): void + private function initAdminPages(): void { foreach ($this->pages as $page) { $page->initPage(); @@ -72,7 +72,7 @@ public function initAdminPages(): void * * @action https://developer.wordpress.org/reference/hooks/admin_menu/ */ - public function makeAdminMenu(): void + private function makeAdminMenu(): void { if (empty($this->pages)) { // No pages registered = no pages (no menu) to show. @@ -100,7 +100,7 @@ public function makeAdminMenu(): void $page->getMenuTitle() . $this->renderCounter($page), self::CAPABILITY, $page->getSlug(), - [$page, 'printContents'] + $page->printContents(...), ); if ($page_hook) { $page->setPageHook($page_hook); @@ -118,7 +118,7 @@ public function makeAdminMenu(): void * * @return string[] */ - public function filterActionLinks(array $links): array + private function filterActionLinks(array $links): array { if (current_user_can(self::CAPABILITY) && isset($this->pages['bc-security-setup'])) { $links[] = \sprintf( diff --git a/classes/BlueChip/Security/Core/Admin/AbstractPage.php b/classes/BlueChip/Security/Core/Admin/AbstractPage.php index 50d16aa..e625bd7 100644 --- a/classes/BlueChip/Security/Core/Admin/AbstractPage.php +++ b/classes/BlueChip/Security/Core/Admin/AbstractPage.php @@ -91,7 +91,7 @@ public static function getPageUrl(): string */ public function setPageHook(string $page_hook): void { - add_action('load-' . $page_hook, [$this, 'loadPage']); + add_action('load-' . $page_hook, $this->loadPage(...)); } @@ -109,7 +109,7 @@ public function initPage(): void * * @action https://developer.wordpress.org/reference/hooks/load-page_hook/ */ - public function loadPage(): void + protected function loadPage(): void { // By default do nothing. } diff --git a/classes/BlueChip/Security/Core/Settings.php b/classes/BlueChip/Security/Core/Settings.php index c51f0ea..416cae4 100644 --- a/classes/BlueChip/Security/Core/Settings.php +++ b/classes/BlueChip/Security/Core/Settings.php @@ -233,13 +233,16 @@ public function sanitize(array $settings, array $defaults = []): array // Loop over default values instead of provided $settings - this way only known keys are preserved. foreach ($values as $key => $default_value) { if (isset($settings[$key])) { - // New value is provided, sanitize it either... - $values[$key] = isset(static::SANITIZERS[$key]) - // ...using provided callback... - ? \call_user_func(static::SANITIZERS[$key], $settings[$key], $default_value) - // ...or by type. - : self::sanitizeByType($settings[$key], $default_value) - ; + // Sanitize the value by type first (= ensure the value has expected type). + $value = self::sanitizeByType($settings[$key], $default_value); + + // If custom sanitizer for this setting key is provided... + if (isset(static::SANITIZERS[$key])) { + // ...execute it on type-safe value. + $value = \call_user_func(static::SANITIZERS[$key], $value, $default_value); + } + + $values[$key] = $value; } } @@ -249,8 +252,13 @@ public function sanitize(array $settings, array $defaults = []): array /** * Sanitize the $value according to type of $default value. + * + * @param mixed $value + * @param mixed[]|bool|float|int|string $default + * + * @return mixed[]|bool|float|int|string */ - protected static function sanitizeByType(mixed $value, mixed $default): mixed + protected static function sanitizeByType(mixed $value, array|bool|float|int|string $default): array|bool|float|int|string { if (\is_bool($default)) { return (bool) $value; @@ -324,7 +332,7 @@ public function update(string $name, mixed $value): bool */ public function addUpdateHook(callable $callback): void { - add_action("update_option_{$this->option_name}", [$this, 'updateOption'], 10, 2); + add_action("update_option_{$this->option_name}", $this->updateOption(...), 10, 2); add_action("update_option_{$this->option_name}", $callback, 10, 3); } @@ -335,7 +343,7 @@ public function addUpdateHook(callable $callback): void * @param array $old_value * @param array $new_value */ - public function updateOption(array $old_value, array $new_value): void + private function updateOption(array $old_value, array $new_value): void { $this->data = $new_value; } diff --git a/classes/BlueChip/Security/Helpers/FormHelper.php b/classes/BlueChip/Security/Helpers/FormHelper.php index eb5f918..c855fde 100644 --- a/classes/BlueChip/Security/Helpers/FormHelper.php +++ b/classes/BlueChip/Security/Helpers/FormHelper.php @@ -31,7 +31,7 @@ abstract class FormHelper * name and empty (false-like) value is printed before checkbox - this way, * POST data contains value for checkbox even if it is left unchecked. * Note that this approach works thanks to the fact that PHP retains value - * of the last key occurence in POST data when there are multiple occurences + * of the last key occurrence in POST data when there are multiple occurrences * of the same key (name); when checkbox is checked (and included in POST), * its value overwrites hidden field value. * See: http://stackoverflow.com/a/1992745 diff --git a/classes/BlueChip/Security/Modules/Access/Bouncer.php b/classes/BlueChip/Security/Modules/Access/Bouncer.php index 7105ed8..fba9769 100644 --- a/classes/BlueChip/Security/Modules/Access/Bouncer.php +++ b/classes/BlueChip/Security/Modules/Access/Bouncer.php @@ -41,7 +41,7 @@ public function load(): void // I have to balance two requirements here: // 1) Run the access check as early as possible (I consider `init` hook too late). // 2) Allow myself and others to hook stuff (ie. events logger) in a clean way before access check executes. - add_action('plugins_loaded', [$this, 'checkAccess'], 1, 0); + add_action('plugins_loaded', $this->checkAccess(...), 1, 0); } @@ -50,7 +50,7 @@ public function load(): void */ public function init(): void { - add_filter('authenticate', [$this, 'checkLoginAttempt'], 1, 1); // Leave priority 0 for site maintainers. + add_filter('authenticate', $this->checkLoginAttempt(...), 1, 1); // Leave priority 0 for site maintainers. } @@ -80,12 +80,12 @@ public function isBlocked(Scope $access_scope): bool } - //// Hookers - public methods that should in fact be private - /** * Check if access to website is allowed from given remote address. + * + * @action https://developer.wordpress.org/reference/hooks/plugins_loaded/ */ - public function checkAccess(): void + private function checkAccess(): void { if ($this->isBlocked(Scope::WEBSITE)) { Utils::blockAccessTemporarily($this->remote_address); @@ -95,8 +95,10 @@ public function checkAccess(): void /** * Check if access to login is allowed from given remote address. + * + * @filter https://developer.wordpress.org/reference/hooks/authenticate/ */ - public function checkLoginAttempt(WP_Error|WP_User|null $user): WP_Error|WP_User|null + private function checkLoginAttempt(WP_Error|WP_User|null $user): WP_Error|WP_User|null { if ($this->isBlocked(Scope::ADMIN)) { Utils::blockAccessTemporarily($this->remote_address); diff --git a/classes/BlueChip/Security/Modules/BadRequestsBanner/AdminPage.php b/classes/BlueChip/Security/Modules/BadRequestsBanner/AdminPage.php index 9d9af65..24d605a 100644 --- a/classes/BlueChip/Security/Modules/BadRequestsBanner/AdminPage.php +++ b/classes/BlueChip/Security/Modules/BadRequestsBanner/AdminPage.php @@ -34,7 +34,7 @@ public function __construct(Settings $settings, HtaccessSynchronizer $htaccess_s } - public function loadPage(): void + protected function loadPage(): void { $this->displaySettingsErrors(); } diff --git a/classes/BlueChip/Security/Modules/BadRequestsBanner/BuiltInRules.php b/classes/BlueChip/Security/Modules/BadRequestsBanner/BuiltInRules.php index fdb4181..9177cf8 100644 --- a/classes/BlueChip/Security/Modules/BadRequestsBanner/BuiltInRules.php +++ b/classes/BlueChip/Security/Modules/BadRequestsBanner/BuiltInRules.php @@ -6,6 +6,10 @@ abstract class BuiltInRules { + public const ARCHIVE_FILES = 'archive-files'; + + private const ARCHIVE_FILES_PATTERN = '\.(tgz|zip)$'; + public const BACKUP_FILES = 'backup-files'; private const BACKUP_FILES_PATTERN = 'backup|(\.(back|old|tmp)$)'; @@ -34,6 +38,11 @@ public static function enlist(): array self::README_FILES_PATTERN, __('(any URI targeting /readme.txt file)', 'bc-security') ), + self::ARCHIVE_FILES => new BanRule( + __('Non-existent archive files', 'bc-security'), + self::ARCHIVE_FILES_PATTERN, + __('(any URI targeting file with .tgz or .zip extension)', 'bc-security') + ), self::BACKUP_FILES => new BanRule( __('Non-existent backup files', 'bc-security'), self::BACKUP_FILES_PATTERN, diff --git a/classes/BlueChip/Security/Modules/BadRequestsBanner/Core.php b/classes/BlueChip/Security/Modules/BadRequestsBanner/Core.php index cc12c79..7f157ea 100644 --- a/classes/BlueChip/Security/Modules/BadRequestsBanner/Core.php +++ b/classes/BlueChip/Security/Modules/BadRequestsBanner/Core.php @@ -28,7 +28,7 @@ public function init(): void { // Run only if request did not originate from the webserver itself. if ($this->remote_address !== $this->server_address) { - add_action('wp', [$this, 'check404Queries'], 100, 1); // Run late, allow others to interfere (do their stuff). + add_action('wp', $this->check404Queries(...), 100, 1); // Run late, allow others to interfere (do their stuff). } } @@ -40,7 +40,7 @@ public function init(): void * * @see WP::main() */ - public function check404Queries(WP $wp): void + private function check404Queries(WP $wp): void { /** @var \WP_Query $wp_query */ global $wp_query; diff --git a/classes/BlueChip/Security/Modules/BadRequestsBanner/Settings.php b/classes/BlueChip/Security/Modules/BadRequestsBanner/Settings.php index 47a5cdc..5383e48 100644 --- a/classes/BlueChip/Security/Modules/BadRequestsBanner/Settings.php +++ b/classes/BlueChip/Security/Modules/BadRequestsBanner/Settings.php @@ -8,6 +8,11 @@ class Settings extends CoreSettings { + /** + * @var string Is built-in rule "Archive files" active? [bool:no] + */ + public const BUILT_IN_RULE_ARCHIVE_FILES = BuiltInRules::ARCHIVE_FILES; + /** * @var string Is built-in rule "Backup files" active? [bool:no] */ @@ -44,6 +49,7 @@ class Settings extends CoreSettings * @var array Default values for all settings. */ protected const DEFAULTS = [ + self::BUILT_IN_RULE_ARCHIVE_FILES => false, self::BUILT_IN_RULE_BACKUP_FILES => false, self::BUILT_IN_RULE_PHP_FILES => false, self::BUILT_IN_RULE_README_FILES => false, @@ -51,13 +57,6 @@ class Settings extends CoreSettings self::BAN_DURATION => 60, ]; - /** - * @var array Custom sanitizers. - */ - protected const SANITIZERS = [ - self::BAD_REQUEST_PATTERNS => [self::class, 'parseList'], - ]; - /** * @return BanRule[] List of active ban rules. diff --git a/classes/BlueChip/Security/Modules/Checklist/AdminPage.php b/classes/BlueChip/Security/Modules/Checklist/AdminPage.php index 02b2f00..570ad83 100644 --- a/classes/BlueChip/Security/Modules/Checklist/AdminPage.php +++ b/classes/BlueChip/Security/Modules/Checklist/AdminPage.php @@ -54,7 +54,7 @@ public function initPage(): void } - public function loadPage(): void + protected function loadPage(): void { $this->enqueueCssAssets(['checklist' => 'checklist.css',]); $this->enqueueJsAssets(['checklist' => 'checklist.js',]); diff --git a/classes/BlueChip/Security/Modules/Checklist/Checks/PhpVersionSupported.php b/classes/BlueChip/Security/Modules/Checklist/Checks/PhpVersionSupported.php index 88f1750..8b78ca1 100644 --- a/classes/BlueChip/Security/Modules/Checklist/Checks/PhpVersionSupported.php +++ b/classes/BlueChip/Security/Modules/Checklist/Checks/PhpVersionSupported.php @@ -14,9 +14,9 @@ class PhpVersionSupported extends Checklist\BasicCheck * @link https://www.php.net/supported-versions.php */ private const SUPPORTED_VERSIONS = [ - '8.0' => '2023-11-26', '8.1' => '2024-11-25', '8.2' => '2025-12-08', + '8.3' => '2026-11-23', ]; diff --git a/classes/BlueChip/Security/Modules/Checklist/Manager.php b/classes/BlueChip/Security/Modules/Checklist/Manager.php index bc663e0..1295d67 100644 --- a/classes/BlueChip/Security/Modules/Checklist/Manager.php +++ b/classes/BlueChip/Security/Modules/Checklist/Manager.php @@ -44,14 +44,14 @@ public function __construct(AutorunSettings $settings, CronManager $cron_manager public function init(): void { // When settings are updated, ensure that cron jobs for advanced checks are properly (de)activated. - $this->settings->addUpdateHook([$this, 'updateCronJobs']); + $this->settings->addUpdateHook($this->updateCronJobs(...)); // Hook into cron job execution. - add_action(CronJobs::CHECKLIST_CHECK, [$this, 'runBasicChecks'], 10, 0); + add_action(CronJobs::CHECKLIST_CHECK, $this->runBasicChecks(...), 10, 0); foreach ($this->getAdvancedChecks() as $advanced_check) { - add_action($advanced_check->getCronJobHook(), [$advanced_check, 'runInCron'], 10, 0); + add_action($advanced_check->getCronJobHook(), $advanced_check->runInCron(...), 10, 0); } // Register AJAX handler. - AjaxHelper::addHandler(self::ASYNC_CHECK_ACTION, [$this, 'runCheck']); + AjaxHelper::addHandler(self::ASYNC_CHECK_ACTION, $this->runCheck(...)); } @@ -175,10 +175,8 @@ public function getBasicChecks(bool $only_meaningful = true): array /** * Run all basic checks that are meaningful and are set to be monitored in non-interactive mode. - * - * @internal Method is intended to be run from within cron request. */ - public function runBasicChecks(): void + private function runBasicChecks(): void { $checks = $this->getBasicChecks(); $issues = []; @@ -209,10 +207,8 @@ public function runBasicChecks(): void /** * Run check (asynchronously). - * - * @internal Method is intended to be run from within AJAX requests. */ - public function runCheck(): void + private function runCheck(): void { if (empty($check_id = \filter_input(INPUT_POST, 'check_id'))) { wp_send_json_error([ @@ -240,7 +236,7 @@ public function runCheck(): void /** * Activate or deactivate cron jobs for advanced checks according to settings. */ - public function updateCronJobs(): void + private function updateCronJobs(): void { foreach ($this->getAdvancedChecks(false) as $check_id => $advanced_check) { if ($this->settings[$check_id]) { diff --git a/classes/BlueChip/Security/Modules/Cron/Jobs.php b/classes/BlueChip/Security/Modules/Cron/Jobs.php index a157245..fa4d5e4 100644 --- a/classes/BlueChip/Security/Modules/Cron/Jobs.php +++ b/classes/BlueChip/Security/Modules/Cron/Jobs.php @@ -12,6 +12,9 @@ abstract class Jobs /** string: Hook name for "External blocklist refresh" cron job */ public const EXTERNAL_BLOCKLIST_REFRESH = 'bc-security/external-blocklist-refresh'; + /** string: Hook name for "Failed logins table clean up" cron job */ + public const FAILED_LOGINS_CLEAN_UP = 'bc-security/failed-logins-clean-up'; + /** string: Hook name for "Automatic internal blocklist purging" cron job */ public const INTERNAL_BLOCKLIST_CLEAN_UP = 'bc-security/internal-blocklist-clean-up'; diff --git a/classes/BlueChip/Security/Modules/Cron/Settings.php b/classes/BlueChip/Security/Modules/Cron/Settings.php index 5f7f644..d1c7a88 100644 --- a/classes/BlueChip/Security/Modules/Cron/Settings.php +++ b/classes/BlueChip/Security/Modules/Cron/Settings.php @@ -17,6 +17,7 @@ class Settings extends CoreSettings protected const DEFAULTS = [ Jobs::CHECKLIST_CHECK => true, Jobs::EXTERNAL_BLOCKLIST_REFRESH => false, + Jobs::FAILED_LOGINS_CLEAN_UP => true, Jobs::INTERNAL_BLOCKLIST_CLEAN_UP => true, Jobs::LOGS_CLEAN_UP_BY_AGE => true, Jobs::LOGS_CLEAN_UP_BY_SIZE => true, diff --git a/classes/BlueChip/Security/Modules/ExternalBlocklist/AdminPage.php b/classes/BlueChip/Security/Modules/ExternalBlocklist/AdminPage.php index 93f0007..ed97fbe 100644 --- a/classes/BlueChip/Security/Modules/ExternalBlocklist/AdminPage.php +++ b/classes/BlueChip/Security/Modules/ExternalBlocklist/AdminPage.php @@ -39,7 +39,7 @@ public function __construct(Settings $settings, Manager $eb_manager) } - public function loadPage(): void + protected function loadPage(): void { $this->displaySettingsErrors(); } diff --git a/classes/BlueChip/Security/Modules/ExternalBlocklist/Manager.php b/classes/BlueChip/Security/Modules/ExternalBlocklist/Manager.php index 0282509..bc1bbf1 100644 --- a/classes/BlueChip/Security/Modules/ExternalBlocklist/Manager.php +++ b/classes/BlueChip/Security/Modules/ExternalBlocklist/Manager.php @@ -55,10 +55,10 @@ public function __construct(private Settings $settings, private CronManager $cro public function init(): void { // Whenever list of enabled sources changes, update local cache and enable or disable related cron job. - $this->settings->addUpdateHook([$this, 'updateLocalCacheState']); + $this->settings->addUpdateHook($this->updateLocalCacheState(...)); // Hook to action triggered by cron job (and integration tests). - add_action(Jobs::EXTERNAL_BLOCKLIST_REFRESH, [$this, 'refreshSources'], 10, 0); + add_action(Jobs::EXTERNAL_BLOCKLIST_REFRESH, $this->refreshSources(...), 10, 0); } @@ -124,10 +124,8 @@ public function isBlocked(string $ip_address, Scope $access_scope): bool /** * Warm up or tear down blocklist sources depending on their activation status. * Also activate or deactivate cron job for blocklist refresh. - * - * @internal Not part of public API. */ - public function updateLocalCacheState(): void + private function updateLocalCacheState(): void { $cron_job_required = false; @@ -150,10 +148,8 @@ public function updateLocalCacheState(): void /** * Warm up all enabled sources. - * - * @internal Not part of public API. */ - public function refreshSources(): void + private function refreshSources(): void { foreach ($this->sources as $class => $source) { if ($this->settings->isEnabled($class)) { diff --git a/classes/BlueChip/Security/Modules/ExternalBlocklist/Settings.php b/classes/BlueChip/Security/Modules/ExternalBlocklist/Settings.php index d828817..e946a00 100644 --- a/classes/BlueChip/Security/Modules/ExternalBlocklist/Settings.php +++ b/classes/BlueChip/Security/Modules/ExternalBlocklist/Settings.php @@ -26,15 +26,8 @@ class Settings extends CoreSettings /** * Sanitize lock scope values. Allow only expected values. - * - * @internal Form data are submitted as string, so always accept string type for $value. - * - * @param int|string $value - * @param int $default - * - * @return int */ - public static function sanitizeAccessScope(int|string $value, int $default): int + public static function sanitizeAccessScope(int $value, int $default): int { return Scope::tryFrom((int) $value) ? ((int) $value) : $default; } diff --git a/classes/BlueChip/Security/Modules/Hardening/AdminPage.php b/classes/BlueChip/Security/Modules/Hardening/AdminPage.php index f99c501..a856ade 100644 --- a/classes/BlueChip/Security/Modules/Hardening/AdminPage.php +++ b/classes/BlueChip/Security/Modules/Hardening/AdminPage.php @@ -36,7 +36,7 @@ public function __construct(Settings $settings) } - public function loadPage(): void + protected function loadPage(): void { $this->displaySettingsErrors(); diff --git a/classes/BlueChip/Security/Modules/Hardening/Core.php b/classes/BlueChip/Security/Modules/Hardening/Core.php index e488c13..8306836 100644 --- a/classes/BlueChip/Security/Modules/Hardening/Core.php +++ b/classes/BlueChip/Security/Modules/Hardening/Core.php @@ -29,7 +29,7 @@ class Core implements Initializable private const PWNED_PASSWORD_META_KEY = 'bc-security/pwned-password'; - private bool $rest_api_supressed = false; + private bool $rest_api_suppressed = false; public function __construct(private Settings $settings) @@ -44,7 +44,7 @@ public function init(): void { if ($this->settings[Settings::DISABLE_PINGBACKS]) { // Disable pingbacks. - add_filter('xmlrpc_methods', [$this, 'disablePingbacks'], 10, 1); + add_filter('xmlrpc_methods', $this->disablePingbacks(...), 10, 1); } if ($this->settings[Settings::DISABLE_XML_RPC]) { // Disable all XML-RPC methods requiring authentication. @@ -56,13 +56,13 @@ public function init(): void } if ($this->settings[Settings::DISABLE_USERNAMES_DISCOVERY]) { // Alter REST API responses. - add_filter('oembed_response_data', [$this, 'filterAuthorInOembed'], 100, 1); - add_filter('rest_request_before_callbacks', [$this, 'filterJsonAPIAuthor'], 100, 3); - add_filter('rest_post_dispatch', [$this, 'adjustJsonAPIHeaders'], 100, 1); + add_filter('oembed_response_data', $this->filterAuthorInOembed(...), 100, 1); + add_filter('rest_request_before_callbacks', $this->filterJsonAPIAuthor(...), 100, 3); + add_filter('rest_post_dispatch', $this->adjustJsonAPIHeaders(...), 100, 1); if (!is_admin()) { // Prevent usernames enumeration. - add_filter('request', [$this, 'filterAuthorQuery'], 5, 1); - add_action('parse_request', [$this, 'stopAuthorScan'], 10, 1); + add_filter('request', $this->filterAuthorQuery(...), 5, 1); + add_action('parse_request', $this->stopAuthorScan(...), 10, 1); } } if ($this->settings[Settings::DISABLE_LOGIN_WITH_EMAIL]) { @@ -70,26 +70,26 @@ public function init(): void // https://developer.wordpress.org/reference/hooks/authenticate/#more-information remove_filter('authenticate', 'wp_authenticate_email_password', 20); // Add a warning to the login screen. - add_filter('login_message', [$this, 'warnAboutDisabledLoginWithEmail'], 100, 0); + add_filter('login_message', $this->warnAboutDisabledLoginWithEmail(...), 100, 0); } if ($this->settings[Settings::DISABLE_LOGIN_WITH_USERNAME]) { // Remove the option to authenticate with username and password. // https://developer.wordpress.org/reference/hooks/authenticate/#more-information remove_filter('authenticate', 'wp_authenticate_username_password', 20); // Add a warning to the login screen. - add_filter('login_message', [$this, 'warnAboutDisabledLoginWithUsername'], 100, 0); + add_filter('login_message', $this->warnAboutDisabledLoginWithUsername(...), 100, 0); } if ($this->settings[Settings::CHECK_PASSWORDS]) { // Check user password on successful login. - add_action('wp_login', [$this, 'checkUserPassword'], 10, 2); + add_action('wp_login', $this->checkUserPassword(...), 10, 2); // Display warning notice if pwned password has been detected for current user. - add_action('current_screen', [$this, 'displayPasswordPwnedNotice'], 10, 1); + add_action('current_screen', $this->displayPasswordPwnedNotice(...), 10, 1); } if ($this->settings[Settings::VALIDATE_PASSWORDS]) { // Validate password on user creation or profile update. - add_action('user_profile_update_errors', [$this, 'validatePasswordUpdate'], 10, 3); + add_action('user_profile_update_errors', $this->validatePasswordUpdate(...), 10, 3); // Validate password on password reset. - add_action('validate_password_reset', [$this, 'validatePasswordReset'], 10, 2); + add_action('validate_password_reset', $this->validatePasswordReset(...), 10, 2); } } @@ -103,7 +103,7 @@ public function init(): void * * @return array */ - public function disablePingbacks(array $methods): array + private function disablePingbacks(array $methods): array { unset($methods['pingback.ping']); return $methods; @@ -119,7 +119,7 @@ public function disablePingbacks(array $methods): array * * @return array */ - public function filterAuthorInOembed(array $data): array + private function filterAuthorInOembed(array $data): array { if (isset($data['author_name'])) { unset($data['author_name']); @@ -142,7 +142,7 @@ public function filterAuthorInOembed(array $data): array * * @return WP_REST_Response|WP_HTTP_Response|WP_Error|mixed */ - public function filterJsonAPIAuthor(mixed $response, array $handler, WP_REST_Request $request): mixed + private function filterJsonAPIAuthor(mixed $response, array $handler, WP_REST_Request $request): mixed { $route = $request->get_route(); @@ -158,7 +158,7 @@ public function getUrlBase(): string )->getUrlBase(); if (\preg_match('#' . \preg_quote($url_base, '#') . '/*$#i', $route)) { - $this->rest_api_supressed = true; + $this->rest_api_suppressed = true; return rest_ensure_response(new WP_Error( 'rest_user_cannot_view', __('Sorry, you are not allowed to list users.'), // WP core message @@ -169,7 +169,7 @@ public function getUrlBase(): string $matches = []; if (\preg_match('#' . \preg_quote($url_base, '#') . '/+(\d+)/*$#i', $route, $matches)) { if (get_current_user_id() !== (int) $matches[1]) { - $this->rest_api_supressed = true; + $this->rest_api_suppressed = true; return rest_ensure_response(new WP_Error( 'rest_user_invalid_id', __('Invalid user ID.'), // WP core message. @@ -190,9 +190,9 @@ public function getUrlBase(): string * * @return \WP_HTTP_Response */ - public function adjustJsonAPIHeaders(\WP_HTTP_Response $response): \WP_HTTP_Response + private function adjustJsonAPIHeaders(\WP_HTTP_Response $response): \WP_HTTP_Response { - if ($this->rest_api_supressed) { + if ($this->rest_api_suppressed) { $response->header('Allow', 'GET'); } @@ -207,7 +207,7 @@ public function adjustJsonAPIHeaders(\WP_HTTP_Response $response): \WP_HTTP_Resp * * @return array */ - public function filterAuthorQuery(array $query_vars): array + private function filterAuthorQuery(array $query_vars): array { if (self::smellsLikeAuthorScan($query_vars) && (self::smellsLikeAuthorScan($_GET) || self::smellsLikeAuthorScan($_POST))) { // I smell author scanning! @@ -227,7 +227,7 @@ public function filterAuthorQuery(array $query_vars): array * * @return bool True if `author` key is present and its value is either an array or can be seen as numeric. */ - protected static function smellsLikeAuthorScan(array $query_vars): bool + private static function smellsLikeAuthorScan(array $query_vars): bool { return !empty($query_vars['author']) && (\is_array($query_vars['author']) || \is_numeric(\preg_replace('/[^0-9]/', '', $query_vars['author']))); } @@ -238,7 +238,7 @@ protected static function smellsLikeAuthorScan(array $query_vars): bool * * @param \WP $wp */ - public function stopAuthorScan(\WP $wp): void + private function stopAuthorScan(\WP $wp): void { if ($wp->query_vars[self::AUTHOR_SCAN_QUERY_VAR] ?? false) { status_header(404); @@ -256,7 +256,7 @@ public function stopAuthorScan(\WP $wp): void /** * @return string HTML string with warning about login with email being disabled. */ - public function warnAboutDisabledLoginWithEmail(): string + private function warnAboutDisabledLoginWithEmail(): string { return '

' . sprintf(esc_html__('%s: Login with email is disabled on this website!', 'bc-security'), '' . esc_html__('Important', 'bc-security') . '') . '

'; } @@ -265,7 +265,7 @@ public function warnAboutDisabledLoginWithEmail(): string /** * @return string HTML string with warning about login with username being disabled. */ - public function warnAboutDisabledLoginWithUsername(): string + private function warnAboutDisabledLoginWithUsername(): string { return '

' . sprintf(esc_html__('%s: Login with username is disabled on this website!', 'bc-security'), '' . esc_html__('Important', 'bc-security') . '') . '

'; } @@ -276,7 +276,7 @@ public function warnAboutDisabledLoginWithUsername(): string * * @action https://developer.wordpress.org/reference/hooks/wp_login/ */ - public function checkUserPassword(string $username, WP_User $user): void + private function checkUserPassword(string $username, WP_User $user): void { if (empty($password = \filter_input(INPUT_POST, 'pwd'))) { // Non-interactive sign on (probably). @@ -300,7 +300,7 @@ public function checkUserPassword(string $username, WP_User $user): void * * @param \WP_Screen $screen */ - public function displayPasswordPwnedNotice(\WP_Screen $screen): void + private function displayPasswordPwnedNotice(\WP_Screen $screen): void { $user = wp_get_current_user(); @@ -335,7 +335,7 @@ public function displayPasswordPwnedNotice(\WP_Screen $screen): void * @param bool $update Whether this is a user update. * @param object $user User object (passed by reference). */ - public function validatePasswordUpdate(WP_Error &$errors, bool $update, object &$user): void + private function validatePasswordUpdate(WP_Error &$errors, bool $update, object &$user): void { if ($errors->get_error_code()) { // There is an error reported already, skip the check. @@ -359,7 +359,7 @@ public function validatePasswordUpdate(WP_Error &$errors, bool $update, object & * @param WP_Error $errors * @param WP_User|WP_Error $user WP_User object if the login and reset key match. WP_Error object otherwise. */ - public function validatePasswordReset(WP_Error $errors, WP_Error|WP_User $user): void + private function validatePasswordReset(WP_Error $errors, WP_Error|WP_User $user): void { if ($errors->get_error_code()) { // There is an error reported already, skip the check. @@ -381,7 +381,7 @@ public function validatePasswordReset(WP_Error $errors, WP_Error|WP_User $user): * @param string $password * @param WP_Error $errors WP_Error object (passed by reference). */ - protected static function checkIfPasswordHasBeenPwned(string $password, WP_Error &$errors): void + private static function checkIfPasswordHasBeenPwned(string $password, WP_Error &$errors): void { if (HaveIBeenPwned::hasPasswordBeenPwned($password)) { $message = \sprintf( diff --git a/classes/BlueChip/Security/Modules/InternalBlocklist/AdminPage.php b/classes/BlueChip/Security/Modules/InternalBlocklist/AdminPage.php index d4df487..85e9e07 100644 --- a/classes/BlueChip/Security/Modules/InternalBlocklist/AdminPage.php +++ b/classes/BlueChip/Security/Modules/InternalBlocklist/AdminPage.php @@ -75,7 +75,7 @@ public function __construct( } - public function loadPage(): void + protected function loadPage(): void { $this->resetCount(); $this->processActions(); diff --git a/classes/BlueChip/Security/Modules/InternalBlocklist/Manager.php b/classes/BlueChip/Security/Modules/InternalBlocklist/Manager.php index 6a70f7e..cc5ac11 100644 --- a/classes/BlueChip/Security/Modules/InternalBlocklist/Manager.php +++ b/classes/BlueChip/Security/Modules/InternalBlocklist/Manager.php @@ -109,8 +109,8 @@ public function deactivate(): void public function init(): void { // Hook into cron job execution. - add_action(Modules\Cron\Jobs::INTERNAL_BLOCKLIST_CLEAN_UP, [$this, 'pruneInCron'], 10, 0); - add_action(self::HTACCESS_SYNCHRONIZATION, [$this, 'runSynchronizationWithHtaccessFile'], 10, 0); + add_action(Modules\Cron\Jobs::INTERNAL_BLOCKLIST_CLEAN_UP, $this->pruneInCron(...), 10, 0); + add_action(self::HTACCESS_SYNCHRONIZATION, $this->runSynchronizationWithHtaccessFile(...), 10, 0); } @@ -176,7 +176,7 @@ public function countFrom(int $timestamp): int * @param string $order_by * @param string $order * - * @return array> + * @return array> */ public function fetch(Scope $access_scope = Scope::ANY, int $from = 0, int $limit = 20, string $order_by = '', string $order = ''): array { @@ -342,7 +342,7 @@ public function prune(): bool * * @internal Runs `prune` method and discards its return value. */ - public function pruneInCron(): void + private function pruneInCron(): void { $this->prune(); } @@ -440,7 +440,7 @@ public function unlockMany(array $ids): int } // Prepare query. $query = \sprintf( - "UDPATE {$this->blocklist_table} SET release_time = '%s' WHERE %s", + "UPDATE {$this->blocklist_table} SET release_time = '%s' WHERE %s", MySQLDateTime::formatDateTime(\time()), \implode(' OR ', \array_map(fn (int $id): string => \sprintf('id = %d', $id), $ids)) ); @@ -465,7 +465,7 @@ public function unlockMany(array $ids): int * * @return int|null Record ID or null if no record with given $ip_address, $access_scope and ban $reason exists. */ - protected function getId(string $ip_address, Scope $access_scope, BanReason $ban_reason): ?int + private function getId(string $ip_address, Scope $access_scope, BanReason $ban_reason): ?int { // Prepare query. /** @var string $query */ @@ -502,7 +502,7 @@ public function isHtaccessFileInSync(): bool * * @internal This is alias of synchronizeWithHtaccessFile() method that just ignores its return value. */ - public function runSynchronizationWithHtaccessFile(): void + private function runSynchronizationWithHtaccessFile(): void { $this->synchronizeWithHtaccessFile(); } diff --git a/classes/BlueChip/Security/Modules/Log/AdminPage.php b/classes/BlueChip/Security/Modules/Log/AdminPage.php index c53e8e8..b205537 100644 --- a/classes/BlueChip/Security/Modules/Log/AdminPage.php +++ b/classes/BlueChip/Security/Modules/Log/AdminPage.php @@ -94,7 +94,7 @@ function () { } - public function loadPage(): void + protected function loadPage(): void { $this->resetCount(); $this->displaySettingsErrors(); diff --git a/classes/BlueChip/Security/Modules/Log/EventsMonitor.php b/classes/BlueChip/Security/Modules/Log/EventsMonitor.php index e418a09..a4ae245 100644 --- a/classes/BlueChip/Security/Modules/Log/EventsMonitor.php +++ b/classes/BlueChip/Security/Modules/Log/EventsMonitor.php @@ -29,8 +29,8 @@ public function __construct(private string $remote_address, private string $serv public function load(): void { // Depending on access scope, blocklist can be checked very early, so add these monitors early. - add_action(AccessHooks::EXTERNAL_BLOCKLIST_HIT_EVENT, [$this, 'logExternalBlocklistHit'], 10, 3); - add_action(AccessHooks::INTERNAL_BLOCKLIST_HIT_EVENT, [$this, 'logInternalBlocklistHit'], 10, 2); + add_action(AccessHooks::EXTERNAL_BLOCKLIST_HIT_EVENT, $this->logExternalBlocklistHit(...), 10, 3); + add_action(AccessHooks::INTERNAL_BLOCKLIST_HIT_EVENT, $this->logInternalBlocklistHit(...), 10, 2); } @@ -38,29 +38,29 @@ public function init(): void { // Log the following WordPress events: // - bad authentication cookie - add_action('auth_cookie_bad_username', [$this, 'logBadCookie'], 5, 1); - add_action('auth_cookie_bad_hash', [$this, 'logBadCookie'], 5, 1); + add_action('auth_cookie_bad_username', $this->logBadCookie(...), 5, 1); + add_action('auth_cookie_bad_hash', $this->logBadCookie(...), 5, 1); // - failed login - add_action('wp_login_failed', [$this, 'logFailedLogin'], 5, 2); + add_action('wp_login_failed', $this->logFailedLogin(...), 5, 2); // - successful login - add_action('wp_login', [$this, 'logSuccessfulLogin'], 5, 1); + add_action('wp_login', $this->logSuccessfulLogin(...), 5, 1); // - 404 query (only if request did not originate from the webserver itself) if ($this->remote_address !== $this->server_address) { - add_action('wp', [$this, 'log404Queries'], 20, 1); + add_action('wp', $this->log404Queries(...), 20, 1); } // Log the following BC Security events: // - lockout event - add_action(LoginHooks::LOCKOUT_EVENT, [$this, 'logLockoutEvent'], 10, 3); + add_action(LoginHooks::LOCKOUT_EVENT, $this->logLockoutEvent(...), 10, 3); // - bad request event - add_action(BadRequestsBannerHooks::BAD_REQUEST_EVENT, [$this, 'logBadRequestEvent'], 10, 3); + add_action(BadRequestsBannerHooks::BAD_REQUEST_EVENT, $this->logBadRequestEvent(...), 10, 3); } /** * Log external blocklist hit. */ - public function logExternalBlocklistHit(string $remote_address, Scope $access_scope, Source $source): void + private function logExternalBlocklistHit(string $remote_address, Scope $access_scope, Source $source): void { do_action(Action::EVENT, (new Events\BlocklistHit())->setIpAddress($remote_address)->setRequestType($access_scope)->setSource($source)); } @@ -69,7 +69,7 @@ public function logExternalBlocklistHit(string $remote_address, Scope $access_sc /** * Log internal blocklist hit. */ - public function logInternalBlocklistHit(string $remote_address, Scope $access_scope): void + private function logInternalBlocklistHit(string $remote_address, Scope $access_scope): void { do_action(Action::EVENT, (new Events\BlocklistHit())->setIpAddress($remote_address)->setRequestType($access_scope)); } @@ -82,7 +82,7 @@ public function logInternalBlocklistHit(string $remote_address, Scope $access_sc * * @see WP::main() */ - public function log404Queries(WP $wp): void + private function log404Queries(WP $wp): void { /** @var \WP_Query $wp_query */ global $wp_query; @@ -98,7 +98,7 @@ public function log404Queries(WP $wp): void * * @param array $cookie_elements */ - public function logBadCookie(array $cookie_elements): void + private function logBadCookie(array $cookie_elements): void { do_action(Action::EVENT, (new Events\AuthBadCookie())->setUsername($cookie_elements['username'])); } @@ -107,7 +107,7 @@ public function logBadCookie(array $cookie_elements): void /** * Log failed login. */ - public function logFailedLogin(string $username, WP_Error $error): void + private function logFailedLogin(string $username, WP_Error $error): void { do_action(Action::EVENT, (new Events\LoginFailure())->setUsername($username)->setError($error)); } @@ -116,7 +116,7 @@ public function logFailedLogin(string $username, WP_Error $error): void /** * Log successful login. */ - public function logSuccessfulLogin(string $username): void + private function logSuccessfulLogin(string $username): void { do_action(Action::EVENT, (new Events\LoginSuccessful())->setUsername($username)); } @@ -125,7 +125,7 @@ public function logSuccessfulLogin(string $username): void /** * Log lockout event. */ - public function logLockoutEvent(string $remote_address, string $username, int $duration): void + private function logLockoutEvent(string $remote_address, string $username, int $duration): void { do_action(Action::EVENT, (new Events\LoginLockout())->setDuration($duration)->setIpAddress($remote_address)->setUsername($username)); } @@ -134,7 +134,7 @@ public function logLockoutEvent(string $remote_address, string $username, int $d /** * Log bad request event. */ - public function logBadRequestEvent(string $remote_address, string $request, BanRule $ban_rule): void + private function logBadRequestEvent(string $remote_address, string $request, BanRule $ban_rule): void { do_action(Action::EVENT, (new Events\BadRequestBan())->setBanRuleName($ban_rule->getName())->setIpAddress($remote_address)->setRequestUri($request)); } diff --git a/classes/BlueChip/Security/Modules/Log/Logger.php b/classes/BlueChip/Security/Modules/Log/Logger.php index e96ea9e..0b77b0a 100644 --- a/classes/BlueChip/Security/Modules/Log/Logger.php +++ b/classes/BlueChip/Security/Modules/Log/Logger.php @@ -90,26 +90,26 @@ public function load(): void { // Expose log methods via do_action() - inspired by Wonolog: // https://github.com/inpsyde/Wonolog/blob/master/docs/02-basic-wonolog-concepts.md#level-rich-log-hooks - add_action(Action::EMERGENCY, [$this, 'emergency'], 10, 2); - add_action(Action::ALERT, [$this, 'alert'], 10, 2); - add_action(Action::CRITICAL, [$this, 'critical'], 10, 2); - add_action(Action::ERROR, [$this, 'error'], 10, 2); - add_action(Action::WARNING, [$this, 'warning'], 10, 2); - add_action(Action::NOTICE, [$this, 'notice'], 10, 2); - add_action(Action::INFO, [$this, 'info'], 10, 2); - add_action(Action::DEBUG, [$this, 'debug'], 10, 2); - add_action(Action::LOG, [$this, 'log'], 10, 3); - add_action(Action::EVENT, [$this, 'logEvent'], 10, 1); + add_action(Action::EMERGENCY, $this->emergency(...), 10, 2); + add_action(Action::ALERT, $this->alert(...), 10, 2); + add_action(Action::CRITICAL, $this->critical(...), 10, 2); + add_action(Action::ERROR, $this->error(...), 10, 2); + add_action(Action::WARNING, $this->warning(...), 10, 2); + add_action(Action::NOTICE, $this->notice(...), 10, 2); + add_action(Action::INFO, $this->info(...), 10, 2); + add_action(Action::DEBUG, $this->debug(...), 10, 2); + add_action(Action::LOG, $this->log(...), 10, 3); + add_action(Action::EVENT, $this->logEvent(...), 10, 1); } public function init(): void { // Hook into cron job execution. - add_action(CronJobs::LOGS_CLEAN_UP_BY_AGE, [$this, 'pruneByAgeInCron'], 10, 0); - add_action(CronJobs::LOGS_CLEAN_UP_BY_SIZE, [$this, 'pruneBySizeInCron'], 10, 0); + add_action(CronJobs::LOGS_CLEAN_UP_BY_AGE, $this->pruneByAgeInCron(...), 10, 0); + add_action(CronJobs::LOGS_CLEAN_UP_BY_SIZE, $this->pruneBySizeInCron(...), 10, 0); // Hook into reverse DNS lookup. - add_action(Hooks::HOSTNAME_RESOLVED, [$this, 'processReverseDnsLookupResponse'], 10, 1); + add_action(Hooks::HOSTNAME_RESOLVED, $this->processReverseDnsLookupResponse(...), 10, 1); } @@ -216,7 +216,7 @@ public function translateLogLevel(string $level): ?int * * @param Response $response */ - public function processReverseDnsLookupResponse(Response $response): void + private function processReverseDnsLookupResponse(Response $response): void { $this->wpdb->update( $this->log_table, @@ -376,7 +376,7 @@ public function pruneByAge(): bool * * @internal Runs `pruneByAge` method and discards its return value. */ - public function pruneByAgeInCron(): void + private function pruneByAgeInCron(): void { $this->pruneByAge(); } @@ -416,7 +416,7 @@ public function pruneBySize(): bool * * @internal Runs `pruneBySize` method and discards its return value. */ - public function pruneBySizeInCron(): void + private function pruneBySizeInCron(): void { $this->pruneBySize(); } diff --git a/classes/BlueChip/Security/Modules/Login/AdminPage.php b/classes/BlueChip/Security/Modules/Login/AdminPage.php index c29924d..35c19f9 100644 --- a/classes/BlueChip/Security/Modules/Login/AdminPage.php +++ b/classes/BlueChip/Security/Modules/Login/AdminPage.php @@ -32,7 +32,7 @@ public function __construct(Settings $settings) } - public function loadPage(): void + protected function loadPage(): void { $this->displaySettingsErrors(); } diff --git a/classes/BlueChip/Security/Modules/Login/Bookkeeper.php b/classes/BlueChip/Security/Modules/Login/Bookkeeper.php index 7d94879..36936e9 100644 --- a/classes/BlueChip/Security/Modules/Login/Bookkeeper.php +++ b/classes/BlueChip/Security/Modules/Login/Bookkeeper.php @@ -5,13 +5,15 @@ namespace BlueChip\Security\Modules\Login; use BlueChip\Security\Helpers\MySQLDateTime; +use BlueChip\Security\Modules\Cron\Jobs; +use BlueChip\Security\Modules\Initializable; use BlueChip\Security\Modules\Installable; use wpdb; /** * Storage and retrieval of lockout book-keeping data */ -class Bookkeeper implements Installable +class Bookkeeper implements Initializable, Installable { /** * @var string Name of DB table where failed logins are stored @@ -24,6 +26,7 @@ class Bookkeeper implements Installable */ private string $failed_logins_table; + /** * @param Settings $settings * @param wpdb $wpdb WordPress database access abstraction object @@ -66,6 +69,13 @@ public function uninstall(): void } + public function init(): void + { + // Hook into cron job execution. + add_action(Jobs::FAILED_LOGINS_CLEAN_UP, $this->pruneInCron(...), 10, 0); + } + + /** * Add failed login attempt from $ip_address using $username. * @@ -103,10 +113,8 @@ public function recordFailedLoginAttempt(string $ip_address, string $username): /** * Remove all expired entries from table. - * - * @return mixed */ - public function prune() + public function prune(): bool { // Remove all expired entries (older than threshold). $threshold = \time() - $this->settings->getResetTimeoutDuration(); @@ -117,7 +125,20 @@ public function prune() "DELETE FROM {$this->failed_logins_table} WHERE date_and_time <= %s", MySQLDateTime::formatDateTime($threshold) ); - // Execute query. - return $this->wpdb->query($query); + // Execute query + $result = $this->wpdb->query($query); + // Return result + return $result !== false; + } + + + /** + * @hook \BlueChip\Security\Modules\Cron\Jobs::FAILED_LOGINS_CLEAN_UP + * + * @internal Runs `prune` method and discards its return value. + */ + private function pruneInCron(): void + { + $this->prune(); } } diff --git a/classes/BlueChip/Security/Modules/Login/Gatekeeper.php b/classes/BlueChip/Security/Modules/Login/Gatekeeper.php index ab3dad6..adf126e 100644 --- a/classes/BlueChip/Security/Modules/Login/Gatekeeper.php +++ b/classes/BlueChip/Security/Modules/Login/Gatekeeper.php @@ -53,25 +53,23 @@ public function load(): void */ public function init(): void { - add_filter('illegal_user_logins', [$this, 'filterIllegalUserLogins'], 10, 1); + add_filter('illegal_user_logins', $this->filterIllegalUserLogins(...), 10, 1); - add_filter('authenticate', [$this, 'lockIpIfUsernameOnBlacklist'], 25, 2); // should run after default authentication filters + add_filter('authenticate', $this->lockIpIfUsernameOnBlacklist(...), 25, 2); // should run after default authentication filters if ($this->settings[Settings::GENERIC_LOGIN_ERROR_MESSAGE]) { - add_filter('authenticate', [$this, 'muteStandardErrorMessages'], 100, 1); // 100 ~ should run last - add_filter('shake_error_codes', [$this, 'filterShakeErrorCodes'], 10, 1); + add_filter('authenticate', $this->muteStandardErrorMessages(...), 100, 1); // 100 ~ should run last + add_filter('shake_error_codes', $this->filterShakeErrorCodes(...), 10, 1); } - add_action('wp_login_failed', [$this, 'handleFailedLogin'], 100, 1); + add_action('wp_login_failed', $this->handleFailedLogin(...), 100, 1); // Check authentication cookies: - add_action('auth_cookie_bad_username', [$this, 'handleBadCookie'], 10, 1); - add_action('auth_cookie_bad_hash', [$this, 'handleBadCookie'], 10, 1); + add_action('auth_cookie_bad_username', $this->handleBadCookie(...), 10, 1); + add_action('auth_cookie_bad_hash', $this->handleBadCookie(...), 10, 1); } - //// Hookers - public methods that should in fact be private - /** * Filter the list of blacklisted usernames. * @@ -81,7 +79,7 @@ public function init(): void * * @return string[] */ - public function filterIllegalUserLogins(array $usernames): array + private function filterIllegalUserLogins(array $usernames): array { return \array_merge($usernames, $this->settings->getUsernameBlacklist()); } @@ -90,11 +88,13 @@ public function filterIllegalUserLogins(array $usernames): array /** * Let generic `authentication_failed` error shake the login form. * + * @filter https://developer.wordpress.org/reference/hooks/shake_error_codes/ + * * @param string[] $error_codes * * @return string[] */ - public function filterShakeErrorCodes(array $error_codes): array + private function filterShakeErrorCodes(array $error_codes): array { $error_codes[] = 'authentication_failed'; return $error_codes; @@ -106,7 +106,7 @@ public function filterShakeErrorCodes(array $error_codes): array * * @param array $cookie_elements */ - public function handleBadCookie(array $cookie_elements): void + private function handleBadCookie(array $cookie_elements): void { // Clear authentication cookies completely $this->clearAuthCookie(); @@ -121,9 +121,11 @@ public function handleBadCookie(array $cookie_elements): void * 2) Reset valid value. * 3) Perform lockout if number of retries is above threshold. * + * @action https://developer.wordpress.org/reference/hooks/wp_login_failed/ + * * @param string $username */ - public function handleFailedLogin(string $username): void + private function handleFailedLogin(string $username): void { // Record failed login attempt, get total number of retries for IP $retries = $this->bookkeeper->recordFailedLoginAttempt($this->remote_address, $username); @@ -144,8 +146,10 @@ public function handleFailedLogin(string $username): void * used to log in and is present on username blacklist. * * Filter is called from wp_authenticate(). + * + * @filter https://developer.wordpress.org/reference/hooks/authenticate/ */ - public function lockIpIfUsernameOnBlacklist(WP_Error|WP_User|null $user, string $username): WP_Error|WP_User|null + private function lockIpIfUsernameOnBlacklist(WP_Error|WP_User|null $user, string $username): WP_Error|WP_User|null { // When a non-existing username (or email)... if (is_wp_error($user) && ($user->get_error_code() === 'invalid_username' || $user->get_error_code() === 'invalid_email')) { @@ -164,8 +168,10 @@ public function lockIpIfUsernameOnBlacklist(WP_Error|WP_User|null $user, string * Return null instead of WP_Error when authentication fails because of * invalid username, email or password forcing WP to display generic error * message. + * + * @filter https://developer.wordpress.org/reference/hooks/authenticate/ */ - public function muteStandardErrorMessages(WP_Error|WP_User|null $user): WP_Error|WP_User|null + private function muteStandardErrorMessages(WP_Error|WP_User|null $user): WP_Error|WP_User|null { if (is_wp_error($user)) { switch ($user->get_error_code()) { @@ -179,12 +185,10 @@ public function muteStandardErrorMessages(WP_Error|WP_User|null $user): WP_Error } - //// Private and protected methods - /** * Clear all WordPress authentication cookies (also for current request). */ - protected function clearAuthCookie(): void + private function clearAuthCookie(): void { wp_clear_auth_cookie(); @@ -203,7 +207,7 @@ protected function clearAuthCookie(): void * @param int $duration Duration (in secs) of lockout * @param BanReason $ban_reason Lockout reason */ - protected function lockOut(string $username, int $duration, BanReason $ban_reason): void + private function lockOut(string $username, int $duration, BanReason $ban_reason): void { // Lock IP address if ($this->ib_manager->lock($this->remote_address, $duration, Scope::ADMIN, $ban_reason)) { diff --git a/classes/BlueChip/Security/Modules/Login/Settings.php b/classes/BlueChip/Security/Modules/Login/Settings.php index c4acc1d..6d68e88 100644 --- a/classes/BlueChip/Security/Modules/Login/Settings.php +++ b/classes/BlueChip/Security/Modules/Login/Settings.php @@ -71,13 +71,13 @@ class Settings extends CoreSettings /** * Sanitize "username blacklist" setting. Must be list of valid usernames. * - * @param string|string[] $value + * @param string[] $value * * @return string[] */ - public static function sanitizeUsernameBlacklist($value): array + public static function sanitizeUsernameBlacklist(array $value): array { - return \array_filter(self::parseList($value), '\validate_username'); + return \array_filter($value, '\validate_username'); } diff --git a/classes/BlueChip/Security/Modules/Notifications/AdminPage.php b/classes/BlueChip/Security/Modules/Notifications/AdminPage.php index ecc422f..8f22a64 100644 --- a/classes/BlueChip/Security/Modules/Notifications/AdminPage.php +++ b/classes/BlueChip/Security/Modules/Notifications/AdminPage.php @@ -33,7 +33,7 @@ public function __construct(Settings $settings) } - public function loadPage(): void + protected function loadPage(): void { $this->displaySettingsErrors(); diff --git a/classes/BlueChip/Security/Modules/Notifications/Mailman.php b/classes/BlueChip/Security/Modules/Notifications/Mailman.php index 30e348b..b8d0212 100644 --- a/classes/BlueChip/Security/Modules/Notifications/Mailman.php +++ b/classes/BlueChip/Security/Modules/Notifications/Mailman.php @@ -28,28 +28,24 @@ abstract class Mailman * * @param string|string[] $to Email address(es) of notification recipient(s). * @param string $subject Subject of notification. - * @param string|string[] $message Body of notification. + * @param Message $message Body of notification. * * @return bool True if notification has been sent successfully, false otherwise. */ - public static function send($to, string $subject, $message): bool + public static function send(array|string $to, string $subject, Message $message): bool { - return \wp_mail( + return wp_mail( $to, self::formatSubject($subject), - self::formatMessage(\is_array($message) ? $message : [$message]) + self::formatMessage($message) ); } /** * Strip any HTML tags from $message and add plugin boilerplate to it. - * - * @param string[] $message Message body as list of lines. - * - * @return string */ - private static function formatMessage(array $message): string + private static function formatMessage(Message $message): string { $boilerplate_intro = [ \sprintf( @@ -71,7 +67,7 @@ private static function formatMessage(array $message): string ), ]; - return \implode(self::EOL, \array_merge($boilerplate_intro, self::stripTags($message), $boilerplate_outro)); + return \implode(self::EOL, \array_merge($boilerplate_intro, self::stripTags($message->getRaw()), $boilerplate_outro)); } @@ -142,7 +138,7 @@ function (array $matches) use (&$urls): string { $links_index = \array_map( fn (int $index, string $url): string => \sprintf('[%d] %s', $index + 1, $url), \array_keys($urls), - \array_values($urls) + $urls ); // ...and add it to the message. diff --git a/classes/BlueChip/Security/Modules/Notifications/Message.php b/classes/BlueChip/Security/Modules/Notifications/Message.php new file mode 100644 index 0000000..9fbb72c --- /dev/null +++ b/classes/BlueChip/Security/Modules/Notifications/Message.php @@ -0,0 +1,65 @@ +body = ($text !== '') ? [$text] : []; + } + + + public function __toString(): string + { + return \implode(PHP_EOL, $this->body); + } + + + public function addEmptyLine(): self + { + return $this->addLine(''); + } + + + public function addLine(string $text = ''): self + { + $this->body[] = $text; + return $this; + } + + + /** + * @param string[] $text + * + * @return self + */ + public function addLines(array $text): self + { + $this->body = \array_merge($this->body, \array_is_list($text) ? $text : \array_values($text)); + return $this; + } + + + public function getFingerprint(): string + { + return \sha1((string) $this); + } + + + /** + * @return string[] + */ + public function getRaw(): array + { + return $this->body; + } +} diff --git a/classes/BlueChip/Security/Modules/Notifications/Settings.php b/classes/BlueChip/Security/Modules/Notifications/Settings.php index ca95380..65fd324 100644 --- a/classes/BlueChip/Security/Modules/Notifications/Settings.php +++ b/classes/BlueChip/Security/Modules/Notifications/Settings.php @@ -82,12 +82,12 @@ class Settings extends CoreSettings /** * Sanitize "notification recipients" setting. Must be list of emails. * - * @param string|string[] $value + * @param string[] $value * * @return string[] */ - public static function sanitizeNotificationRecipient($value): array + public static function sanitizeNotificationRecipient(array $value): array { - return \array_filter(self::parseList($value), '\is_email'); + return \array_filter($value, '\is_email'); } } diff --git a/classes/BlueChip/Security/Modules/Notifications/Watchman.php b/classes/BlueChip/Security/Modules/Notifications/Watchman.php index e0d5a45..8e04120 100644 --- a/classes/BlueChip/Security/Modules/Notifications/Watchman.php +++ b/classes/BlueChip/Security/Modules/Notifications/Watchman.php @@ -23,7 +23,13 @@ class Watchman implements Activable, Initializable /** * @var string[] List of notifications recipients */ - private $recipients; + private array $recipients; + + + /** + * @var string[] List of sent notifications. + */ + private array $sent_notifications = []; /** @@ -80,24 +86,24 @@ public function init(): void } if ($this->settings[Settings::CORE_UPDATE_AVAILABLE]) { - add_action('set_site_transient_update_core', [$this, 'watchCoreUpdateAvailable'], 10, 1); + add_action('set_site_transient_update_core', $this->watchCoreUpdateAvailable(...), 10, 1); } if ($this->settings[Settings::PLUGIN_UPDATE_AVAILABLE]) { - add_action('set_site_transient_update_plugins', [$this, 'watchPluginUpdatesAvailable'], 10, 1); + add_action('set_site_transient_update_plugins', $this->watchPluginUpdatesAvailable(...), 10, 1); } if ($this->settings[Settings::THEME_UPDATE_AVAILABLE]) { - add_action('set_site_transient_update_themes', [$this, 'watchThemeUpdatesAvailable'], 10, 1); + add_action('set_site_transient_update_themes', $this->watchThemeUpdatesAvailable(...), 10, 1); } if ($this->settings[Settings::ADMIN_USER_LOGIN]) { - add_action('wp_login', [$this, 'watchWpLogin'], 10, 2); + add_action('wp_login', $this->watchWpLogin(...), 10, 2); } if ($this->settings[Settings::KNOWN_IP_LOCKOUT]) { - add_action(BadRequestBannerHooks::BAD_REQUEST_EVENT, [$this, 'watchBadRequestBanEvents'], 10, 3); - add_action(LoginHooks::LOCKOUT_EVENT, [$this, 'watchLockoutEvents'], 10, 3); + add_action(BadRequestBannerHooks::BAD_REQUEST_EVENT, $this->watchBadRequestBanEvents(...), 10, 3); + add_action(LoginHooks::LOCKOUT_EVENT, $this->watchLockoutEvents(...), 10, 3); } if ($this->settings[Settings::CHECKLIST_ALERT]) { - add_action(ChecklistHooks::ADVANCED_CHECK_ALERT, [$this, 'watchChecklistSingleCheckAlert'], 10, 2); - add_action(ChecklistHooks::BASIC_CHECKS_ALERT, [$this, 'watchChecklistMultipleChecksAlert'], 10, 1); + add_action(ChecklistHooks::ADVANCED_CHECK_ALERT, $this->watchChecklistSingleCheckAlert(...), 10, 2); + add_action(ChecklistHooks::BASIC_CHECKS_ALERT, $this->watchChecklistMultipleChecksAlert(...), 10, 1); } } @@ -124,14 +130,16 @@ public function deactivate(): void $user = wp_get_current_user(); if ($user->ID) { // Name the bastard that turned us off! - $message = \sprintf( - __('User "%s" had just deactivated BC Security plugin on your website!', 'bc-security'), - $user->user_login + $message = new Message( + \sprintf( + __('User "%s" had just deactivated BC Security plugin on your website!', 'bc-security'), + $user->user_login + ) ); } else { // No user means plugin has been probably deactivated via WP-CLI. // See: https://github.com/chesio/bc-security/issues/16#issuecomment-321541102 - $message = __('BC Security plugin on your website has been deactivated!', 'bc-security'); + $message = new Message(__('BC Security plugin on your website has been deactivated!', 'bc-security')); } $this->notify($subject, $message); @@ -147,7 +155,7 @@ public function deactivate(): void * * @param object $update_transient */ - public function watchCoreUpdateAvailable($update_transient): void + private function watchCoreUpdateAvailable($update_transient): void { // Check if update transient has the data we are interested in. if (!isset($update_transient->updates) || !\is_array($update_transient->updates) || empty($update_transient->updates)) { @@ -170,14 +178,13 @@ public function watchCoreUpdateAvailable($update_transient): void } $subject = __('WordPress update available', 'bc-security'); - $message = \sprintf( - __('WordPress has an update to version %s available.', 'bc-security'), - $latest_version + $message = new Message( + \sprintf( + __('WordPress has an update to version %s available.', 'bc-security'), + $latest_version, + ) ); - // Now it is time to make sure the method is not invoked anymore. - remove_action('set_site_transient_update_core', [$this, 'watchCoreUpdateAvailable'], 10); - // Send notification. if ($this->notify($subject, $message) !== false) { // No further notifications for this update. @@ -191,7 +198,7 @@ public function watchCoreUpdateAvailable($update_transient): void * * @param object $update_transient */ - public function watchPluginUpdatesAvailable($update_transient): void + private function watchPluginUpdatesAvailable($update_transient): void { // Check if update transient has the data we are interested in. if (!isset($update_transient->response) || !\is_array($update_transient->response)) { @@ -209,7 +216,7 @@ public function watchPluginUpdatesAvailable($update_transient): void } $subject = __('Plugin updates available', 'bc-security'); - $message = []; + $message = new Message(); foreach ($plugin_updates as $plugin_file => $plugin_update_data) { $plugin_data = Plugin::getPluginData($plugin_file); @@ -227,12 +234,9 @@ public function watchPluginUpdatesAvailable($update_transient): void ); } - $message[] = $plugin_message; + $message->addLine($plugin_message); } - // Now it is time to make sure the method is not invoked anymore. - remove_action('set_site_transient_update_plugins', [$this, 'watchPluginUpdatesAvailable'], 10); - // Send notification. if ($this->notify($subject, $message) !== false) { foreach ($plugin_updates as $plugin_file => $plugin_update_data) { @@ -248,7 +252,7 @@ public function watchPluginUpdatesAvailable($update_transient): void * * @param object $update_transient */ - public function watchThemeUpdatesAvailable($update_transient): void + private function watchThemeUpdatesAvailable($update_transient): void { // Check if update transient has the data we are interested in. if (!isset($update_transient->response) || !\is_array($update_transient->response)) { @@ -266,20 +270,19 @@ public function watchThemeUpdatesAvailable($update_transient): void } $subject = __('Theme updates available', 'bc-security'); - $message = []; + $message = new Message(); foreach ($theme_updates as $theme_slug => $theme_update_data) { $theme = wp_get_theme($theme_slug); - $message[] = \sprintf( - __('Theme "%1$s" has an update to version %2$s available.', 'bc-security'), - $theme, - $theme_update_data['new_version'] + $message->addLine( + \sprintf( + __('Theme "%1$s" has an update to version %2$s available.', 'bc-security'), + $theme, + $theme_update_data['new_version'], + ) ); } - // Now it is time to make sure the method is not invoked anymore. - remove_action('set_site_transient_update_themes', [$this, 'watchThemeUpdatesAvailable'], 10); - // Send notification. if ($this->notify($subject, $message) !== false) { foreach ($theme_updates as $theme_slug => $theme_update_data) { @@ -293,15 +296,17 @@ public function watchThemeUpdatesAvailable($update_transient): void /** * Send notification if known IP has been locked out as result of bad request. */ - public function watchBadRequestBanEvents(string $remote_address, string $uri, BanRule $ban_rule): void + private function watchBadRequestBanEvents(string $remote_address, string $uri, BanRule $ban_rule): void { if (\in_array($remote_address, $this->logger->getKnownIps(), true)) { $subject = __('Known IP locked out', 'bc-security'); - $message = \sprintf( - __('A known IP address %1$s has been locked out due to bad request rule "%2$s" after someone tried to access following URL: %3$s', 'bc-security'), - self::formatRemoteAddress($remote_address), - $ban_rule->getName(), - $uri + $message = new Message( + \sprintf( + __('A known IP address %1$s has been locked out due to bad request rule "%2$s" after someone tried to access following URL: %3$s', 'bc-security'), + self::formatRemoteAddress($remote_address), + $ban_rule->getName(), + $uri, + ) ); $this->notify($subject, $message); @@ -312,15 +317,17 @@ public function watchBadRequestBanEvents(string $remote_address, string $uri, Ba /** * Send notification if known IP has been locked out as result of failed login. */ - public function watchLockoutEvents(string $remote_address, string $username, int $duration): void + private function watchLockoutEvents(string $remote_address, string $username, int $duration): void { if (\in_array($remote_address, $this->logger->getKnownIps(), true)) { $subject = __('Known IP locked out', 'bc-security'); - $message = \sprintf( - __('A known IP address %1$s has been locked out for %2$d seconds after someone tried to log in with username "%3$s".', 'bc-security'), - self::formatRemoteAddress($remote_address), - $duration, - $username + $message = new Message( + \sprintf( + __('A known IP address %1$s has been locked out for %2$d seconds after someone tried to log in with username "%3$s".', 'bc-security'), + self::formatRemoteAddress($remote_address), + $duration, + $username, + ) ); $this->notify($subject, $message); @@ -331,14 +338,16 @@ public function watchLockoutEvents(string $remote_address, string $username, int /** * Send notification if user with admin privileges logged in. */ - public function watchWpLogin(string $username, WP_User $user): void + private function watchWpLogin(string $username, WP_User $user): void { if (Is::admin($user)) { $subject = __('Admin user login', 'bc-security'); - $message = \sprintf( - __('User "%1$s" with administrator privileges just logged in to your WordPress site from IP address %2$s.', 'bc-security'), - $username, - self::formatRemoteAddress($this->remote_address) + $message = new Message( + \sprintf( + __('User "%1$s" with administrator privileges just logged in to your WordPress site from IP address %2$s.', 'bc-security'), + $username, + self::formatRemoteAddress($this->remote_address), + ) ); $this->notify($subject, $message); @@ -349,15 +358,20 @@ public function watchWpLogin(string $username, WP_User $user): void /** * Send notification about single check that failed during checklist monitoring. */ - public function watchChecklistSingleCheckAlert(Check $check, CheckResult $result): void + private function watchChecklistSingleCheckAlert(Check $check, CheckResult $result): void { $subject = __('Checklist monitoring alert', 'bc-security'); - $preamble = [ - \sprintf(__('An issue has been found during checklist monitoring of "%s" check:', 'bc-security'), $check->getName()), - '', - ]; + $message = new Message( + \sprintf( + __('An issue has been found during checklist monitoring of "%s" check:', 'bc-security'), + $check->getName(), + ) + ); + + $message->addEmptyLine(); + $message->addLines($result->getMessage()); - $this->notify($subject, \array_merge($preamble, $result->getMessage())); + $this->notify($subject, $message); } @@ -366,16 +380,18 @@ public function watchChecklistSingleCheckAlert(Check $check, CheckResult $result * * @param array $issues Issues which triggered the alert. */ - public function watchChecklistMultipleChecksAlert(array $issues): void + private function watchChecklistMultipleChecksAlert(array $issues): void { $subject = __('Checklist monitoring alert', 'bc-security'); - $message = [ + $message = new Message( __('Following checks had failed during checklist monitoring:', 'bc-security'), - ]; + ); foreach ($issues as $issue) { - $message[] = ''; - $message[] = \sprintf("%s: %s", $issue['check']->getName(), $issue['result']->getMessageAsPlainText()); + $message + ->addEmptyLine() + ->addLine(\sprintf("%s: %s", $issue['check']->getName(), $issue['result']->getMessageAsPlainText())) + ; } $this->notify($subject, $message); @@ -386,12 +402,35 @@ public function watchChecklistMultipleChecksAlert(array $issues): void * Send email notification with given $subject and $message to recipients configured in plugin settings. * * @param string $subject - * @param string|string[] $message + * @param Message $message * * @return bool|null Null if there are no recipients configured. True if email has been sent, false otherwise. */ - private function notify(string $subject, $message): ?bool + private function notify(string $subject, Message $message): ?bool + { + if ($this->hasMessageBeenSent($message)) { + // Given message has been sent already. + return true; + } + + $status = empty($this->recipients) ? null : Mailman::send($this->recipients, $subject, $message); + + if ($status) { + $this->markMessageAsSent($message); + } + + return $status; + } + + + private function hasMessageBeenSent(Message $message): bool + { + return \in_array($message->getFingerprint(), $this->sent_notifications, true); + } + + + private function markMessageAsSent(Message $message): void { - return empty($this->recipients) ? null : Mailman::send($this->recipients, $subject, $message); + $this->sent_notifications[] = $message->getFingerprint(); } } diff --git a/classes/BlueChip/Security/Modules/Services/ReverseDnsLookup/Resolver.php b/classes/BlueChip/Security/Modules/Services/ReverseDnsLookup/Resolver.php index 39cb0e9..eb80943 100644 --- a/classes/BlueChip/Security/Modules/Services/ReverseDnsLookup/Resolver.php +++ b/classes/BlueChip/Security/Modules/Services/ReverseDnsLookup/Resolver.php @@ -44,7 +44,7 @@ public function deactivate(): void public function init(): void { // Register action for non-blocking hostname resolution. - add_action(self::RESOLVE_REMOTE_ADDRESS, [$this, 'resolveHostname'], 10, 3); + add_action(self::RESOLVE_REMOTE_ADDRESS, $this->resolveHostname(...), 10, 3); } @@ -57,7 +57,7 @@ public function init(): void * @param string $action Name of action to invoke with resolved hostname. * @param array $context Additional parameters that are passed to the action. */ - public function resolveHostname(string $ip_address, string $action, array $context): void + private function resolveHostname(string $ip_address, string $action, array $context): void { if (!empty($hostname = $this->resolveHostnameInForeground($ip_address))) { do_action($action, new Response($ip_address, $hostname, $context)); diff --git a/classes/BlueChip/Security/Modules/Tools/AdminPage.php b/classes/BlueChip/Security/Modules/Tools/AdminPage.php index 2556f63..afe2fe7 100644 --- a/classes/BlueChip/Security/Modules/Tools/AdminPage.php +++ b/classes/BlueChip/Security/Modules/Tools/AdminPage.php @@ -42,7 +42,7 @@ public function __construct(private Settings $settings) } - public function loadPage(): void + protected function loadPage(): void { $this->processActions(); } diff --git a/classes/BlueChip/Security/Plugin.php b/classes/BlueChip/Security/Plugin.php index 076ef85..362abfd 100644 --- a/classes/BlueChip/Security/Plugin.php +++ b/classes/BlueChip/Security/Plugin.php @@ -86,7 +86,7 @@ public function load(): void if (did_action('init')) { $this->init(); } else { - add_action('init', [$this, 'init'], 10, 0); + add_action('init', $this->init(...), 10, 0); } } @@ -97,7 +97,7 @@ public function load(): void * * @action https://developer.wordpress.org/reference/hooks/init/ */ - public function init(): void + private function init(): void { // Initialize all modules that require initialization. foreach ($this->modules as $module) { diff --git a/classes/BlueChip/Security/Setup/AdminPage.php b/classes/BlueChip/Security/Setup/AdminPage.php index 1320057..eef2501 100644 --- a/classes/BlueChip/Security/Setup/AdminPage.php +++ b/classes/BlueChip/Security/Setup/AdminPage.php @@ -33,7 +33,7 @@ public function __construct(Settings $settings) } - public function loadPage(): void + protected function loadPage(): void { $this->displaySettingsErrors(); @@ -92,7 +92,7 @@ public function initPage(): void $this->addSettingsSection( 'site-connection', _x('Site connection', 'Settings section title', 'bc-security'), - [$this, 'printSiteConnectionHint'] + $this->printSiteConnectionHint(...) ); $this->addSettingsField( Settings::CONNECTION_TYPE, @@ -105,7 +105,7 @@ public function initPage(): void $this->addSettingsSection( 'google-api', _x('Google API key', 'Settings section title', 'bc-security'), - [$this, 'printGoogleAPIHint'] + $this->printGoogleAPIHint(...) ); $this->addSettingsField( Settings::GOOGLE_API_KEY, @@ -115,7 +115,7 @@ public function initPage(): void } - public function printGoogleAPIHint(): void + private function printGoogleAPIHint(): void { echo '

'; echo sprintf( @@ -128,7 +128,7 @@ public function printGoogleAPIHint(): void } - public function printSiteConnectionHint(): void + private function printSiteConnectionHint(): void { echo '

'; echo esc_html__('Your server provides following information about remote addresses:', 'bc-security'); diff --git a/classes/BlueChip/Security/Setup/Settings.php b/classes/BlueChip/Security/Setup/Settings.php index 3672d93..f945a6f 100644 --- a/classes/BlueChip/Security/Setup/Settings.php +++ b/classes/BlueChip/Security/Setup/Settings.php @@ -40,11 +40,6 @@ class Settings extends CoreSettings /** * Sanitize connection type. Allow only expected values. - * - * @param string $value - * @param string $default - * - * @return string */ public static function sanitizeConnectionType(string $value, string $default): string { diff --git a/composer.json b/composer.json index f6d75b9..1af335b 100644 --- a/composer.json +++ b/composer.json @@ -31,11 +31,11 @@ "brain/monkey": "^2.3", "mockery/mockery": "^1.4", "php-parallel-lint/php-parallel-lint": "^1.3", - "phpunit/phpunit": "^9.5", + "phpunit/phpunit": "^10.5", "slevomat/coding-standard": "^8.0", "squizlabs/php_codesniffer": "^3.2", "szepeviktor/phpstan-wordpress": "^1.0", - "yoast/phpunit-polyfills": "^1.0" + "yoast/phpunit-polyfills": "^2.0" }, "autoload-dev": { "psr-4": { @@ -49,8 +49,9 @@ "phpcs": "phpcs", "phpstan": "phpstan analyze", "full-integration-tests": "phpunit --configuration tests/integration/phpunit.xml", - "integration-tests": "phpunit --configuration tests/integration/phpunit.xml --exclude-group external", - "unit-tests": "phpunit --configuration tests/unit/phpunit.xml", + "integration-tests": "phpunit --configuration tests/integration/phpunit.xml --exclude-group external --no-coverage", + "unit-tests": "phpunit --configuration tests/unit/phpunit.xml --no-coverage", + "unit-tests-with-coverage": "phpunit --configuration tests/unit/phpunit.xml", "ci": [ "@phpcs", "@phpstan", diff --git a/composer.lock b/composer.lock index 154bf98..b8cca25 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dceba5b1a6fadd1649161f76f0a3a64e", + "content-hash": "d94926db5d35ba415c0873cfc7c9513e", "packages": [ { "name": "composer/installers", @@ -155,16 +155,16 @@ "packages-dev": [ { "name": "antecedent/patchwork", - "version": "2.1.17", + "version": "2.1.28", "source": { "type": "git", "url": "https://github.com/antecedent/patchwork.git", - "reference": "df5aba175a44c2996ced4edf8ec9f9081b5348c0" + "reference": "6b30aff81ebadf0f2feb9268d3e08385cebcc08d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antecedent/patchwork/zipball/df5aba175a44c2996ced4edf8ec9f9081b5348c0", - "reference": "df5aba175a44c2996ced4edf8ec9f9081b5348c0", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/6b30aff81ebadf0f2feb9268d3e08385cebcc08d", + "reference": "6b30aff81ebadf0f2feb9268d3e08385cebcc08d", "shasum": "" }, "require": { @@ -185,7 +185,7 @@ } ], "description": "Method redefinition (monkey-patching) functionality for PHP.", - "homepage": "http://patchwork2.org/", + "homepage": "https://antecedent.github.io/patchwork/", "keywords": [ "aop", "aspect", @@ -197,9 +197,9 @@ ], "support": { "issues": "https://github.com/antecedent/patchwork/issues", - "source": "https://github.com/antecedent/patchwork/tree/2.1.17" + "source": "https://github.com/antecedent/patchwork/tree/2.1.28" }, - "time": "2021-10-21T14:22:43+00:00" + "time": "2024-02-06T09:26:11+00:00" }, { "name": "brain/monkey", @@ -349,76 +349,6 @@ }, "time": "2023-01-05T11:28:13+00:00" }, - { - "name": "doctrine/instantiator", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "require-dev": { - "doctrine/coding-standard": "^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], - "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], - "time": "2022-12-30T00:23:10+00:00" - }, { "name": "hamcrest/hamcrest-php", "version": "v2.0.1", @@ -472,16 +402,16 @@ }, { "name": "mockery/mockery", - "version": "1.6.6", + "version": "1.6.7", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "b8e0bb7d8c604046539c1115994632c74dcb361e" + "reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/b8e0bb7d8c604046539c1115994632c74dcb361e", - "reference": "b8e0bb7d8c604046539c1115994632c74dcb361e", + "url": "https://api.github.com/repos/mockery/mockery/zipball/0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06", + "reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06", "shasum": "" }, "require": { @@ -494,9 +424,7 @@ }, "require-dev": { "phpunit/phpunit": "^8.5 || ^9.6.10", - "psalm/plugin-phpunit": "^0.18.4", - "symplify/easy-coding-standard": "^11.5.0", - "vimeo/psalm": "^4.30" + "symplify/easy-coding-standard": "^12.0.8" }, "type": "library", "autoload": { @@ -553,7 +481,7 @@ "security": "https://github.com/mockery/mockery/security/advisories", "source": "https://github.com/mockery/mockery" }, - "time": "2023-08-09T00:03:52+00:00" + "time": "2023-12-10T02:24:34+00:00" }, { "name": "myclabs/deep-copy", @@ -616,25 +544,27 @@ }, { "name": "nikic/php-parser", - "version": "v4.17.1", + "version": "v5.0.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -642,7 +572,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -666,26 +596,27 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" }, - "time": "2023-08-13T19:53:39+00:00" + "time": "2024-03-05T20:51:40+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -726,9 +657,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -840,25 +777,27 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.3.2", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "f22b00cacd3b9addc2b07ff48290084503c48574" + "reference": "6105bdab2f26c0204fe90ecc53d5684754550e8f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/f22b00cacd3b9addc2b07ff48290084503c48574", - "reference": "f22b00cacd3b9addc2b07ff48290084503c48574", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/6105bdab2f26c0204fe90ecc53d5684754550e8f", + "reference": "6105bdab2f26c0204fe90ecc53d5684754550e8f", "shasum": "" }, "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "nikic/php-parser": "^4.13", "php": "^7.4 || ~8.0.0", "php-stubs/generator": "^0.8.3", "phpdocumentor/reflection-docblock": "^5.3", - "phpstan/phpstan": "^1.10.12", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan": "^1.10.49", + "phpunit/phpunit": "^9.5", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.11" }, "suggest": { "paragonie/sodium_compat": "Pure PHP implementation of libsodium", @@ -879,22 +818,22 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.3.2" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.4.3" }, - "time": "2023-10-14T10:08:05+00:00" + "time": "2024-02-11T18:56:19+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.2", + "version": "1.26.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "bcad8d995980440892759db0c32acae7c8e79442" + "reference": "231e3186624c03d7e7c890ec662b81e6b0405227" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bcad8d995980440892759db0c32acae7c8e79442", - "reference": "bcad8d995980440892759db0c32acae7c8e79442", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/231e3186624c03d7e7c890ec662b81e6b0405227", + "reference": "231e3186624c03d7e7c890ec662b81e6b0405227", "shasum": "" }, "require": { @@ -926,22 +865,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.26.0" }, - "time": "2023-09-26T12:28:12+00:00" + "time": "2024-02-23T16:05:55+00:00" }, { "name": "phpstan/phpstan", - "version": "1.10.39", + "version": "1.10.59", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "d9dedb0413f678b4d03cbc2279a48f91592c97c4" + "reference": "e607609388d3a6d418a50a49f7940e8086798281" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d9dedb0413f678b4d03cbc2279a48f91592c97c4", - "reference": "d9dedb0413f678b4d03cbc2279a48f91592c97c4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e607609388d3a6d418a50a49f7940e8086798281", + "reference": "e607609388d3a6d418a50a49f7940e8086798281", "shasum": "" }, "require": { @@ -990,39 +929,39 @@ "type": "tidelift" } ], - "time": "2023-10-17T15:46:26+00:00" + "time": "2024-02-20T13:59:13+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.29", + "version": "10.1.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" + "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", + "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-text-template": "^3.0", + "sebastian/code-unit-reverse-lookup": "^3.0", + "sebastian/complexity": "^3.0", + "sebastian/environment": "^6.0", + "sebastian/lines-of-code": "^2.0", + "sebastian/version": "^4.0", "theseer/tokenizer": "^1.2.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -1031,7 +970,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "10.1-dev" } }, "autoload": { @@ -1060,7 +999,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.14" }, "funding": [ { @@ -1068,32 +1007,32 @@ "type": "github" } ], - "time": "2023-09-19T04:57:46+00:00" + "time": "2024-03-12T15:33:41+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -1120,7 +1059,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" }, "funding": [ { @@ -1128,28 +1068,28 @@ "type": "github" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2023-08-31T06:24:48+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.1.1", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "suggest": { "ext-pcntl": "*" @@ -1157,7 +1097,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -1183,7 +1123,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" }, "funding": [ { @@ -1191,32 +1131,32 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2023-02-03T06:56:09+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -1242,7 +1182,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" }, "funding": [ { @@ -1250,32 +1191,32 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2023-08-31T14:07:24+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -1301,7 +1242,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" }, "funding": [ { @@ -1309,24 +1250,23 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2023-02-03T06:57:52+00:00" }, { "name": "phpunit/phpunit", - "version": "9.6.13", + "version": "10.5.13", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" + "reference": "20a63fc1c6db29b15da3bd02d4b6cf59900088a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/20a63fc1c6db29b15da3bd02d4b6cf59900088a7", + "reference": "20a63fc1c6db29b15da3bd02d4b6cf59900088a7", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -1336,27 +1276,26 @@ "myclabs/deep-copy": "^1.10.1", "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.28", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", - "sebastian/version": "^3.0.2" + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.5", + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-invoker": "^4.0", + "phpunit/php-text-template": "^3.0", + "phpunit/php-timer": "^6.0", + "sebastian/cli-parser": "^2.0", + "sebastian/code-unit": "^2.0", + "sebastian/comparator": "^5.0", + "sebastian/diff": "^5.0", + "sebastian/environment": "^6.0", + "sebastian/exporter": "^5.1", + "sebastian/global-state": "^6.0.1", + "sebastian/object-enumerator": "^5.0", + "sebastian/recursion-context": "^5.0", + "sebastian/type": "^4.0", + "sebastian/version": "^4.0" }, "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "ext-soap": "To be able to generate mocks based on WSDL files" }, "bin": [ "phpunit" @@ -1364,7 +1303,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.6-dev" + "dev-main": "10.5-dev" } }, "autoload": { @@ -1396,7 +1335,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.13" }, "funding": [ { @@ -1412,32 +1351,32 @@ "type": "tidelift" } ], - "time": "2023-09-19T05:39:22+00:00" + "time": "2024-03-12T15:37:41+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -1460,7 +1399,8 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" }, "funding": [ { @@ -1468,32 +1408,32 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T07:12:49+00:00" }, { "name": "sebastian/code-unit", - "version": "1.0.8", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -1516,7 +1456,7 @@ "homepage": "https://github.com/sebastianbergmann/code-unit", "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" }, "funding": [ { @@ -1524,32 +1464,32 @@ "type": "github" } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2023-02-03T06:58:43+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -1571,7 +1511,7 @@ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" }, "funding": [ { @@ -1579,34 +1519,36 @@ "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2023-02-03T06:59:15+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "2db5010a484d53ebf536087a70b4a5423c102372" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372", + "reference": "2db5010a484d53ebf536087a70b4a5423c102372", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -1645,7 +1587,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1" }, "funding": [ { @@ -1653,33 +1596,33 @@ "type": "github" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2023-08-14T13:18:12+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "68ff824baeae169ec9f2137158ee529584553799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", - "php": ">=7.3" + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.2-dev" } }, "autoload": { @@ -1702,7 +1645,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" }, "funding": [ { @@ -1710,33 +1654,33 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-21T08:37:17+00:00" }, { "name": "sebastian/diff", - "version": "4.0.5", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -1768,7 +1712,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" }, "funding": [ { @@ -1776,27 +1721,27 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T07:15:17+00:00" }, { "name": "sebastian/environment", - "version": "5.1.5", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951", + "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "suggest": { "ext-posix": "*" @@ -1804,7 +1749,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -1823,7 +1768,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -1831,7 +1776,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.0.1" }, "funding": [ { @@ -1839,34 +1785,34 @@ "type": "github" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2023-04-11T05:39:26+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -1908,7 +1854,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" }, "funding": [ { @@ -1916,38 +1863,35 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-03-02T07:17:12+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.6", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -1966,13 +1910,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" }, "funding": [ { @@ -1980,33 +1925,33 @@ "type": "github" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2024-03-02T07:19:19+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", - "php": ">=7.3" + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -2029,7 +1974,8 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" }, "funding": [ { @@ -2037,34 +1983,34 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-21T08:38:20+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2086,7 +2032,7 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" }, "funding": [ { @@ -2094,32 +2040,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2023-02-03T07:08:32+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -2141,7 +2087,7 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" }, "funding": [ { @@ -2149,32 +2095,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2023-02-03T07:06:18+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2204,62 +2150,7 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-02-03T06:07:39+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" }, "funding": [ { @@ -2267,32 +2158,32 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2023-02-03T07:05:40+00:00" }, { "name": "sebastian/type", - "version": "3.2.1", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -2315,7 +2206,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" }, "funding": [ { @@ -2323,29 +2214,29 @@ "type": "github" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2023-02-03T07:10:45+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -2368,7 +2259,7 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" }, "funding": [ { @@ -2376,36 +2267,36 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2023-02-07T11:34:05+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.13.4", + "version": "8.15.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "4b2af2fb17773656d02fbfb5d18024ebd19fe322" + "reference": "7d1d957421618a3803b593ec31ace470177d7817" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/4b2af2fb17773656d02fbfb5d18024ebd19fe322", - "reference": "4b2af2fb17773656d02fbfb5d18024ebd19fe322", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/7d1d957421618a3803b593ec31ace470177d7817", + "reference": "7d1d957421618a3803b593ec31ace470177d7817", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": "^1.23.0", - "squizlabs/php_codesniffer": "^3.7.1" + "phpstan/phpdoc-parser": "^1.23.1", + "squizlabs/php_codesniffer": "^3.9.0" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.10.26", - "phpstan/phpstan-deprecation-rules": "1.1.3", - "phpstan/phpstan-phpunit": "1.3.13", - "phpstan/phpstan-strict-rules": "1.5.1", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.8|10.2.6" + "phpstan/phpstan": "1.10.60", + "phpstan/phpstan-deprecation-rules": "1.1.4", + "phpstan/phpstan-phpunit": "1.3.16", + "phpstan/phpstan-strict-rules": "1.5.2", + "phpunit/phpunit": "8.5.21|9.6.8|10.5.11" }, "type": "phpcodesniffer-standard", "extra": { @@ -2429,7 +2320,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.13.4" + "source": "https://github.com/slevomat/coding-standard/tree/8.15.0" }, "funding": [ { @@ -2441,20 +2332,20 @@ "type": "tidelift" } ], - "time": "2023-07-25T10:28:55+00:00" + "time": "2024-03-09T15:20:58+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.2", + "version": "3.9.0", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", "shasum": "" }, "require": { @@ -2464,11 +2355,11 @@ "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "bin": [ - "bin/phpcs", - "bin/phpcbf" + "bin/phpcbf", + "bin/phpcs" ], "type": "library", "extra": { @@ -2483,35 +2374,58 @@ "authors": [ { "name": "Greg Sherwood", - "role": "lead" + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", "standards", "static analysis" ], "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" }, - "time": "2023-02-22T23:07:41+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-02-16T15:06:51+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" + "reference": "21bd091060673a1177ae842c0ef8fe30893114d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2", + "reference": "21bd091060673a1177ae842c0ef8fe30893114d2", "shasum": "" }, "require": { @@ -2519,9 +2433,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -2561,7 +2472,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0" }, "funding": [ { @@ -2577,26 +2488,26 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "szepeviktor/phpstan-wordpress", - "version": "v1.3.2", + "version": "v1.3.3", "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "b8516ed6bab7ec50aae981698ce3f67f1be2e45a" + "reference": "f7ff091331bc00c5688fe4ce0c4d51d06fa61553" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/b8516ed6bab7ec50aae981698ce3f67f1be2e45a", - "reference": "b8516ed6bab7ec50aae981698ce3f67f1be2e45a", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/f7ff091331bc00c5688fe4ce0c4d51d06fa61553", + "reference": "f7ff091331bc00c5688fe4ce0c4d51d06fa61553", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", "php-stubs/wordpress-stubs": "^4.7 || ^5.0 || ^6.0", - "phpstan/phpstan": "^1.10.30", + "phpstan/phpstan": "^1.10.31", "symfony/polyfill-php73": "^1.12.0" }, "require-dev": { @@ -2637,22 +2548,22 @@ ], "support": { "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.2" + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.3" }, - "time": "2023-10-16T17:23:56+00:00" + "time": "2024-02-26T13:55:50+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -2681,7 +2592,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -2689,34 +2600,33 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2024-03-03T12:36:25+00:00" }, { "name": "yoast/phpunit-polyfills", - "version": "1.0.4", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "3c621ff5429d2b1ff96dc5808ad6cde99d31ea4c" + "reference": "c758753e8f9dac251fed396a73c8305af3f17922" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/3c621ff5429d2b1ff96dc5808ad6cde99d31ea4c", - "reference": "3c621ff5429d2b1ff96dc5808ad6cde99d31ea4c", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/c758753e8f9dac251fed396a73c8305af3f17922", + "reference": "c758753e8f9dac251fed396a73c8305af3f17922", "shasum": "" }, "require": { - "php": ">=5.4", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "php": ">=5.6", + "phpunit/phpunit": "^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0" }, "require-dev": { - "yoast/yoastcs": "^2.2.1" + "yoast/yoastcs": "^2.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev", - "dev-develop": "1.x-dev" + "dev-main": "2.x-dev" } }, "autoload": { @@ -2750,7 +2660,7 @@ "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", "source": "https://github.com/Yoast/PHPUnit-Polyfills" }, - "time": "2022-11-16T09:07:52+00:00" + "time": "2023-06-06T20:28:24+00:00" } ], "aliases": [], diff --git a/tests/integration/phpunit.xml b/tests/integration/phpunit.xml index 91e8477..3670e5f 100644 --- a/tests/integration/phpunit.xml +++ b/tests/integration/phpunit.xml @@ -1,13 +1,5 @@ - + ./src/Cases diff --git a/tests/integration/src/Cases/Helpers/HaveIBeenPwnedTest.php b/tests/integration/src/Cases/Helpers/HaveIBeenPwnedTest.php index af1856b..5d7bd05 100644 --- a/tests/integration/src/Cases/Helpers/HaveIBeenPwnedTest.php +++ b/tests/integration/src/Cases/Helpers/HaveIBeenPwnedTest.php @@ -7,19 +7,19 @@ use BlueChip\Security\Helpers\HaveIBeenPwned; use BlueChip\Security\Tests\Integration\Constants; use BlueChip\Security\Tests\Integration\TestCase; +use PHPUnit\Framework\Attributes\Group; /** * Test integration with Pwned Passwords API. * * @internal These tests result in external requests to https://api.pwnedpasswords.com being made! */ -class HaveIBeenPwnedTest extends TestCase +final class HaveIBeenPwnedTest extends TestCase { /** * Test that pwned password is reported as such. - * - * @group external */ + #[Group('external')] public function testPwnedPassword() { $result = HaveIBeenPwned::hasPasswordBeenPwned(Constants::PWNED_PASSWORD); @@ -35,9 +35,8 @@ public function testPwnedPassword() /** * Test that safe password is reported as such. - * - * @group external */ + #[Group('external')] public function testSafePassword() { $result = HaveIBeenPwned::hasPasswordBeenPwned(Constants::SAFE_PASSWORD); diff --git a/tests/integration/src/Cases/Helpers/IsTest.php b/tests/integration/src/Cases/Helpers/IsTest.php index 6f298b3..d8ab84f 100644 --- a/tests/integration/src/Cases/Helpers/IsTest.php +++ b/tests/integration/src/Cases/Helpers/IsTest.php @@ -8,7 +8,7 @@ use BlueChip\Security\Helpers\Is; use BlueChip\Security\Tests\Integration\TestCase; -class IsTest extends TestCase +final class IsTest extends TestCase { /** * Test Is::admin() method and `bc-security/filter:is-admin` filter. diff --git a/tests/integration/src/Cases/Helpers/UtilsTest.php b/tests/integration/src/Cases/Helpers/UtilsTest.php index d8910e4..f8b200c 100644 --- a/tests/integration/src/Cases/Helpers/UtilsTest.php +++ b/tests/integration/src/Cases/Helpers/UtilsTest.php @@ -7,7 +7,7 @@ use BlueChip\Security\Helpers\Utils; use BlueChip\Security\Tests\Integration\TestCase; -class UtilsTest extends TestCase +final class UtilsTest extends TestCase { /** * Ensure that blocking access results in wp_die() being called with proper response code. diff --git a/tests/integration/src/Cases/Modules/Cron/JobTest.php b/tests/integration/src/Cases/Modules/Cron/JobTest.php index 0498394..ce66e19 100644 --- a/tests/integration/src/Cases/Modules/Cron/JobTest.php +++ b/tests/integration/src/Cases/Modules/Cron/JobTest.php @@ -7,7 +7,7 @@ use BlueChip\Security\Modules\Cron; use BlueChip\Security\Tests\Integration\TestCase; -class JobTest extends TestCase +final class JobTest extends TestCase { private const HOOK = 'test-job'; diff --git a/tests/integration/src/Cases/Modules/ExternalBlocklist/ManagerTest.php b/tests/integration/src/Cases/Modules/ExternalBlocklist/ManagerTest.php index 63833b3..f0c5fed 100644 --- a/tests/integration/src/Cases/Modules/ExternalBlocklist/ManagerTest.php +++ b/tests/integration/src/Cases/Modules/ExternalBlocklist/ManagerTest.php @@ -12,13 +12,14 @@ use BlueChip\Security\Setup\IpAddress; use BlueChip\Security\Tests\Integration\Constants; use BlueChip\Security\Tests\Integration\TestCase; +use PHPUnit\Framework\Attributes\Group; /** * Test integration of external blocklists. * * @internal These tests result in external requests! */ -class ManagerTest extends TestCase +final class ManagerTest extends TestCase { protected function prepareTest(): void { @@ -32,9 +33,8 @@ protected function prepareTest(): void /** * Test external blocklist populated with IP prefixes for Amazon Web Services. - * - * @group external */ + #[Group('external')] public function testAmazonWebServicesBlocklist() { // Refresh external blocklist. diff --git a/tests/integration/src/Cases/Modules/Hardening/ActiveTest.php b/tests/integration/src/Cases/Modules/Hardening/ActiveTest.php index 5c099d4..aab5b84 100644 --- a/tests/integration/src/Cases/Modules/Hardening/ActiveTest.php +++ b/tests/integration/src/Cases/Modules/Hardening/ActiveTest.php @@ -5,11 +5,12 @@ namespace BlueChip\Security\Tests\Integration\Cases\Modules\Hardening; use BlueChip\Security\Tests\Integration\Constants; +use PHPUnit\Framework\Attributes\Group; /** * Test hardening with all options on. */ -class ActiveTest extends TestCase +final class ActiveTest extends TestCase { protected function prepareTest(): void { @@ -40,9 +41,8 @@ public function testHardeningActive(): void /** * Test user editation with password change. - * - * @group external */ + #[Group('external')] public function testPasswordChange(): void { // Test strong password - should pass. diff --git a/tests/integration/src/Cases/Modules/Hardening/InactiveTest.php b/tests/integration/src/Cases/Modules/Hardening/InactiveTest.php index 895da35..47fef52 100644 --- a/tests/integration/src/Cases/Modules/Hardening/InactiveTest.php +++ b/tests/integration/src/Cases/Modules/Hardening/InactiveTest.php @@ -5,11 +5,12 @@ namespace BlueChip\Security\Tests\Integration\Cases\Modules\Hardening; use BlueChip\Security\Tests\Integration\Constants; +use PHPUnit\Framework\Attributes\Group; /** * Test hardening with all options off. */ -class InactiveTest extends TestCase +final class InactiveTest extends TestCase { protected function prepareTest(): void { @@ -40,9 +41,8 @@ public function testHardeningInactive(): void /** * Test user editation with password change. - * - * @group external */ + #[Group('external')] public function testPasswordChange(): void { // Test strong password - should pass. diff --git a/tests/integration/src/Cases/Modules/Login/GatekeeperTest.php b/tests/integration/src/Cases/Modules/Login/GatekeeperTest.php index f1d84ae..d0cbd25 100644 --- a/tests/integration/src/Cases/Modules/Login/GatekeeperTest.php +++ b/tests/integration/src/Cases/Modules/Login/GatekeeperTest.php @@ -9,7 +9,7 @@ use BlueChip\Security\Tests\Integration\Constants; use BlueChip\Security\Tests\Integration\TestCase; -class GatekeeperTest extends TestCase +final class GatekeeperTest extends TestCase { /** * @internal WP test suite already includes user with username `admin`, so there is no need to explicitly create it. diff --git a/tests/integration/src/TestCase.php b/tests/integration/src/TestCase.php index 7b33381..6a40802 100644 --- a/tests/integration/src/TestCase.php +++ b/tests/integration/src/TestCase.php @@ -11,6 +11,17 @@ */ abstract class TestCase extends \WP_UnitTestCase { + /** + * @internal Overriding this method from \WP_UnitTestCase_Base class allows our tests to run on PHPUnit 10. + * + * @link https://core.trac.wordpress.org/ticket/59486#comment:6 + */ + public function expectDeprecated(): void + { + return; + } + + /** * Allow to load the plugin with context customised for particular test suite. */ @@ -33,6 +44,6 @@ public function setUp(): void */ protected function prepareTest(): void { - // Empty by default, can be overriden in descendants. + // Empty by default, can be overridden in descendants. } } diff --git a/tests/static-analysis/bootstrap.php b/tests/static-analysis/bootstrap.php index 4f1a7ca..920b32c 100644 --- a/tests/static-analysis/bootstrap.php +++ b/tests/static-analysis/bootstrap.php @@ -7,6 +7,3 @@ define('AUTH_COOKIE', ''); define('SECURE_AUTH_COOKIE', ''); define('LOGGED_IN_COOKIE', ''); - -// Upcoming in WordPress 6.4: -function wp_admin_notice (string $message, array $args): void {} diff --git a/tests/unit/phpunit.xml b/tests/unit/phpunit.xml index b816476..a0a3f2d 100644 --- a/tests/unit/phpunit.xml +++ b/tests/unit/phpunit.xml @@ -1,17 +1,6 @@ - - - - ../../classes - + + @@ -21,4 +10,9 @@ ./src/Cases + + + ../../classes + + diff --git a/tests/unit/src/Cases/Helpers/IpAddressTest.php b/tests/unit/src/Cases/Helpers/IpAddressTest.php index 16f7105..fd49363 100644 --- a/tests/unit/src/Cases/Helpers/IpAddressTest.php +++ b/tests/unit/src/Cases/Helpers/IpAddressTest.php @@ -5,10 +5,12 @@ namespace BlueChip\Security\Tests\Unit\Cases\Helpers; use BlueChip\Security\Helpers\IpAddress; +use BlueChip\Security\Tests\Unit\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; -class IpAddressTest extends \BlueChip\Security\Tests\Unit\TestCase +final class IpAddressTest extends TestCase { - public function providePrefixMatchData(): array + public static function providePrefixMatchData(): array { return [ ['192.168.0.0/32', '192.168.0.0', true], @@ -26,9 +28,7 @@ public function providePrefixMatchData(): array ]; } - /** - * @dataProvider providePrefixMatchData - */ + #[DataProvider('providePrefixMatchData')] public function testIpPrefixMatch(string $cidr_range, string $ip_address, bool $result): void { $this->assertSame($result, IpAddress::matchesPrefix($ip_address, $cidr_range)); diff --git a/tests/unit/src/Cases/Helpers/IsTest.php b/tests/unit/src/Cases/Helpers/IsTest.php index e4866d0..72e47f6 100644 --- a/tests/unit/src/Cases/Helpers/IsTest.php +++ b/tests/unit/src/Cases/Helpers/IsTest.php @@ -8,8 +8,9 @@ use Brain\Monkey\Functions; use BlueChip\Security\Helpers\Hooks; use BlueChip\Security\Helpers\Is; +use BlueChip\Security\Tests\Unit\TestCase; -class IsTest extends \BlueChip\Security\Tests\Unit\TestCase +final class IsTest extends TestCase { /** * Ensure that `bc-security/filter:is-admin` fires when Is::admin() method is invoked. diff --git a/tests/unit/src/Cases/Helpers/PluginTest.php b/tests/unit/src/Cases/Helpers/PluginTest.php index b11842b..f82a949 100644 --- a/tests/unit/src/Cases/Helpers/PluginTest.php +++ b/tests/unit/src/Cases/Helpers/PluginTest.php @@ -5,10 +5,12 @@ namespace BlueChip\Security\Tests\Unit\Cases\Helpers; use BlueChip\Security\Helpers\Plugin; +use BlueChip\Security\Tests\Unit\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; -class PluginTest extends \BlueChip\Security\Tests\Unit\TestCase +final class PluginTest extends TestCase { - public function provideWordPressOrgUriData(): array + public static function provideWordPressOrgUriData(): array { return [ // WordPress 5.7 and older does not support UpdateURI header. @@ -31,9 +33,8 @@ public function provideWordPressOrgUriData(): array /** * Check Plugin::hasWordPressOrgUpdateUri() method. - * - * @dataProvider provideWordPressOrgUriData */ + #[DataProvider('provideWordPressOrgUriData')] public function testHasWordPressOrgUpdateUri(bool $value, string $plugin_basename, array $plugin_data): void { $this->assertSame($value, Plugin::hasWordPressOrgUpdateUri($plugin_basename, $plugin_data)); diff --git a/tests/unit/src/Cases/Modules/BadRequestsBanner/BuiltInRulesTest.php b/tests/unit/src/Cases/Modules/BadRequestsBanner/BuiltInRulesTest.php index 7f649dc..2875d18 100644 --- a/tests/unit/src/Cases/Modules/BadRequestsBanner/BuiltInRulesTest.php +++ b/tests/unit/src/Cases/Modules/BadRequestsBanner/BuiltInRulesTest.php @@ -5,75 +5,126 @@ namespace BlueChip\Security\Tests\Unit\Cases\Modules\BadRequestsBanner; use BlueChip\Security\Modules\BadRequestsBanner\BuiltInRules; +use BlueChip\Security\Tests\Unit\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; -class BuiltInRulesTest extends \BlueChip\Security\Tests\Unit\TestCase +final class BuiltInRulesTest extends TestCase { - public function provideUrisForBackupFilesRule(): array + public static function provideUris(): array { return [ - 'Backup file' => ['website-backup.zip', true], - 'Backup (back) file' => ['wp-config.php.back', true], - 'Backup (old) file' => ['script.php.old', true], - 'Backup (tmp) file' => ['some/important/file.tmp', true], - 'Image file' => ['dummy.png', false], - 'PHP file' => ['wp-config.php', false], - 'Readme.txt file' => ['wp-content/some-plugin/readme.txt', false], + 'Archive (tgz) file' => [ + 'data.tgz', + [ + BuiltInRules::ARCHIVE_FILES => true, + BuiltInRules::BACKUP_FILES => false, + BuiltInRules::PHP_FILES => false, + BuiltInRules::README_FILES => false, + ], + ], + 'Archive and backup (zip) file' => [ + 'website-backup.zip', + [ + BuiltInRules::ARCHIVE_FILES => true, + BuiltInRules::BACKUP_FILES => true, + BuiltInRules::PHP_FILES => false, + BuiltInRules::README_FILES => false, + ], + ], + 'Backup (back) file' => [ + 'wp-config.php.back', + [ + BuiltInRules::ARCHIVE_FILES => false, + BuiltInRules::BACKUP_FILES => true, + BuiltInRules::PHP_FILES => false, + BuiltInRules::README_FILES => false, + ], + ], + 'Backup (old) file' => [ + 'script.php.old', + [ + BuiltInRules::ARCHIVE_FILES => false, + BuiltInRules::BACKUP_FILES => true, + BuiltInRules::PHP_FILES => false, + BuiltInRules::README_FILES => false, + ], + ], + 'Backup (tmp) file' => [ + 'some/important/file.tmp', + [ + BuiltInRules::ARCHIVE_FILES => false, + BuiltInRules::BACKUP_FILES => true, + BuiltInRules::PHP_FILES => false, + BuiltInRules::README_FILES => false, + ], + ], + 'CSS asset' => [ + 'wp-content/theme/dummy/styles.css', + [ + BuiltInRules::ARCHIVE_FILES => false, + BuiltInRules::BACKUP_FILES => false, + BuiltInRules::PHP_FILES => false, + BuiltInRules::README_FILES => false, + ], + ], + 'Image file' => [ + 'plugin/non-existent/image.png', + [ + BuiltInRules::ARCHIVE_FILES => false, + BuiltInRules::BACKUP_FILES => false, + BuiltInRules::PHP_FILES => false, + BuiltInRules::README_FILES => false, + ], + ], + 'JS asset' => [ + 'wp-content/themes/dummy/script.js', + [ + BuiltInRules::ARCHIVE_FILES => false, + BuiltInRules::BACKUP_FILES => false, + BuiltInRules::PHP_FILES => false, + BuiltInRules::README_FILES => false, + ], + ], + 'PHP file' => [ + '_wp-config.php', + [ + BuiltInRules::ARCHIVE_FILES => false, + BuiltInRules::BACKUP_FILES => false, + BuiltInRules::PHP_FILES => true, + BuiltInRules::README_FILES => false, + ], + ], + 'Humans.txt file' => [ + 'humans.txt', + [ + BuiltInRules::ARCHIVE_FILES => false, + BuiltInRules::BACKUP_FILES => false, + BuiltInRules::PHP_FILES => false, + BuiltInRules::README_FILES => false, + ], + ], + 'Readme.txt file' => [ + 'wp-content/plugins/some-plugin/readme.txt', + [ + BuiltInRules::ARCHIVE_FILES => false, + BuiltInRules::BACKUP_FILES => false, + BuiltInRules::PHP_FILES => false, + BuiltInRules::README_FILES => true, + ], + ], ]; } - public function provideUrisForPhpFilesRule(): array + #[DataProvider('provideUris')] + public function testBuiltInRules(string $uri, array $results): void { - return [ - 'Backup file' => ['website-backup.zip', false], - 'Backup (back) file' => ['wp-config.php.back', false], - 'Backup (old) file' => ['script.php.old', false], - 'Backup (tmp) file' => ['some/important/file.tmp', false], - 'Image file' => ['dummy.png', false], - 'PHP file' => ['wp-config.php', true], - 'Readme.txt file' => ['wp-content/some-plugin/readme.txt', false], - ]; - } - - public function provideUrisForReadmeTxtFilesRule(): array - { - return [ - 'Backup file' => ['website-backup.zip', false], - 'Backup (back) file' => ['wp-config.php.back', false], - 'Backup (old) file' => ['script.php.old', false], - 'Backup (tmp) file' => ['some/important/file.tmp', false], - 'Image file' => ['dummy.png', false], - 'PHP file' => ['wp-config.php', false], - 'Readme.txt file' => ['wp-content/some-plugin/readme.txt', true], - ]; - } - - /** - * @dataProvider provideUrisForBackupFilesRule - */ - public function testBackupFilesRule(string $uri, bool $result): void - { - $ban_rule = BuiltInRules::get(BuiltInRules::BACKUP_FILES); - - $this->assertSame($ban_rule->matches($uri), $result); - } - - /** - * @dataProvider provideUrisForPhpFilesRule - */ - public function testPhpFilesRule(string $uri, bool $result): void - { - $ban_rule = BuiltInRules::get(BuiltInRules::PHP_FILES); - - $this->assertSame($ban_rule->matches($uri), $result); - } - - /** - * @dataProvider provideUrisForReadmeTxtFilesRule - */ - public function testReadmeTxtFilesRule(string $uri, bool $result): void - { - $ban_rule = BuiltInRules::get(BuiltInRules::README_FILES); - - $this->assertSame($ban_rule->matches($uri), $result); + foreach ($results as $rule_identifier => $result) { + $ban_rule = BuiltInRules::get($rule_identifier); + $this->assertSame( + $ban_rule->matches($uri), + $result, + sprintf($result ? 'Rule "%s" must not match URI %s' : 'Rule "%s" must match URI %s', $ban_rule->getName(), $uri) + ); + } } } diff --git a/tests/unit/src/Cases/Modules/Log/EventsManagerTest.php b/tests/unit/src/Cases/Modules/Log/EventsManagerTest.php index 5c5f3fc..489cf1c 100644 --- a/tests/unit/src/Cases/Modules/Log/EventsManagerTest.php +++ b/tests/unit/src/Cases/Modules/Log/EventsManagerTest.php @@ -5,8 +5,9 @@ namespace BlueChip\Security\Tests\Unit\Cases\Modules\Log; use BlueChip\Security\Modules\Log; +use BlueChip\Security\Tests\Unit\TestCase; -class EventsManagerTest extends \BlueChip\Security\Tests\Unit\TestCase +final class EventsManagerTest extends TestCase { /** * Ensure that mapping from event ID to event class is sane. diff --git a/tests/unit/src/Cases/Modules/Log/EventsMonitorTest.php b/tests/unit/src/Cases/Modules/Log/EventsMonitorTest.php index f812aa1..086490e 100644 --- a/tests/unit/src/Cases/Modules/Log/EventsMonitorTest.php +++ b/tests/unit/src/Cases/Modules/Log/EventsMonitorTest.php @@ -6,13 +6,12 @@ use Brain\Monkey\Actions; use BlueChip\Security\Modules\Log; +use BlueChip\Security\Modules\Log\EventsMonitor; +use BlueChip\Security\Tests\Unit\TestCase; -class EventsMonitorTest extends \BlueChip\Security\Tests\Unit\TestCase +final class EventsMonitorTest extends TestCase { - /** - * @var \BlueChip\Security\Modules\Log\EventsMonitor - */ - private $monitor; + private EventsMonitor $monitor; protected function setUp(): void @@ -27,7 +26,7 @@ public function testBadCookie(): void { Actions\expectDone(Log\Action::EVENT)->once()->with(\Mockery::type(Log\Events\AuthBadCookie::class)); - $this->monitor->logBadCookie(['username' => 'test-user']); + $this->runUnaccessibleMethod($this->monitor, 'logBadCookie', ['username' => 'test-user']); } @@ -40,7 +39,7 @@ public function testFailedLogin(): void 'get_error_message' => 'Test error message.', ]); - $this->monitor->logFailedLogin('test-user', $wp_error); + $this->runUnaccessibleMethod($this->monitor, 'logFailedLogin', 'test-user', $wp_error); } @@ -48,7 +47,7 @@ public function testLoginLockout(): void { Actions\expectDone(Log\Action::EVENT)->once()->with(\Mockery::type(Log\Events\LoginLockout::class)); - $this->monitor->logLockoutEvent('4.3.2.1', 'test-user', 600); + $this->runUnaccessibleMethod($this->monitor, 'logLockoutEvent', '4.3.2.1', 'test-user', 600); } @@ -56,6 +55,6 @@ public function testSuccessfulLogin(): void { Actions\expectDone(Log\Action::EVENT)->once()->with(\Mockery::type(Log\Events\LoginSuccessful::class)); - $this->monitor->logSuccessfulLogin('test-user'); + $this->runUnaccessibleMethod($this->monitor, 'logSuccessfulLogin', 'test-user'); } } diff --git a/tests/unit/src/Cases/Modules/Log/EventsTest.php b/tests/unit/src/Cases/Modules/Log/EventsTest.php index 2be3c5a..468ed79 100644 --- a/tests/unit/src/Cases/Modules/Log/EventsTest.php +++ b/tests/unit/src/Cases/Modules/Log/EventsTest.php @@ -5,8 +5,9 @@ namespace BlueChip\Security\Tests\Unit\Cases\Modules\Log; use BlueChip\Security\Modules\Log; +use BlueChip\Security\Tests\Unit\TestCase; -class EventsTest extends \BlueChip\Security\Tests\Unit\TestCase +final class EventsTest extends TestCase { /** * Ensure that every event type (class) has the necessary constants: ID and LEVEL. diff --git a/tests/unit/src/Cases/Setup/IpAddressTest.php b/tests/unit/src/Cases/Setup/IpAddressTest.php index b6c5255..0f8dd69 100644 --- a/tests/unit/src/Cases/Setup/IpAddressTest.php +++ b/tests/unit/src/Cases/Setup/IpAddressTest.php @@ -5,8 +5,10 @@ namespace BlueChip\Security\Tests\Unit\Cases\Setup; use BlueChip\Security\Setup\IpAddress; +use BlueChip\Security\Tests\Unit\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; -class IpAddressTest extends \BlueChip\Security\Tests\Unit\TestCase +final class IpAddressTest extends TestCase { protected function setUp(): void { @@ -30,7 +32,7 @@ protected function tearDown(): void } - public function provideRemoteAddressGetterData(): array + public static function provideRemoteAddressGetterData(): array { return [ 'no setting' => ['', '1.1.1.1'], @@ -42,16 +44,14 @@ public function provideRemoteAddressGetterData(): array } - /** - * @dataProvider provideRemoteAddressGetterData - */ + #[DataProvider('provideRemoteAddressGetterData')] public function testRemoteAddressGetter(string $connection_type, string $ip_address): void { $this->assertSame($ip_address, IpAddress::get($connection_type)); } - public function provideRemoteAddressGetterFallbackData(): array + public static function provideRemoteAddressGetterFallbackData(): array { return [ 'forwarded for' => [IpAddress::HTTP_X_FORWARDED_FOR, '1.1.1.1'], @@ -63,8 +63,8 @@ public function provideRemoteAddressGetterFallbackData(): array /** * Test the case when connection type is set to proxy, but there is actually no proxy info. - * @dataProvider provideRemoteAddressGetterFallbackData */ + #[DataProvider('provideRemoteAddressGetterFallbackData')] public function testRemoteAddressGetterFallback(string $connection_type, string $ip_address): void { // Make sure the requested connection info is empty. @@ -74,7 +74,7 @@ public function testRemoteAddressGetterFallback(string $connection_type, string } - public function provideRawRemoteAddressGetterData(): array + public static function provideRawRemoteAddressGetterData(): array { return [ 'no setting' => ['', ''], @@ -86,9 +86,7 @@ public function provideRawRemoteAddressGetterData(): array } - /** - * @dataProvider provideRawRemoteAddressGetterData - */ + #[DataProvider('provideRawRemoteAddressGetterData')] public function testRemoteRawAddressGetter(string $connection_type, string $ip_address): void { $this->assertSame($ip_address, IpAddress::getRaw($connection_type)); diff --git a/tests/unit/src/Cases/Setup/IpAddressValidationTest.php b/tests/unit/src/Cases/Setup/IpAddressValidationTest.php index 7d74a1e..06560dd 100644 --- a/tests/unit/src/Cases/Setup/IpAddressValidationTest.php +++ b/tests/unit/src/Cases/Setup/IpAddressValidationTest.php @@ -6,8 +6,9 @@ use BlueChip\Security\Setup\IpAddress; use BlueChip\Security\Tests\Unit\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; -class IpAddressValidationTest extends TestCase +final class IpAddressValidationTest extends TestCase { protected function setUp(): void { @@ -32,7 +33,7 @@ protected function tearDown(): void } - public function provideRemoteAddressGetterData(): array + public static function provideRemoteAddressGetterData(): array { return [ 'valid IP' => [IpAddress::REMOTE_ADDR, '23.23.23.23'], @@ -42,9 +43,7 @@ public function provideRemoteAddressGetterData(): array } - /** - * @dataProvider provideRemoteAddressGetterData - */ + #[DataProvider('provideRemoteAddressGetterData')] public function testRemoteAddressGetter(string $connection_type, ?string $ip_address): void { $this->assertSame($ip_address, IpAddress::get($connection_type)); diff --git a/tests/unit/src/TestCase.php b/tests/unit/src/TestCase.php index abddf68..862c6c7 100644 --- a/tests/unit/src/TestCase.php +++ b/tests/unit/src/TestCase.php @@ -47,7 +47,7 @@ protected function tearDown(): void /** * Run private or protected $method from $object with given $args and pass its return value. */ - protected function runUnaccessibleMethod(object $object, string $method, array $args) + protected function runUnaccessibleMethod(object $object, string $method, ...$args) { $method = (new \ReflectionClass(\get_class($object)))->getMethod($method); $method->setAccessible(true);