From 80404fc413d9634e875cdcb27ca34d80ab496428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Mon, 6 Aug 2018 13:56:48 +0200 Subject: [PATCH 01/13] Update README.md Give credits to Vincent Driessen for his "A successful Git branching model" article. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index aee011b..d1a14f7 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ Some of the modules listed above come with settings panel. Further customization 1. Part of [psr/log](https://packagist.org/packages/psr/log) package codebase is shipped with the plugin. 1. [WordPress core integrity check](#wordpress-core-integrity-check) is heavily inspired by [Checksum Verifier](https://github.com/pluginkollektiv/checksum-verifier) plugin by Sergej Müller. 1. Some features (like "[Removed plugins check](#removed-plugins-check)") are inspired by [Wordfence Security](https://wordpress.org/plugins/wordfence/) from [Defiant](https://www.defiant.com/). +1. Big thanks to [Vincent Driessen](https://nvie.com/about/) for his "[A successful Git branching model](https://nvie.com/posts/a-successful-git-branching-model/)" article that I find particularly useful every time I do some work on BC Security. ## Alternatives (and why I do not use them) From c66de7069fd5064d3ccea39e4a40b86292401ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Mon, 6 Aug 2018 16:20:55 +0200 Subject: [PATCH 02/13] Restore develop as version number in develop branch. --- bc-security.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-security.php b/bc-security.php index dd0628f..83d6f95 100644 --- a/bc-security.php +++ b/bc-security.php @@ -3,7 +3,7 @@ * Plugin Name: BC Security * Plugin URI: https://github.com/chesio/bc-security * Description: Helps keeping WordPress websites secure. - * Version: 0.9.0 + * Version: develop * Author: Česlav Przywara * Author URI: https://www.chesio.com * Requires PHP: 7.0 From 07a2dea787a8f76187c45df179f07365cd616c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Mon, 6 Aug 2018 17:26:54 +0200 Subject: [PATCH 03/13] Allow to specify connection type by constant. Fixes #56. --- README.md | 4 ++++ classes/BlueChip/Security/Setup/AdminPage.php | 14 ++++++++++++++ classes/BlueChip/Security/Setup/Core.php | 13 +++++++++++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d1a14f7..056383b 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ Helps keeping WordPress websites secure. * BC Security has not been tested on WordPress multisite installation. * BC Security is primarily being developed for Apache webserver and Unix-like environments. +## Setup + +Several features of BC Security depends on the knowledge of remote IP address, so it is important that you let the plugin know how your server is connected to the Internet. You can either set connection type via _Setup_ page or with via `BC_SECURITY_CONNECTION_TYPE` constant. + ## Features ### Checklist diff --git a/classes/BlueChip/Security/Setup/AdminPage.php b/classes/BlueChip/Security/Setup/AdminPage.php index 83605ac..5bd6105 100644 --- a/classes/BlueChip/Security/Setup/AdminPage.php +++ b/classes/BlueChip/Security/Setup/AdminPage.php @@ -5,6 +5,7 @@ namespace BlueChip\Security\Setup; +use BlueChip\Security\Helpers\AdminNotices; use BlueChip\Security\Helpers\FormHelper; class AdminPage extends \BlueChip\Security\Core\Admin\AbstractPage @@ -34,6 +35,19 @@ public function __construct(Settings $settings) public function loadPage() { $this->displaySettingsErrors(); + + if (!empty($connection_type = Core::getConnectionType())) { + // Connection type is set via constant. + AdminNotices::add( + sprintf( + __('You have set BC_SECURITY_CONNECTION_TYPE to %s, therefore the setting below is ignored.', 'bc-security'), + $connection_type + ), + AdminNotices::WARNING, + false, // ~ not dismissible + false // ~ do not escape HTML + ); + } } diff --git a/classes/BlueChip/Security/Setup/Core.php b/classes/BlueChip/Security/Setup/Core.php index 5f2f646..5dd9c8c 100644 --- a/classes/BlueChip/Security/Setup/Core.php +++ b/classes/BlueChip/Security/Setup/Core.php @@ -23,13 +23,22 @@ public function __construct(Settings $settings) /** - * Get remote IP address according to configured connection type. + * @return string Connection type as set by `BC_SECURITY_CONNECTION_TYPE` constant or empty string if constant is not set. + */ + public static function getConnectionType(): string + { + return defined('BC_SECURITY_CONNECTION_TYPE') ? BC_SECURITY_CONNECTION_TYPE : ''; + } + + + /** + * Get remote IP address according to connection type configured either by constant or backend setting. * * @return string */ public function getRemoteAddress(): string { - return IpAddress::get($this->connection_type); + return IpAddress::get(self::getConnectionType() ?: $this->connection_type); } From 9ecbad8f7012d2a8e3dcccee92eb4f4631907849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Mon, 6 Aug 2018 21:35:37 +0200 Subject: [PATCH 04/13] Indicate count of monitored checks that failed in last run. Fixes #54. --- .../Security/Modules/Checklist/AdminPage.php | 13 +++- .../Security/Modules/Checklist/Check.php | 4 +- .../Checks/DisplayOfPhpErrorsIsOff.php | 2 +- .../Checks/ErrorLogNotPubliclyAccessible.php | 2 +- .../Security/Modules/Checklist/Manager.php | 59 +++++++++++++++---- 5 files changed, 63 insertions(+), 17 deletions(-) diff --git a/classes/BlueChip/Security/Modules/Checklist/AdminPage.php b/classes/BlueChip/Security/Modules/Checklist/AdminPage.php index 29072c6..62b6d58 100644 --- a/classes/BlueChip/Security/Modules/Checklist/AdminPage.php +++ b/classes/BlueChip/Security/Modules/Checklist/AdminPage.php @@ -80,6 +80,15 @@ public function loadPage() } + /** + * @return int Number of meaningful checks that are monitored and failed the last time they have been executed. + */ + public function getCount(): int + { + return count($this->checklist_manager->getChecks(['meaningful' => true, 'monitored' => true, 'status' => false])); + } + + /** * Output admin page. */ @@ -102,9 +111,9 @@ public function printContents() echo '
'; - $this->printBasicChecksSection($this->checklist_manager->getChecks(true, BasicCheck::class)); + $this->printBasicChecksSection($this->checklist_manager->getBasicChecks()); - $this->printAdvancedChecksSection($this->checklist_manager->getChecks(true, AdvancedCheck::class)); + $this->printAdvancedChecksSection($this->checklist_manager->getAdvancedChecks()); $this->printChecklistMonitoringSection(); diff --git a/classes/BlueChip/Security/Modules/Checklist/Check.php b/classes/BlueChip/Security/Modules/Checklist/Check.php index 8d4c3ab..096bf43 100644 --- a/classes/BlueChip/Security/Modules/Checklist/Check.php +++ b/classes/BlueChip/Security/Modules/Checklist/Check.php @@ -102,11 +102,11 @@ public function getResult(): CheckResult /** - * By default, every check makes sense. + * By default, every check is meaningful. * * @return bool */ - public function makesSense(): bool + public function isMeaningful(): bool { return true; } diff --git a/classes/BlueChip/Security/Modules/Checklist/Checks/DisplayOfPhpErrorsIsOff.php b/classes/BlueChip/Security/Modules/Checklist/Checks/DisplayOfPhpErrorsIsOff.php index 7384c65..f25a184 100644 --- a/classes/BlueChip/Security/Modules/Checklist/Checks/DisplayOfPhpErrorsIsOff.php +++ b/classes/BlueChip/Security/Modules/Checklist/Checks/DisplayOfPhpErrorsIsOff.php @@ -27,7 +27,7 @@ public function __construct() * * @return bool */ - public function makesSense(): bool + public function isMeaningful(): bool { return defined('WP_ENV') && (WP_ENV === 'production'); } diff --git a/classes/BlueChip/Security/Modules/Checklist/Checks/ErrorLogNotPubliclyAccessible.php b/classes/BlueChip/Security/Modules/Checklist/Checks/ErrorLogNotPubliclyAccessible.php index 725628a..8756de2 100644 --- a/classes/BlueChip/Security/Modules/Checklist/Checks/ErrorLogNotPubliclyAccessible.php +++ b/classes/BlueChip/Security/Modules/Checklist/Checks/ErrorLogNotPubliclyAccessible.php @@ -23,7 +23,7 @@ public function __construct() * * @return bool */ - public function makesSense(): bool + public function isMeaningful(): bool { return WP_DEBUG && WP_DEBUG_LOG; } diff --git a/classes/BlueChip/Security/Modules/Checklist/Manager.php b/classes/BlueChip/Security/Modules/Checklist/Manager.php index 295062c..b142871 100644 --- a/classes/BlueChip/Security/Modules/Checklist/Manager.php +++ b/classes/BlueChip/Security/Modules/Checklist/Manager.php @@ -51,7 +51,7 @@ public function init() $this->settings->addUpdateHook([$this, 'updateCronJobs']); // Hook into cron job execution. add_action(Modules\Cron\Jobs::CHECKLIST_CHECK, [$this, 'runBasicChecks'], 10, 0); - foreach ($this->getChecks(true, AdvancedCheck::class) as $advanced_check) { + foreach ($this->getAdvancedChecks() as $advanced_check) { add_action($advanced_check->getCronJobHook(), [$advanced_check, 'runInCron'], 10, 0); } // Register AJAX handler. @@ -103,23 +103,40 @@ public function constructChecks(\wpdb $wpdb): array /** * Return list of all implemented checks, optionally filtered. * - * @param bool $meaningful_only If true, only checks that make sense in current context are returned. - * @param string $class [optional] Return only checks of given class. + * @param array $filters [optional] Extra conditions to filter the list by: class (string), meaningful (boolean), + * monitored (boolean), status (null|boolean). * @return \BlueChip\Security\Modules\Checklist\Check[] */ - public function getChecks(bool $meaningful_only = false, string $class = ''): array + public function getChecks(array $filters = []): array { $checks = $this->checks; - if (!empty($class)) { + if (isset($filters['class'])) { + $class = $filters['class']; $checks = array_filter($checks, function (Check $check) use ($class): bool { return $check instanceof $class; }); } - if ($meaningful_only) { - $checks = array_filter($checks, function (Check $check): bool { - return $check->makesSense(); + if (isset($filters['meaningful'])) { + $is_meaningful = $filters['meaningful']; + $checks = array_filter($checks, function (Check $check) use ($is_meaningful): bool { + return $is_meaningful ? $check->isMeaningful() : !$check->isMeaningful(); + }); + } + + if (isset($filters['monitored'])) { + $monitored = $filters['monitored']; + $settings = $this->settings; + $checks = array_filter($checks, function (string $check_id) use ($monitored, $settings): bool { + return $monitored ? $settings[$check_id] : !$settings[$check_id]; + }, ARRAY_FILTER_USE_KEY); + } + + if (isset($filters['status'])) { + $status = $filters['status']; + $checks = array_filter($checks, function (Check $check) use ($status): bool { + return $check->getResult()->getStatus() === $status; }); } @@ -128,13 +145,33 @@ public function getChecks(bool $meaningful_only = false, string $class = ''): ar /** - * Run all basic checks that make sense in current context and are set to be monitored in non-interactive mode. + * @param bool $meaningful + * @return \BlueChip\Security\Modules\Checklist\Check[] + */ + public function getAdvancedChecks(bool $meaningful = true): array + { + return $this->getChecks(['meaningful' => $meaningful, 'class' => AdvancedCheck::class]); + } + + + /** + * @param bool $meaningful + * @return \BlueChip\Security\Modules\Checklist\Check[] + */ + public function getBasicChecks(bool $meaningful = true): array + { + return $this->getChecks(['meaningful' => $meaningful, 'class' => BasicCheck::class]); + } + + + /** + * 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() { - $checks = $this->getChecks(true, BasicCheck::class); + $checks = $this->getBasicChecks(); $issues = []; foreach ($checks as $check_id => $check) { @@ -197,7 +234,7 @@ public function runCheck() */ public function updateCronJobs() { - foreach ($this->getChecks(false, AdvancedCheck::class) as $check_id => $advanced_check) { + foreach ($this->getAdvancedChecks(false) as $check_id => $advanced_check) { if ($this->settings[$check_id]) { $this->cron_manager->activateJob($advanced_check->getCronJobHook()); } else { From 9f6d2b1514c608c1df4efcd297fccee582c20e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Tue, 7 Aug 2018 14:33:46 +0200 Subject: [PATCH 05/13] Flush object cache as part of transients clean-up. Fixes #57. --- .../BlueChip/Security/Helpers/Transients.php | 29 +++++++++++++++++++ classes/BlueChip/Security/Plugin.php | 8 +---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/classes/BlueChip/Security/Helpers/Transients.php b/classes/BlueChip/Security/Helpers/Transients.php index 0fd28a3..7090cc2 100644 --- a/classes/BlueChip/Security/Helpers/Transients.php +++ b/classes/BlueChip/Security/Helpers/Transients.php @@ -15,6 +15,7 @@ abstract class Transients */ const NAME_PREFIX = 'bc-security_'; + /** * Delete transient. * @@ -26,6 +27,32 @@ public static function deleteFromSite(string ...$key): bool return delete_site_transient(self::name($key)); } + + /** + * Remove all stored transients from database. Entire object cache is flushed as well, so use with caution. + * + * @link https://css-tricks.com/the-deal-with-wordpress-transients/ + * + * @param \wpdb $wpdb WordPress database access abstraction object + */ + public static function flush(\wpdb $wpdb) + { + $table_name = is_multisite() ? $wpdb->sitemeta : $wpdb->options; + + // First, delete all transients from database... + $wpdb->query( + sprintf( + "DELETE FROM {$table_name} WHERE (option_name LIKE '%s' OR option_name LIKE '%s')", + '_site_transient_' . self::NAME_PREFIX . '%', + '_site_transient_timeout_' . self::NAME_PREFIX . '%' + ) + ); + + // ...then flush object cache, because transients may be stored there as well. + wp_cache_flush(); + } + + /** * Get transient. * @@ -37,6 +64,7 @@ public static function getForSite(string ...$key) return get_site_transient(self::name($key)); } + /** * Set transient. * @@ -52,6 +80,7 @@ public static function setForSite($value, ...$args): bool return set_site_transient(self::name($args), $value, $expiration); } + /** * Create transient name from $key. * diff --git a/classes/BlueChip/Security/Plugin.php b/classes/BlueChip/Security/Plugin.php index 72e3cbc..0d73206 100644 --- a/classes/BlueChip/Security/Plugin.php +++ b/classes/BlueChip/Security/Plugin.php @@ -240,13 +240,7 @@ public function uninstall() } // Remove site transients set by plugin. - $this->wpdb->query( - sprintf( - "DELETE FROM {$this->wpdb->options} WHERE (option_name LIKE '%s' OR option_name LIKE '%s')", - '_site_transient_' . Helpers\Transients::NAME_PREFIX . '%', - '_site_transient_timeout_' . Helpers\Transients::NAME_PREFIX . '%' - ) - ); + Helpers\Transients::flush($this->wpdb); // Uninstall every module that requires it. foreach ($this->modules as $module) { From 9b6930af1477fdbc30b449903cb52faacf725207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Wed, 22 Aug 2018 12:39:31 +0200 Subject: [PATCH 06/13] Slightly refactor Plugin constructor. I find it cleaner to not use $this on right side of assignment in constructor. --- classes/BlueChip/Security/Plugin.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/classes/BlueChip/Security/Plugin.php b/classes/BlueChip/Security/Plugin.php index 0d73206..d0f0e05 100644 --- a/classes/BlueChip/Security/Plugin.php +++ b/classes/BlueChip/Security/Plugin.php @@ -43,17 +43,13 @@ public function __construct(string $plugin_filename, \wpdb $wpdb) $this->wpdb = $wpdb; // Read plugin settings. - $this->settings = $this->constructSettings(); + $this->settings = $settings = self::constructSettings(); // Get setup info. - $setup = new Setup\Core($this->settings['setup']); - - // IP addresses are at core interest within this plugin :) - $remote_address = $setup->getRemoteAddress(); - $server_address = $setup->getServerAddress(); + $setup = new Setup\Core($settings['setup']); // Construct modules. - $this->modules = $this->constructModules($wpdb, $remote_address, $server_address, $this->settings); + $this->modules = self::constructModules($wpdb, $setup->getRemoteAddress(), $setup->getServerAddress(), $settings); } @@ -62,7 +58,7 @@ public function __construct(string $plugin_filename, \wpdb $wpdb) * * @return array */ - private function constructSettings(): array + private static function constructSettings(): array { return [ 'cron-jobs' => new Modules\Cron\Settings('bc-security-cron-jobs'), @@ -85,7 +81,7 @@ private function constructSettings(): array * @param array $settings * @return array */ - private function constructModules(\wpdb $wpdb, string $remote_address, string $server_address, array $settings): array + private static function constructModules(\wpdb $wpdb, string $remote_address, string $server_address, array $settings): array { $hostname_resolver = new Modules\Services\ReverseDnsLookup\Resolver(); $cron_job_manager = new Modules\Cron\Manager($settings['cron-jobs']); From 92f733644a0ad7b5c03bd5811043d356c6a7fba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Mon, 3 Sep 2018 15:29:20 +0200 Subject: [PATCH 07/13] Fix Checklist\Manager::getAdvancedChecks() and ::getBasicChecks() methods. --- .../Security/Modules/Checklist/Manager.php | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/classes/BlueChip/Security/Modules/Checklist/Manager.php b/classes/BlueChip/Security/Modules/Checklist/Manager.php index b142871..a59d9a6 100644 --- a/classes/BlueChip/Security/Modules/Checklist/Manager.php +++ b/classes/BlueChip/Security/Modules/Checklist/Manager.php @@ -145,22 +145,30 @@ public function getChecks(array $filters = []): array /** - * @param bool $meaningful + * @param bool $only_meaningful * @return \BlueChip\Security\Modules\Checklist\Check[] */ - public function getAdvancedChecks(bool $meaningful = true): array + public function getAdvancedChecks(bool $only_meaningful = true): array { - return $this->getChecks(['meaningful' => $meaningful, 'class' => AdvancedCheck::class]); + $filters = ['class' => AdvancedCheck::class]; + if ($only_meaningful) { + $filters['meaningful'] = true; + } + return $this->getChecks($filters); } /** - * @param bool $meaningful + * @param bool $only_meaningful * @return \BlueChip\Security\Modules\Checklist\Check[] */ - public function getBasicChecks(bool $meaningful = true): array + public function getBasicChecks(bool $only_meaningful = true): array { - return $this->getChecks(['meaningful' => $meaningful, 'class' => BasicCheck::class]); + $filters = ['class' => BasicCheck::class]; + if ($only_meaningful) { + $filters['meaningful'] = true; + } + return $this->getChecks($filters); } From 47fb9ed83260c750c873870c4bda87ee184861bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Thu, 22 Nov 2018 13:18:06 +0100 Subject: [PATCH 08/13] Append URL to plugin changelog in plugin update notifications. Fixes #58. --- classes/BlueChip/Security/Helpers/Hooks.php | 8 ++++ classes/BlueChip/Security/Helpers/Plugin.php | 39 ++++++++++++++++++- .../Modules/Notifications/Watchman.php | 17 ++++++-- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/classes/BlueChip/Security/Helpers/Hooks.php b/classes/BlueChip/Security/Helpers/Hooks.php index d8bf187..0c651ff 100644 --- a/classes/BlueChip/Security/Helpers/Hooks.php +++ b/classes/BlueChip/Security/Helpers/Hooks.php @@ -16,4 +16,12 @@ interface Hooks * @see \BlueChip\Security\Helpers\Is::admin() */ const IS_ADMIN = 'bc-security/filter:is-admin'; + + + /** + * Filter: allows to change plugin's changelog URL. + * + * @see \BlueChip\Security\Helpers\Plugin::getChangelogUrl() + */ + const PLUGIN_CHANGELOG_URL = 'bc-security/filter:plugin-changelog-url'; } diff --git a/classes/BlueChip/Security/Helpers/Plugin.php b/classes/BlueChip/Security/Helpers/Plugin.php index 5b79504..30e32f7 100644 --- a/classes/BlueChip/Security/Helpers/Plugin.php +++ b/classes/BlueChip/Security/Helpers/Plugin.php @@ -14,12 +14,35 @@ abstract class Plugin */ const CHECKSUMS_API_URL_BASE = 'https://downloads.wordpress.org/plugin-checksums/'; - /** - * @var string + * @var string URL of Plugins Directory. */ const PLUGINS_DIRECTORY_URL = 'https://wordpress.org/plugins/'; + /** + * @var string Path (although not technically) to changelog page relative to URL of plugin homepage at Plugins Directory. + */ + const PLUGINS_DIRECTORY_CHANGELOG_PATH = '#developers'; + + + /** + * @param string $plugin_basename + * @return string URL of the plugin changelog page or empty string, if it cannot be determined. + */ + public static function getChangelogUrl(string $plugin_basename): string + { + // By default, changelog URL is unknown. + $url = ''; + + if (self::hasReadmeTxt($plugin_basename)) { + // Assume that any plugin with readme.txt comes from Plugins Directory. + $url = self::getDirectoryUrl($plugin_basename) . self::PLUGINS_DIRECTORY_CHANGELOG_PATH; + } + + // Allow the changelog URL to be filtered. + return apply_filters(Hooks::PLUGIN_CHANGELOG_URL, $url, $plugin_basename); + } + /** * @param string $plugin_basename @@ -103,6 +126,18 @@ public static function getPluginsInstalledFromWordPressOrg(): array } + /** + * @internal Only use in admin (back-end) context. + * @param string $plugin_basename + * @return array + */ + public static function getPluginData(string $plugin_basename): array + { + // Note: get_plugin_data() function is only defined in admin. + return get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin_basename); + } + + /** * Get absolute path to plugin directory for given $plugin_basename (ie. "bc-security/bc-security.php"). * diff --git a/classes/BlueChip/Security/Modules/Notifications/Watchman.php b/classes/BlueChip/Security/Modules/Notifications/Watchman.php index 0a96554..05aaf88 100644 --- a/classes/BlueChip/Security/Modules/Notifications/Watchman.php +++ b/classes/BlueChip/Security/Modules/Notifications/Watchman.php @@ -6,6 +6,7 @@ namespace BlueChip\Security\Modules\Notifications; use BlueChip\Security\Helpers\Is; +use BlueChip\Security\Helpers\Plugin; use BlueChip\Security\Helpers\Transients; use BlueChip\Security\Modules; use BlueChip\Security\Modules\Log\Logger; @@ -223,14 +224,22 @@ public function watchPluginUpdatesAvailable($update_transient) $message = []; foreach ($plugin_updates as $plugin_file => $plugin_update_data) { - // Note: get_plugin_data() function is only defined in admin, - // but it seems that it is always available in this context... - $plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin_file); - $message[] = sprintf( + $plugin_data = Plugin::getPluginData($plugin_file); + $plugin_message = sprintf( __('Plugin "%1$s" has an update to version %2$s available.', 'bc-security'), $plugin_data['Name'], $plugin_update_data->new_version ); + + if (!empty($plugin_changelog_url = Plugin::getChangelogUrl($plugin_file))) { + // Append link to changelog, if available. + $plugin_message .= ' ' . sprintf( + __('Changelog: %1$s', 'bc-security'), + $plugin_changelog_url + ); + } + + $message[] = $plugin_message; } // Now it is time to make sure the method is not invoked anymore. From 8a531745ff79b86237c7fb0a4f59b917fe695d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Thu, 22 Nov 2018 14:24:36 +0100 Subject: [PATCH 09/13] Add new (empty yet) admin page "Tools". --- .../Security/Modules/Tools/AdminPage.php | 31 +++++++++++++++++++ classes/BlueChip/Security/Plugin.php | 1 + 2 files changed, 32 insertions(+) create mode 100644 classes/BlueChip/Security/Modules/Tools/AdminPage.php diff --git a/classes/BlueChip/Security/Modules/Tools/AdminPage.php b/classes/BlueChip/Security/Modules/Tools/AdminPage.php new file mode 100644 index 0000000..47a03dc --- /dev/null +++ b/classes/BlueChip/Security/Modules/Tools/AdminPage.php @@ -0,0 +1,31 @@ +page_title = _x('Tools', 'Dashboard page title', 'bc-security'); + $this->menu_title = _x('Tools', 'Dashboard menu item name', 'bc-security'); + } + + + public function printContents() + { + echo '
'; + echo '

' . esc_html($this->page_title) . '

'; + echo '
'; + } +} diff --git a/classes/BlueChip/Security/Plugin.php b/classes/BlueChip/Security/Plugin.php index d0f0e05..f63eb21 100644 --- a/classes/BlueChip/Security/Plugin.php +++ b/classes/BlueChip/Security/Plugin.php @@ -176,6 +176,7 @@ public function init() $this->settings['log'], $this->modules['logger'] )) + ->addPage(new Modules\Tools\AdminPage()) ; } } From a6af4dd5f1b1f830b353df6f97f2988caed9ebea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Thu, 22 Nov 2018 16:30:34 +0100 Subject: [PATCH 10/13] Implement export/import/reset settings actions. --- classes/BlueChip/Security/Core/Settings.php | 38 ++- .../Security/Modules/Tools/AdminPage.php | 216 +++++++++++++++++- classes/BlueChip/Security/Plugin.php | 2 +- 3 files changed, 253 insertions(+), 3 deletions(-) diff --git a/classes/BlueChip/Security/Core/Settings.php b/classes/BlueChip/Security/Core/Settings.php index 322f369..44199f2 100644 --- a/classes/BlueChip/Security/Core/Settings.php +++ b/classes/BlueChip/Security/Core/Settings.php @@ -148,6 +148,42 @@ public function getOptionName(): string } + /** + * Get option data. + * + * @return array + */ + public function get(): array + { + return $this->data; + } + + + /** + * Set $data as option data. + * + * @param array $data + * @return bool + */ + public function set(array $data): bool + { + $this->data = $this->sanitize($data); + return $this->persist(); + } + + + /** + * Reset option data. + * + * @return bool + */ + public function reset(): bool + { + $this->data = static::DEFAULTS; + return $this->persist(); + } + + /** * Sanitize $settings array: only keep known keys, provide default values for missing keys. * @@ -221,7 +257,7 @@ protected static function parseList($list): array /** * Persist the value of data into database. * - * @return bool + * @return bool True, if settings have been updated (= changed), false otherwise. */ protected function persist(): bool { diff --git a/classes/BlueChip/Security/Modules/Tools/AdminPage.php b/classes/BlueChip/Security/Modules/Tools/AdminPage.php index 47a03dc..9027e53 100644 --- a/classes/BlueChip/Security/Modules/Tools/AdminPage.php +++ b/classes/BlueChip/Security/Modules/Tools/AdminPage.php @@ -5,6 +5,9 @@ namespace BlueChip\Security\Modules\Tools; +use BlueChip\Security\Helpers\AdminNotices; +use BlueChip\Security\Setup; + class AdminPage extends \BlueChip\Security\Core\Admin\AbstractPage { /** @@ -12,13 +15,43 @@ class AdminPage extends \BlueChip\Security\Core\Admin\AbstractPage */ const SLUG = 'bc-security-tools'; + /** + * @var string + */ + const EXPORT_ACTION = 'export-settings'; /** + * @var string */ - public function __construct() + const IMPORT_ACTION = 'import-settings'; + + /** + * @var string + */ + const RESET_ACTION = 'reset-settings'; + + + /** + * @var \BlueChip\Security\Core\Settings[] Plugin settings collection + */ + private $settings = []; + + + /** + * @param \BlueChip\Security\Core\Settings[] $settings Plugin settings collection + */ + public function __construct(array $settings) { $this->page_title = _x('Tools', 'Dashboard page title', 'bc-security'); $this->menu_title = _x('Tools', 'Dashboard menu item name', 'bc-security'); + + $this->settings = $settings; + } + + + public function loadPage() + { + $this->processActions(); } @@ -26,6 +59,187 @@ public function printContents() { echo '
'; echo '

' . esc_html($this->page_title) . '

'; + + $this->printExportForm(); + echo '
'; + $this->printImportForm(); + echo '
'; + $this->printResetForm(); + echo '
'; } + + + private function printExportForm() + { + echo '

' . esc_html__('Export settings', 'bc-security') . '

'; + echo '

' . esc_html__('Create JSON file with plugin settings that can be used as backup or to clone the settings to another installation.', 'bc-security') . '

'; + echo ''; + // Form nonce + wp_nonce_field(self::EXPORT_ACTION, self::NONCE_NAME); + // Submit button + submit_button(__('Export settings', 'bc-security'), 'primary', self::EXPORT_ACTION, true); + echo ''; + } + + + private function printImportForm() + { + echo '

' . esc_html__('Import settings', 'bc-security') . '

'; + echo '

' . esc_html__('Import only JSON files created with the same version of the plugin!', 'bc-security') . '

'; + echo '
'; + // Form nonce + wp_nonce_field(self::IMPORT_ACTION, self::NONCE_NAME); + // File input + echo '
'; + echo ''; + // Submit button + submit_button(__('Import settings', 'bc-security'), 'primary', self::IMPORT_ACTION, true); + echo '
'; + } + + + private function printResetForm() + { + echo '

' . esc_html__('Reset settings', 'bc-security') . '

'; + echo '

'; + echo sprintf( + /* translators: %s: link to plugin setup page */ + esc_html__('Set all plugin settings (including %s) back to their default values.', 'bc-security'), + sprintf( + '%s', + Setup\AdminPage::getPageUrl(), + esc_html__('connection type', 'bc-security') + ) + ); + echo '

'; + echo '
'; + // Form nonce + wp_nonce_field(self::RESET_ACTION, self::NONCE_NAME); + // Submit button + submit_button(__('Reset settings', 'bc-security'), 'primary', self::RESET_ACTION, true); + echo '
'; + } + + + /** + * Dispatch any action that is indicated by POST data (form submission). + */ + private function processActions() + { + $nonce = filter_input(INPUT_POST, self::NONCE_NAME, FILTER_SANITIZE_STRING); + if (empty($nonce)) { + // No nonce, no action. + return; + } + + if (isset($_POST[self::EXPORT_ACTION]) && wp_verify_nonce($nonce, self::EXPORT_ACTION)) { + // Export settings to a file. + $this->processExportAction(); + } + + if (isset($_POST[self::IMPORT_ACTION]) && wp_verify_nonce($nonce, self::IMPORT_ACTION)) { + // Import settings from provided file. + $this->processImportAction(); + } + + if (isset($_POST[self::RESET_ACTION]) && wp_verify_nonce($nonce, self::RESET_ACTION)) { + // Reset all settings to default values. + $this->processResetAction(); + } + } + + + private function processExportAction() + { + $export = []; + + foreach ($this->settings as $settings) { + $export[$settings->getOptionName()] = $settings->get(); + } + + // Send headers. + $file_name = 'bc-security-export-' . date('Y-m-d') . '.json'; + header("Content-Disposition: attachment; filename={$file_name}"); + header("Content-Type: application/json; charset=utf-8"); + + // Send content. + echo json_encode($export, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + exit; + } + + + private function processImportAction() + { + $import_file = $_FILES['import-file']; + + // Validate file upload. + if (empty($import_file['size'])) { + AdminNotices::add(__('No file selected.', 'bc-security'), AdminNotices::ERROR); + return; + } + if ($import_file['error']) { + AdminNotices::add(__('File failed to upload. Please try again.', 'bc-security'), AdminNotices::ERROR); + return; + } + if (pathinfo($import_file['name'], PATHINFO_EXTENSION) !== 'json') { + AdminNotices::add(__('Incorrect file type!', 'bc-security'), AdminNotices::ERROR); + return; + } + + // Read the file. + if (empty($json = file_get_contents($import_file['tmp_name']))) { + AdminNotices::add(__('File could not be read!', 'bc-security'), AdminNotices::ERROR); + return; + } + + // Parse JSON. + if (empty($import = json_decode($json, true))) { // true -> convert objects into associative arrays + AdminNotices::add(__('File is either empty or corrupted!', 'bc-security'), AdminNotices::ERROR); + return; + } + + $status = true; + + foreach ($this->settings as $settings) { + $option_name = $settings->getOptionName(); + if (!isset($import[$option_name])) { + $status = false; + continue; + } + + $data = $import[$option_name]; + if (!is_array($data)) { + $status = false; + continue; + } + + $settings->set($data); + } + + if ($status) { + AdminNotices::add( + __('Plugin settings have been imported successfully.', 'bc-security'), + AdminNotices::SUCCESS + ); + } else { + AdminNotices::add( + __('Some or all plugin settings could not be updated. Make sure you are importing file that has been created by the same version of the plugin.', 'bc-security'), + AdminNotices::WARNING + ); + } + } + + + private function processResetAction() + { + foreach ($this->settings as $settings) { + $settings->reset(); + } + + AdminNotices::add( + __('Plugin settings have been reset to their defaults.', 'bc-security'), + AdminNotices::SUCCESS + ); + } } diff --git a/classes/BlueChip/Security/Plugin.php b/classes/BlueChip/Security/Plugin.php index f63eb21..58a31fc 100644 --- a/classes/BlueChip/Security/Plugin.php +++ b/classes/BlueChip/Security/Plugin.php @@ -176,7 +176,7 @@ public function init() $this->settings['log'], $this->modules['logger'] )) - ->addPage(new Modules\Tools\AdminPage()) + ->addPage(new Modules\Tools\AdminPage($this->settings)) ; } } From 92d0a56363fa5a2d3f52d74426150fb779b558cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Thu, 22 Nov 2018 16:42:00 +0100 Subject: [PATCH 11/13] Mention export/import in README. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 056383b..eb199e3 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Helps keeping WordPress websites secure. Several features of BC Security depends on the knowledge of remote IP address, so it is important that you let the plugin know how your server is connected to the Internet. You can either set connection type via _Setup_ page or with via `BC_SECURITY_CONNECTION_TYPE` constant. +**Note:** If you already have an installation with BC Security set up and would like to set up another installation in the same way, you can export plugin settings (including connection type) from the former installation and import them to the latter. + ## Features ### Checklist From c8f81f1da981e8f39dffed9aa1b651673dec3b95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Thu, 22 Nov 2018 16:59:28 +0100 Subject: [PATCH 12/13] Raise required PHP version to 7.1. Soft requirement, no PHP 7.0 incompatible changes have been introduced yet. Fixes #61. --- README.md | 2 +- bc-security.php | 6 +++--- composer.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index eb199e3..e37a222 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Helps keeping WordPress websites secure. ## Requirements -* [PHP](https://secure.php.net/) 7.0 or newer +* [PHP](https://secure.php.net/) 7.1 or newer * [WordPress](https://wordpress.org/) 4.9 or newer ## Limitations diff --git a/bc-security.php b/bc-security.php index 83d6f95..498f0ee 100644 --- a/bc-security.php +++ b/bc-security.php @@ -6,20 +6,20 @@ * Version: develop * Author: Česlav Przywara * Author URI: https://www.chesio.com - * Requires PHP: 7.0 + * Requires PHP: 7.1 * Requires WP: 4.9 * Tested up to: 4.9 * Text Domain: bc-security * GitHub Plugin URI: https://github.com/chesio/bc-security */ -if (version_compare(PHP_VERSION, '7.0', '<')) { +if (version_compare(PHP_VERSION, '7.1', '<')) { // Warn user that his/her PHP version is too low for this plugin to function. add_action('admin_notices', function () { echo '

'; echo esc_html( sprintf( - __('BC Security plugin requires PHP 7.0 to function properly, but you have version %s installed. The plugin has been auto-deactivated.', 'bc-security'), + __('BC Security plugin requires PHP 7.1 to function properly, but you have version %s installed. The plugin has been auto-deactivated.', 'bc-security'), PHP_VERSION ) ); diff --git a/composer.json b/composer.json index 1d1835b..dab2219 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "issues": "https://github.com/chesio/bc-security/issues" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1.0", "composer/installers": "~1.0" }, "require-dev": { From c376f9b80f6d8f64ad0fa5bd99c45cd3c93212a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Ceslav=20Przywara?= Date: Fri, 23 Nov 2018 11:54:42 +0100 Subject: [PATCH 13/13] Bump version number to 0.10.0. --- bc-security.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-security.php b/bc-security.php index 498f0ee..1733f5a 100644 --- a/bc-security.php +++ b/bc-security.php @@ -3,7 +3,7 @@ * Plugin Name: BC Security * Plugin URI: https://github.com/chesio/bc-security * Description: Helps keeping WordPress websites secure. - * Version: develop + * Version: 0.10.0 * Author: Česlav Przywara * Author URI: https://www.chesio.com * Requires PHP: 7.1