diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index c8e50c3..557eaf6 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -3,22 +3,30 @@ name: CI test suite on: pull_request: paths: + - '.github/workflows/**' - 'composer.*' - phpcs.xml - phpstan.neon - '**.php' push: paths: + - '.github/workflows/**' - 'composer.*' - phpcs.xml - phpstan.neon - '**.php' +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: strategy: matrix: - php-versions: ['8.0', '7.4', '7.3'] + php-versions: ['8.1', '8.0', '7.4', '7.3'] runs-on: ubuntu-latest @@ -35,7 +43,7 @@ jobs: run: composer validate - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest --classmap-authoritative + uses: ramsey/composer-install@v1 - name: Run CI test suite run: composer run-script ci diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b2e61c1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# BC Security Changelog + +## Upcoming version 0.19.0 (2022-06-02) + +### Added + +* PHP 8.1 is supported [#116](https://github.com/chesio/bc-security/issues/116). +* WordPress versions 5.9 and 6.0 are supported [#121](https://github.com/chesio/bc-security/issues/121) and [#127](https://github.com/chesio/bc-security/issues/127). +* An option to restrict login options has been implemented: login via email or login via username can be disabled [#123](https://github.com/chesio/bc-security/issues/123). +* [Changelog.md](CHANGELOG.md) has been added [#125](https://github.com/chesio/bc-security/issues/125). + +### Removed + +* "Check auth cookies" setting has been removed [#124](https://github.com/chesio/bc-security/issues/124). + +## Version 0.18.1 (2021-12-29) + +### Fixed + +* EOL dates for PHP versions in PHP version check has been updated: EOL date for PHP 7.3 has been removed, EOL date for PHP 8.1 has been added [#115](https://github.com/chesio/bc-security/issues/115). + +## Version 0.18.0 (2021-11-30) + +### Added + +* 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). + +## Older releases + +Notes on changes in all releases can be found [here](https://github.com/chesio/bc-security/releases). diff --git a/README.md b/README.md index 875afcb..00ab363 100644 --- a/README.md +++ b/README.md @@ -76,13 +76,13 @@ Since security measures for development instalations do not have to be as strict Basic checks cover common security practices. They do not require any information from third party sources to proceed and thus do not leak any information about your website: 1. Is backend editing of plugin and theme PHP files disabled? -1. Are directory listings disabled? -1. Is execution of PHP files from uploads directory forbidden? -1. Is display of PHP errors off by default? This check is only run in *live environment*. -1. Is error log file not publicly available? This check is only run if both `WP_DEBUG` and `WP_DEBUG_LOG` constants are set to true. -1. Are there no common usernames like admin or administrator on the system? -1. Are user passwords hashed with [more secure hashing algorithm](https://roots.io/improving-wordpress-password-security/) than MD5 used by [WordPress by default](https://core.trac.wordpress.org/ticket/21022)? -1. Is PHP version still supported? +2. Are directory listings disabled? +3. Is execution of PHP files from uploads directory forbidden? +4. Is display of PHP errors off by default? This check is only run in *live environment*. +5. Is error log file not publicly available? This check is only run if both `WP_DEBUG` and `WP_DEBUG_LOG` constants are set to true. +6. Are there no common usernames like admin or administrator on the system? +7. Are user passwords hashed with [more secure hashing algorithm](https://roots.io/improving-wordpress-password-security/) than MD5 used by [WordPress by default](https://core.trac.wordpress.org/ticket/21022)? +8. Is PHP version still supported? #### Advanced checks @@ -94,7 +94,7 @@ In the moment, list of installed plugins (but only those with _readme.txt_ file) WordPress core files verification is done in two phases: 1. Official md5 checksums from WordPress.org are used to determine if any of core files have been modified. -1. All files in root directory, _wp-admin_ directory (including subdirectories) and _wp-includes_ directory (including subdirectories) are checked against official checksums list in order to find out any unknown files. +2. All files in root directory, _wp-admin_ directory (including subdirectories) and _wp-includes_ directory (including subdirectories) are checked against official checksums list in order to find out any unknown files. The check uses the same checksums API as [`core verify-checksums`](https://developer.wordpress.org/cli/commands/core/verify-checksums/) command from [WP-CLI](https://wp-cli.org/). @@ -120,10 +120,11 @@ Both basic and advanced checks can be run manually from a dedicated page in back BC Security allows you to: 1. Disable pingbacks -1. Disable XML RPC methods that require authentication -1. Disable application passwords -1. Prevent usernames discovery via [REST API requests](https://developer.wordpress.org/rest-api/reference/users/) and [username enumeration](https://hackertarget.com/wordpress-user-enumeration/) -1. Check and/or validate user passwords using [Pwned Passwords](https://haveibeenpwned.com/Passwords) database and [API](https://haveibeenpwned.com/API/v2#PwnedPasswords) +2. Disable XML RPC methods that require authentication +3. Disable application passwords +4. Prevent usernames discovery via [REST API requests](https://developer.wordpress.org/rest-api/reference/users/) and [username eumeration](https://hackertarget.com/wordpress-user-enumeration/) +5. Disable login with email or login with username to reduce risk from brute-force or [credential stuffing attacks](https://owasp.org/www-community/attacks/Credential_stuffing). +6. Check and/or validate user passwords using [Pwned Passwords](https://haveibeenpwned.com/Passwords) database and [API](https://haveibeenpwned.com/API/v2#PwnedPasswords) #### Passwords check @@ -136,7 +137,7 @@ Passwords are validated on user creation, password change or password reset. If ### Login security 1. BC Security allows you to limit number of login attempts from single IP address. Implementation of this feature is heavily inspired by popular [Limit Login Attempts](https://wordpress.org/plugins/limit-login-attempts/) plugin with an extra feature of immediate blocking of specific usernames (like _admin_ or _administrator_). -1. BC Security offers an option to only display generic error message as a result of failed login attempt when wrong username, email or password is provided. +2. BC Security offers an option to only display generic error message as a result of failed login attempt when wrong username, email or password is provided. ### IP blacklist @@ -149,12 +150,12 @@ Out-dated records are automatically removed from the list by WP-Cron job schedul BC Security allows to send automatic email notification to configured recipients on following occasions: 1. WordPress update is available. -1. Plugin update is available. -1. Theme update is available. -1. User with administrator privileges has logged in. -1. Known IP address has been locked out (see note below). -1. [Checklist monitoring](#checklist-monitoring) triggers an alert. Note: there is one notification sent if any of basic checks fails, but separate notification is sent if any of advanced checks fails. -1. BC Security plugin has been deactivated. +2. Plugin update is available. +3. Theme update is available. +4. User with administrator privileges has logged in. +5. Known IP address has been locked out (see note below). +6. [Checklist monitoring](#checklist-monitoring) triggers an alert. Note: there is one notification sent if any of basic checks fails, but separate notification is sent if any of advanced checks fails. +7. BC Security plugin has been deactivated. Note: _Known IP address_ is an IP address from which a successful login attempt had been previously made. Information about successful login attempts is fetched from [event logs](#events-logging). @@ -165,8 +166,8 @@ You can mute all email notifications by setting constant `BC_SECURITY_MUTE_NOTIF BC Security logs both short and long lockout events (see [Login Security](#login-security) feature). Also, the following events triggered by WordPress core are logged: 1. Attempts to authenticate with bad cookie -1. Failed and successful login attempts -1. Requests that result in 404 page +2. Failed and successful login attempts +3. Requests that result in 404 page Logs are stored in database and can be viewed on backend. Logs are automatically deleted based on their age and overall size: by default no more than 20 thousands of records are kept and any log records older than 365 days are removed, but these limits can be configured. @@ -202,4 +203,4 @@ Some of the modules listed above come with settings panel. Further customization ## Alternatives (and why I do not use them) 1. [Wordfence Security](https://wordpress.org/plugins/wordfence/) - likely the current number one plugin for WordPress Security. My problem with Wordfence is that _"when you use [Wordfence], statistics about your website visitors are automatically collected"_ (see the full [Terms of Use and Privacy Policy](https://www.wordfence.com/terms-of-use-and-privacy-policy/)). In other words, in order to offer some of its great features, Wordfence is [phoning home](https://en.wikipedia.org/wiki/Phoning_home). -1. [All In One WP Security & Firewall](https://wordpress.org/plugins/all-in-one-wp-security-and-firewall/) - another very popular security plugin for WordPress. I have used AIOWPSF for quite some time; it has a lot of features, but also lot of small bugs (sometimes [not that small](https://sumofpwn.nl/advisory/2016/cross_site_scripting_in_all_in_one_wp_security___firewall_wordpress_plugin.html)). I [used to contribute](https://github.com/Arsenal21/all-in-one-wordpress-security/commits?author=chesio) to the plugin, but the codebase is [rather messy](https://github.com/Arsenal21/all-in-one-wordpress-security/pull/34) and after some time I got tired struggling with it. +2. [All In One WP Security & Firewall](https://wordpress.org/plugins/all-in-one-wp-security-and-firewall/) - another very popular security plugin for WordPress. I have used AIOWPSF for quite some time; it has a lot of features, but also lot of small bugs (sometimes [not that small](https://sumofpwn.nl/advisory/2016/cross_site_scripting_in_all_in_one_wp_security___firewall_wordpress_plugin.html)). I [used to contribute](https://github.com/Arsenal21/all-in-one-wordpress-security/commits?author=chesio) to the plugin, but the codebase is [rather messy](https://github.com/Arsenal21/all-in-one-wordpress-security/pull/34) and after some time I got tired struggling with it. diff --git a/bc-security.php b/bc-security.php index 427dc73..58da6c7 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.18.1 + * Version: 0.19.0 * Author: Česlav Przywara * Author URI: https://www.chesio.com * Requires PHP: 7.3 * Requires WP: 5.5 - * Tested up to: 5.8 + * Tested up to: 6.0 * Text Domain: bc-security * GitHub Plugin URI: https://github.com/chesio/bc-security * Update URI: https://github.com/chesio/bc-security @@ -53,13 +53,18 @@ // Register autoloader for this plugin. require_once __DIR__ . '/autoload.php'; -// Construct plugin instance. -$bc_security = new \BlueChip\Security\Plugin(__FILE__, $GLOBALS['wpdb']); +return call_user_func(function () { + // Construct plugin instance. + $bc_security = new \BlueChip\Security\Plugin(__FILE__, $GLOBALS['wpdb']); -// Register activation hook. -register_activation_hook(__FILE__, [$bc_security, 'activate']); -// Register deactivation hook. -register_deactivation_hook(__FILE__, [$bc_security, 'deactivate']); + // Register activation hook. + register_activation_hook(__FILE__, [$bc_security, 'activate']); + // Register deactivation hook. + register_deactivation_hook(__FILE__, [$bc_security, 'deactivate']); -// Load the plugin. -$bc_security->load(); + // Boot up the plugin immediately after all plugins are loaded. + add_action('plugins_loaded', [$bc_security, 'load'], 0, 0); + + // Return the instance. + return $bc_security; +}); diff --git a/classes/BlueChip/Security/Admin.php b/classes/BlueChip/Security/Admin.php index 243a14c..beff268 100644 --- a/classes/BlueChip/Security/Admin.php +++ b/classes/BlueChip/Security/Admin.php @@ -28,6 +28,7 @@ class Admin * Initialize admin area of the plugin. * * @param string $plugin_filename + * * @return self */ public function init(string $plugin_filename): self @@ -43,6 +44,7 @@ public function init(string $plugin_filename): self * Add a page to plugin dashboard menu. * * @param \BlueChip\Security\Core\Admin\AbstractPage $page + * * @return self */ public function addPage(Core\Admin\AbstractPage $page): self @@ -55,7 +57,7 @@ public function addPage(Core\Admin\AbstractPage $page): self /** * @action https://developer.wordpress.org/reference/hooks/admin_init/ */ - public function initAdminPages() + public function initAdminPages(): void { foreach ($this->pages as $page) { $page->initPage(); @@ -68,7 +70,7 @@ public function initAdminPages() * * @action https://developer.wordpress.org/reference/hooks/admin_menu/ */ - public function makeAdminMenu() + public function makeAdminMenu(): void { if (empty($this->pages)) { // No pages registered = no pages (no menu) to show. @@ -80,11 +82,11 @@ public function makeAdminMenu() // Add (main) menu page add_menu_page( - '', // obsolete as soon as page has subpages + '', // Page title is obsolete as soon as page has subpages. _x('BC Security', 'Dashboard menu item name', 'bc-security'), self::CAPABILITY, $main_page->getSlug(), - '', // obsolete as soon as page has subpages + '__return_empty_string', // Page content is obsolete as soon as page has subpages. Passing an empty string would prevent the callback being registered at all, but it breaks static analysis - see: https://core.trac.wordpress.org/ticket/52539 self::ICON ); @@ -110,8 +112,9 @@ public function makeAdminMenu() * * @filter https://developer.wordpress.org/reference/hooks/plugin_action_links_plugin_file/ * - * @param array $links - * @return array + * @param string[] $links + * + * @return string[] */ public function filterActionLinks(array $links): array { @@ -130,6 +133,7 @@ public function filterActionLinks(array $links): array * Format counter indicator for menu title for given $page. * * @param \BlueChip\Security\Core\Admin\AbstractPage $page + * * @return string */ private function renderCounter(Core\Admin\AbstractPage $page): string diff --git a/classes/BlueChip/Security/Core/Admin/AbstractPage.php b/classes/BlueChip/Security/Core/Admin/AbstractPage.php index 20c4b1c..81660e6 100644 --- a/classes/BlueChip/Security/Core/Admin/AbstractPage.php +++ b/classes/BlueChip/Security/Core/Admin/AbstractPage.php @@ -31,7 +31,7 @@ abstract class AbstractPage /** * Output page contents. */ - abstract public function printContents(); + abstract public function printContents(): void; /** @@ -87,7 +87,7 @@ public static function getPageUrl(): string * * @param string $page_hook */ - public function setPageHook(string $page_hook) + public function setPageHook(string $page_hook): void { add_action('load-' . $page_hook, [$this, 'loadPage']); } @@ -96,7 +96,7 @@ public function setPageHook(string $page_hook) /** * Run on admin initialization (in `admin_init` hook). */ - public function initPage() + public function initPage(): void { // By default do nothing. } @@ -107,7 +107,7 @@ public function initPage() * * @action https://developer.wordpress.org/reference/hooks/load-page_hook/ */ - public function loadPage() + public function loadPage(): void { // By default do nothing. } diff --git a/classes/BlueChip/Security/Core/Admin/CountablePage.php b/classes/BlueChip/Security/Core/Admin/CountablePage.php index 9b50154..c7b5948 100644 --- a/classes/BlueChip/Security/Core/Admin/CountablePage.php +++ b/classes/BlueChip/Security/Core/Admin/CountablePage.php @@ -18,7 +18,7 @@ trait CountablePage * * @param \BlueChip\Security\Modules\Countable $counter */ - protected function setCounter(\BlueChip\Security\Modules\Countable $counter) + protected function setCounter(\BlueChip\Security\Modules\Countable $counter): void { $this->counter = $counter; } @@ -27,7 +27,7 @@ protected function setCounter(\BlueChip\Security\Modules\Countable $counter) /** * Reset count(er). */ - protected function resetCount() + protected function resetCount(): void { $user = wp_get_current_user(); // Update $user's last view time for this page. diff --git a/classes/BlueChip/Security/Core/Admin/ListingPage.php b/classes/BlueChip/Security/Core/Admin/ListingPage.php index a524eaa..ee9de5b 100644 --- a/classes/BlueChip/Security/Core/Admin/ListingPage.php +++ b/classes/BlueChip/Security/Core/Admin/ListingPage.php @@ -15,7 +15,7 @@ trait ListingPage private $per_page_option_name; - abstract protected function initListTable(); + abstract protected function initListTable(): void; /** @@ -23,7 +23,7 @@ abstract protected function initListTable(); * * @param string $option_name */ - private function setPerPageOption(string $option_name) + private function setPerPageOption(string $option_name): void { $this->per_page_option_name = $option_name; @@ -36,7 +36,7 @@ private function setPerPageOption(string $option_name) /** * @link https://developer.wordpress.org/reference/functions/add_screen_option/ */ - private function addPerPageOption() + private function addPerPageOption(): void { add_screen_option('per_page', [ 'label' => __('Records', 'bc-security'), diff --git a/classes/BlueChip/Security/Core/Admin/PageWithAssets.php b/classes/BlueChip/Security/Core/Admin/PageWithAssets.php index 7a0b63c..9a1cac8 100644 --- a/classes/BlueChip/Security/Core/Admin/PageWithAssets.php +++ b/classes/BlueChip/Security/Core/Admin/PageWithAssets.php @@ -15,7 +15,7 @@ trait PageWithAssets /** * @param \BlueChip\Security\Core\AssetsManager $assets_manager */ - protected function useAssetsManager(AssetsManager $assets_manager) + protected function useAssetsManager(AssetsManager $assets_manager): void { $this->assets_manager = $assets_manager; } @@ -24,7 +24,7 @@ protected function useAssetsManager(AssetsManager $assets_manager) /** * @param array $assets JS assets to enqueue in [ handle => filename ] format. */ - protected function enqueueJsAssets(array $assets) + protected function enqueueJsAssets(array $assets): void { add_action('admin_enqueue_scripts', function () use ($assets) { foreach ($assets as $handle => $filename) { @@ -32,7 +32,7 @@ protected function enqueueJsAssets(array $assets) $handle, $this->assets_manager->getScriptFileUrl($filename), ['jquery'], - \filemtime($this->assets_manager->getScriptFilePath($filename)), + (string) \filemtime($this->assets_manager->getScriptFilePath($filename)), true ); } @@ -43,7 +43,7 @@ protected function enqueueJsAssets(array $assets) /** * @param array $assets CSS assets to enqueue in [ handle => filename ] format. */ - protected function enqueueCssAssets(array $assets) + protected function enqueueCssAssets(array $assets): void { add_action('admin_enqueue_scripts', function () use ($assets) { foreach ($assets as $handle => $filename) { @@ -51,7 +51,7 @@ protected function enqueueCssAssets(array $assets) $handle, $this->assets_manager->getStyleFileUrl($filename), [], - \filemtime($this->assets_manager->getStyleFilePath($filename)) + (string) \filemtime($this->assets_manager->getStyleFilePath($filename)) ); } }, 10, 0); diff --git a/classes/BlueChip/Security/Core/Admin/SettingsPage.php b/classes/BlueChip/Security/Core/Admin/SettingsPage.php index 2271c89..af0805f 100644 --- a/classes/BlueChip/Security/Core/Admin/SettingsPage.php +++ b/classes/BlueChip/Security/Core/Admin/SettingsPage.php @@ -38,7 +38,7 @@ trait SettingsPage * * @param \BlueChip\Security\Core\Settings $settings */ - protected function useSettings(\BlueChip\Security\Core\Settings $settings) + protected function useSettings(\BlueChip\Security\Core\Settings $settings): void { // Remember the settings. $this->settings = $settings; @@ -50,7 +50,7 @@ protected function useSettings(\BlueChip\Security\Core\Settings $settings) /** * Display settings errors via admin notices. */ - public function displaySettingsErrors() + public function displaySettingsErrors(): void { add_action('admin_notices', 'settings_errors'); } @@ -59,7 +59,7 @@ public function displaySettingsErrors() /** * Register setting. */ - public function registerSettings() + public function registerSettings(): void { register_setting($this->option_group, $this->option_name, [$this->settings, 'sanitize']); } @@ -68,7 +68,7 @@ public function registerSettings() /** * Unregister setting. */ - public function unregisterSettings() + public function unregisterSettings(): void { unregister_setting($this->option_group, $this->option_name); } @@ -81,7 +81,7 @@ public function unregisterSettings() * of SETTINGS_* constants as $page. * @param string $page */ - public function setSettingsPage(string $page) + public function setSettingsPage(string $page): void { $this->recent_page = $page; } @@ -118,7 +118,7 @@ protected function getFieldBaseProperties(string $key, $value = null): array * @param string $title * @param callable|null $callback [optional] */ - public function addSettingsSection(string $section, string $title, ?callable $callback = null) + public function addSettingsSection(string $section, string $title, ?callable $callback = null): void { if (!\is_string($this->recent_page)) { _doing_it_wrong(__METHOD__, 'No recent page set yet!', '0.1.0'); @@ -142,7 +142,7 @@ public function addSettingsSection(string $section, string $title, ?callable $ca * @param callable $callback Callback that produces form input for the field * @param array $args [Optional] Any extra arguments for $callback function */ - public function addSettingsField(string $key, string $title, callable $callback, array $args = []) + public function addSettingsField(string $key, string $title, callable $callback, array $args = []): void { if (!\is_string($this->recent_page)) { _doing_it_wrong(__METHOD__, 'No recent page set yet!', '0.1.0'); @@ -170,7 +170,7 @@ public function addSettingsField(string $key, string $title, callable $callback, /** * Output nonce, action and other hidden fields. */ - public function printSettingsFields() + public function printSettingsFields(): void { settings_fields($this->option_group); } @@ -179,7 +179,7 @@ public function printSettingsFields() /** * Output visible form fields. */ - public function printSettingsSections() + public function printSettingsSections(): void { if (!\is_string($this->recent_page)) { _doing_it_wrong(__METHOD__, 'No recent page set!', '0.1.0'); @@ -193,7 +193,7 @@ public function printSettingsSections() /** * Output form for settings manipulation. */ - protected function printSettingsForm() + protected function printSettingsForm(): void { echo '
'; diff --git a/classes/BlueChip/Security/Core/AssetsManager.php b/classes/BlueChip/Security/Core/AssetsManager.php index d9a7a7a..422b77c 100644 --- a/classes/BlueChip/Security/Core/AssetsManager.php +++ b/classes/BlueChip/Security/Core/AssetsManager.php @@ -31,6 +31,7 @@ public function __construct(string $plugin_filename) /** * @param string $filename Asset filename (ie. asset.js). + * * @return string Absolute path to the asset. */ public function getScriptFilePath(string $filename): string @@ -41,6 +42,7 @@ public function getScriptFilePath(string $filename): string /** * @param string $filename Asset filename (ie. asset.js). + * * @return string URL of the asset. */ public function getScriptFileUrl(string $filename): string @@ -51,6 +53,7 @@ public function getScriptFileUrl(string $filename): string /** * @param string $filename Asset filename (ie. asset.css). + * * @return string Absolute path to the asset. */ public function getStyleFilePath(string $filename): string @@ -61,6 +64,7 @@ public function getStyleFilePath(string $filename): string /** * @param string $filename Asset filename (ie. asset.css). + * * @return string URL of the asset. */ public function getStyleFileUrl(string $filename): string diff --git a/classes/BlueChip/Security/Core/ListTable.php b/classes/BlueChip/Security/Core/ListTable.php index 4def270..be6552a 100644 --- a/classes/BlueChip/Security/Core/ListTable.php +++ b/classes/BlueChip/Security/Core/ListTable.php @@ -80,7 +80,7 @@ public function __construct(string $url, string $per_page_option_name, array $ar * @param string $single The text to be used in notice if action affected single item. * @param string $plural The text to be used in notice if action affected multiple items. */ - protected function displayNotice(string $action, string $single, string $plural) + protected function displayNotice(string $action, string $single, string $plural): void { // Have any items been affected by given action? $result = \filter_input(INPUT_GET, $action, FILTER_VALIDATE_INT); @@ -104,6 +104,7 @@ protected function displayNotice(string $action, string $single, string $plural) * @param int $id * @param string $class * @param string $label + * * @return string */ protected function renderRowAction(string $action, int $id, string $class, string $label): string @@ -127,6 +128,7 @@ protected function renderRowAction(string $action, int $id, string $class, strin * Return content for "checkbox" column. * * @param array $item + * * @return string */ public function column_cb($item) // phpcs:ignore @@ -140,6 +142,7 @@ public function column_cb($item) // phpcs:ignore * * @param array $item * @param string $column_name + * * @return string */ public function column_default($item, $column_name) // phpcs:ignore @@ -152,16 +155,20 @@ public function column_default($item, $column_name) // phpcs:ignore * Display datetime database fields in local time. * * @param string $datetime Datetime string retrieved from database. - * @return string Date and time of $datetime formatted in local time. + * + * @return string Date and time of $datetime formatted in local time. Empty string if $datetime could not be parsed. */ public function formatDateAndTime(string $datetime): string { - return wp_date(self::DATETIME_FORMAT, MySQLDateTime::parseTimestamp($datetime)); + $timestamp = MySQLDateTime::parseTimestamp($datetime); + return ($timestamp !== null) ? (wp_date(self::DATETIME_FORMAT, $timestamp) ?: '') : ''; } /** * Output "no items" message. + * + * @return void */ public function no_items() // phpcs:ignore { diff --git a/classes/BlueChip/Security/Core/Settings.php b/classes/BlueChip/Security/Core/Settings.php index 54ad107..ad20153 100644 --- a/classes/BlueChip/Security/Core/Settings.php +++ b/classes/BlueChip/Security/Core/Settings.php @@ -9,7 +9,7 @@ * * @link https://developer.wordpress.org/plugins/settings/settings-api/ */ -abstract class Settings implements \ArrayAccess +abstract class Settings implements \ArrayAccess, \IteratorAggregate { /** * @var array Default values for all settings. Descendant classes should override it. @@ -48,6 +48,7 @@ public function __construct(string $option_name) * Get value of setting under key $name. * * @param string $name + * * @return mixed A null value is returned if $name is not a valid key. */ public function __get(string $name) @@ -67,7 +68,7 @@ public function __get(string $name) * @param string $name * @param mixed $value */ - public function __set(string $name, $value) + public function __set(string $name, $value): void { if (isset($this->data[$name])) { $this->update($name, $value); @@ -85,6 +86,7 @@ public function __set(string $name, $value) * @internal Implements ArrayAccess interface. * * @param string $offset + * * @return bool */ public function offsetExists($offset): bool @@ -99,6 +101,7 @@ public function offsetExists($offset): bool * @internal Implements ArrayAccess interface. * * @param string $offset + * * @return mixed A null value is returned if $offset is not a valid key. */ public function offsetGet($offset) @@ -115,7 +118,7 @@ public function offsetGet($offset) * @param string $offset * @param mixed $value */ - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { $this->update($offset, $value); } @@ -128,12 +131,20 @@ public function offsetSet($offset, $value) * * @param string $offset */ - public function offsetUnset($offset) + public function offsetUnset($offset): void { $this->update($offset, null); } + //// IteratorAggregate API ///////////////////////////////////////////////// + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->data); + } + + /** * Get option name. * @@ -160,6 +171,7 @@ public function get(): array * Set $data as option data. * * @param array $data + * * @return bool */ public function set(array $data): bool @@ -215,6 +227,7 @@ public function persist(): bool * * @param array $settings Input data to sanitize. * @param array $defaults [optional] If provided, used as default values for sanitization instead of local data. + * * @return array */ public function sanitize(array $settings, array $defaults = []): array @@ -244,6 +257,7 @@ public function sanitize(array $settings, array $defaults = []): array * * @param mixed $value * @param mixed $default + * * @return mixed */ protected static function sanitizeByType($value, $default) @@ -265,8 +279,9 @@ protected static function sanitizeByType($value, $default) /** * Parse a list of items separated by EOL character into array. Trim any empty lines (items). * - * @param array|string $list - * @return array + * @param string|string[] $list + * + * @return string[] */ protected static function parseList($list): array { @@ -279,9 +294,10 @@ protected static function parseList($list): array * * @param string $name * @param mixed $value + * * @return bool */ - protected function update(string $name, $value): bool + public function update(string $name, $value): bool { if (!isset($this->data[$name])) { // Cannot update, invalid setting name. @@ -298,7 +314,7 @@ protected function update(string $name, $value): bool $data[$name] = $value; } - // Sanitize new value and update cache + // Sanitize new value and update cache. $this->data = $this->sanitize($data); // Make changes permanent. return $this->persist(); @@ -316,7 +332,7 @@ protected function update(string $name, $value): bool * * @param callable $callback Callback that accepts up to three parameters: $old_value, $value, $option_name. */ - public function addUpdateHook(callable $callback) + public function addUpdateHook(callable $callback): void { add_action("update_option_{$this->option_name}", [$this, 'updateOption'], 10, 2); add_action("update_option_{$this->option_name}", $callback, 10, 3); @@ -325,10 +341,11 @@ public function addUpdateHook(callable $callback) /** * @action https://developer.wordpress.org/reference/hooks/update_option_option/ + * * @param array $old_value * @param array $new_value */ - public function updateOption(array $old_value, array $new_value) + public function updateOption(array $old_value, array $new_value): void { $this->data = $new_value; } diff --git a/classes/BlueChip/Security/Helpers/AdminNotices.php b/classes/BlueChip/Security/Helpers/AdminNotices.php index 7c51aba..8d85c2f 100644 --- a/classes/BlueChip/Security/Helpers/AdminNotices.php +++ b/classes/BlueChip/Security/Helpers/AdminNotices.php @@ -17,20 +17,17 @@ abstract class AdminNotices * * @link https://make.wordpress.org/core/2015/04/23/spinners-and-dismissible-admin-notices-in-4-2/ * - * @param array|string $message Single message or array of messages. + * @param string $message Message to display in admin notice. * @param string $type [optional] Type: 'notice-error', 'notice-warning', 'notice-success' or 'notice-info] (default). * @param bool $is_dismissible [optional] Should the notice be dismissible? Default is true. * @param bool $escape_html [optional] Should the content of message be HTML escaped? Default is true. */ - public static function add($message, string $type = self::INFO, bool $is_dismissible = true, bool $escape_html = true) + public static function add(string $message, string $type = self::INFO, bool $is_dismissible = true, bool $escape_html = true): void { $classes = \implode(' ', \array_filter(['notice', $type, $is_dismissible ? 'is-dismissible' : ''])); add_action('admin_notices', function () use ($message, $classes, $escape_html) { echo '
'; - $messages = \is_array($message) ? $message : [$message]; - \array_walk($messages, function ($msg) use ($escape_html) { - echo '

' . ($escape_html ? esc_html($msg) : $msg) . '

'; - }); + echo '

' . ($escape_html ? esc_html($message) : $message) . '

'; echo '
'; }); } diff --git a/classes/BlueChip/Security/Helpers/AjaxHelper.php b/classes/BlueChip/Security/Helpers/AjaxHelper.php index f871e09..3d09652 100644 --- a/classes/BlueChip/Security/Helpers/AjaxHelper.php +++ b/classes/BlueChip/Security/Helpers/AjaxHelper.php @@ -19,7 +19,7 @@ abstract class AjaxHelper * @param string $action * @param callable $handler */ - public static function addHandler(string $action, callable $handler) + public static function addHandler(string $action, callable $handler): void { add_action(self::WP_AJAX_PREFIX . $action, function () use ($action, $handler) { // Check AJAX referer for given action - will die if invalid. @@ -38,7 +38,7 @@ public static function addHandler(string $action, callable $handler) * @param string $action * @param array $data */ - public static function injectSetup(string $handle, string $object_name, string $action, array $data = []) + public static function injectSetup(string $handle, string $object_name, string $action, array $data = []): void { add_action('admin_enqueue_scripts', function () use ($handle, $object_name, $action, $data) { // Default localization data for every AJAX request. diff --git a/classes/BlueChip/Security/Helpers/FormHelper.php b/classes/BlueChip/Security/Helpers/FormHelper.php index 205f9cb..c2fdedd 100644 --- a/classes/BlueChip/Security/Helpers/FormHelper.php +++ b/classes/BlueChip/Security/Helpers/FormHelper.php @@ -6,6 +6,7 @@ abstract class FormHelper { /** * @link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea + * * @var int Default value of "cols" attribute of