diff --git a/README.txt b/README.txt index 710faf0..b382299 100644 --- a/README.txt +++ b/README.txt @@ -2,8 +2,8 @@ Contributors: jaredcobb Tags: ccb, church, api, chms Requires at least: 4.6.0 -Tested up to: 4.9.6 -Stable tag: 1.0.6 +Tested up to: 5.0.1 +Stable tag: 1.0.7 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -61,6 +61,13 @@ You'll need to ensure your [group settings](https://churchcommunitybuilder.force == Changelog == += 1.0.7 = +* Fix for a bug where auto-sync was failing when group images are enabled. +* PHP 7.2 and above compatibility improvements. +* Stronger encryption methods for PHP 7.2 and above. +* Performance improvements in how the plugin loads and runs. +* Compatible with Gutenberg, WordPress 5.0. + = 1.0.6 = * Fixes a bug where admin libraries may not be loaded under some cron contexts. diff --git a/ccb-core.php b/ccb-core.php index e679eff..f13eb34 100644 --- a/ccb-core.php +++ b/ccb-core.php @@ -10,7 +10,7 @@ * Plugin Name: Church Community Builder Core API * Plugin URI: https://www.wpccb.com * Description: A plugin to provide a core integration of the Church Community Builder API into WordPress custom post types - * Version: 1.0.6 + * Version: 1.0.7 * Author: Jared Cobb * Author URI: https://www.jaredcobb.com/ * License: GPL-2.0+ @@ -27,7 +27,7 @@ define( 'CCB_CORE_PATH', plugin_dir_path( __FILE__ ) ); define( 'CCB_CORE_URL', plugin_dir_url( __FILE__ ) ); define( 'CCB_CORE_BASENAME', plugin_basename( __FILE__ ) ); -define( 'CCB_CORE_VERSION', '1.0.6' ); +define( 'CCB_CORE_VERSION', '1.0.7' ); // Check minimum requirements before proceeding. require_once CCB_CORE_PATH . 'includes/class-ccb-core-requirements.php'; diff --git a/includes/class-ccb-core-admin-ajax.php b/includes/class-ccb-core-admin-ajax.php index 268a316..19524a1 100644 --- a/includes/class-ccb-core-admin-ajax.php +++ b/includes/class-ccb-core-admin-ajax.php @@ -18,20 +18,6 @@ */ class CCB_Core_Admin_AJAX { - /** - * An instance of the CCB_Core_API class - * - * @var CCB_Core_API - */ - private $api; - - /** - * An instance of the CCB_Core_Synchronizer class - * - * @var CCB_Core_Synchronizer - */ - private $synchronizer; - /** * Initialize the class and register hooks * @@ -42,9 +28,6 @@ public function __construct() { add_action( 'wp_ajax_poll_sync', [ $this, 'ajax_poll_sync' ] ); add_action( 'wp_ajax_get_latest_sync', [ $this, 'ajax_get_latest_sync' ] ); add_action( 'wp_ajax_test_credentials', [ $this, 'ajax_test_credentials' ] ); - - $this->api = CCB_Core_API::instance(); - $this->synchronizer = CCB_Core_Synchronizer::instance(); } /** @@ -61,7 +44,7 @@ public function ajax_sync() { // Tell the user to move along and go about their business... CCB_Core_Helpers::instance()->send_non_blocking_json_success(); - $result = $this->synchronizer->synchronize(); + $result = CCB_Core_Synchronizer::instance()->synchronize(); } @@ -201,7 +184,7 @@ public function ajax_get_latest_sync() { */ public function ajax_test_credentials() { check_ajax_referer( 'ccb_core_nonce', 'nonce' ); - $response = $this->api->get( 'api_status' ); + $response = CCB_Core_API::instance()->get( 'api_status' ); if ( 'SUCCESS' === $response['status'] ) { wp_send_json_success(); diff --git a/includes/class-ccb-core-api.php b/includes/class-ccb-core-api.php index 4b52bb5..2076f4a 100644 --- a/includes/class-ccb-core-api.php +++ b/includes/class-ccb-core-api.php @@ -96,9 +96,7 @@ public static function instance() { * @return void */ private function setup() { - // Wait to initialize the API credentials until after WordPress - // has loaded pluggable.php because we are using some WordPress helper functions. - add_action( 'plugins_loaded', [ $this, 'initialize_credentials' ] ); + $this->initialize_credentials(); } /** @@ -258,10 +256,12 @@ private function request( $method, $service, $data = [] ) { // We successfully parsed the XML response, however the // response may contain error messages from CCB. if ( isset( $parsed_response->response->errors->error ) ) { - $result['error'] = esc_html( sprintf( - __( 'The CCB API replied with an error: %s', 'ccb-core' ), - $parsed_response->response->errors->error - ) ); + $result['error'] = esc_html( + sprintf( + __( 'The CCB API replied with an error: %s', 'ccb-core' ), + $parsed_response->response->errors->error + ) + ); $result['status'] = 'ERROR'; } else { $result['status'] = 'SUCCESS'; diff --git a/includes/class-ccb-core-cron.php b/includes/class-ccb-core-cron.php index 4d254fc..394e39a 100644 --- a/includes/class-ccb-core-cron.php +++ b/includes/class-ccb-core-cron.php @@ -18,13 +18,6 @@ */ class CCB_Core_Cron { - /** - * An instance of the CCB_Core_Synchronizer class - * - * @var CCB_Core_Synchronizer - */ - private $synchronizer; - /** * Initialize the class and set its properties. * @@ -38,8 +31,6 @@ public function __construct() { add_action( 'ccb_core_auto_sync_hook', [ $this, 'auto_sync_callback' ] ); // When the cron settings are changed, configure the events. add_action( 'update_option_ccb_core_settings', [ $this, 'cron_settings_changed' ], 10, 2 ); - - $this->synchronizer = CCB_Core_Synchronizer::instance(); } /** @@ -54,10 +45,12 @@ public function custom_cron_schedule( $schedules ) { if ( ! empty( $settings['auto_sync_timeout'] ) ) { $schedules['ccb_core_schedule'] = [ 'interval' => MINUTE_IN_SECONDS * absint( $settings['auto_sync_timeout'] ), - 'display' => esc_html( sprintf( - __( 'Every %s Minutes' ), - absint( $settings['auto_sync_timeout'] ) - ) ), + 'display' => esc_html( + sprintf( + __( 'Every %s Minutes' ), + absint( $settings['auto_sync_timeout'] ) + ) + ), ]; } return $schedules; @@ -109,7 +102,7 @@ private function remove_existing_cron_events() { * @return void */ public function auto_sync_callback() { - $this->synchronizer->synchronize(); + CCB_Core_Synchronizer::instance()->synchronize(); } } diff --git a/includes/class-ccb-core-helpers.php b/includes/class-ccb-core-helpers.php index d3756b3..19ad292 100644 --- a/includes/class-ccb-core-helpers.php +++ b/includes/class-ccb-core-helpers.php @@ -109,9 +109,47 @@ public function refresh_options( $old_value, $new_value ) { * @return string */ public function encrypt( $data ) { + if ( ! function_exists( 'sodium_crypto_secretbox' ) ) { + return $this->legacy_encrypt( $data ); + } + + $encrypted_value = false; + if ( ! empty( $data ) ) { + try { + // Create a one-time random nonce and salt. + $nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ); + $salt = random_bytes( SODIUM_CRYPTO_PWHASH_SALTBYTES ); + // Create a unique key that is seeded from the site's AUTH_KEY constant. + $key = sodium_crypto_pwhash( + SODIUM_CRYPTO_SECRETBOX_KEYBYTES, + AUTH_KEY, + $salt, + SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE + ); + // Encrypt the data with the nonce and salt prepended so that we can + // use them to decrypt the value later. + $encrypted_value = base64_encode( $nonce . $salt . sodium_crypto_secretbox( $data, $nonce, $key ) ); + } catch ( Exception $ex ) { + return new WP_Error( 'encrypt_failure', __( 'The string could not be encrypted via Sodium', 'ccb-core' ) ); + } + + } + + return $encrypted_value; + } + /** + * Encrypts and base64_encodes a string safe for serialization in WordPress + * when the Sodium functions are not available (typically below PHP 7.2) + * + * @param string $data The data to be encrypted. + * + * @return string + */ + private function legacy_encrypt( $data ) { $encrypted_value = false; - $key = wp_salt() . md5( 'ccb-core' ); + $key = hash_hmac( 'sha512', AUTH_SALT, AUTH_KEY ); if ( ! empty( $data ) ) { try { @@ -135,9 +173,68 @@ public function encrypt( $data ) { * @return string */ public function decrypt( $data ) { + if ( ! function_exists( 'sodium_crypto_secretbox_open' ) ) { + return $this->legacy_decrypt( $data ); + } $decrypted_value = false; - $key = wp_salt() . md5( 'ccb-core' ); + if ( ! empty( $data ) ) { + try { + // Decode the stored value. + $decoded_data = base64_decode( $data ); + // Get the stored nonce from the beginning of the multibyte string. + $nonce = mb_substr( + $decoded_data, + 0, + SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, + '8bit' + ); + // Get the stored salt from the middle of the multibyte string. + $salt = mb_substr( + $decoded_data, + SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, + SODIUM_CRYPTO_PWHASH_SALTBYTES, + '8bit' + ); + // Get the encrypted data from the end of the multibyte string. + $cipher = mb_substr( + $decoded_data, + SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_PWHASH_SALTBYTES, + null, + '8bit' + ); + // Generate the known key from the stored salt and site's AUTH_KEY constant. + $key = sodium_crypto_pwhash( + SODIUM_CRYPTO_SECRETBOX_KEYBYTES, + AUTH_KEY, + $salt, + SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE + ); + // Decrypt the data using the stored nonce. + $decrypted_value = sodium_crypto_secretbox_open( $cipher, $nonce, $key ); + } catch ( Exception $ex ) { + return new WP_Error( 'decrypt_failure', __( 'The string could not be decrypted', 'ccb-core' ) ); + } + + } + + return $decrypted_value; + } + + /** + * Decrypts and base64_decodes a string when the Sodium functions + * are not available (typically below PHP 7.2) + * + * @since 1.0.0 + * @access public + * @param string $data The data to be decrypted. + * @return string + */ + public function legacy_decrypt( $data ) { + + $decrypted_value = false; + $key = hash_hmac( 'sha512', AUTH_SALT, AUTH_KEY ); if ( ! empty( $data ) ) { try { @@ -169,10 +266,12 @@ public function send_non_blocking_json_success( $data = [] ) { header( 'Content-Type: application/json' ); header( 'Content-Encoding: none' ); - echo wp_json_encode( [ - 'success' => true, - 'data' => $data, - ] ); + echo wp_json_encode( + [ + 'success' => true, + 'data' => $data, + ] + ); header( 'Connection: close' ); header( 'Content-Length: ' . ob_get_length() ); @@ -211,6 +310,9 @@ public function download_image( $image_url, $filename = '', $post_id = 0 ) { if ( ! function_exists( 'download_url' ) ) { include_once ABSPATH . 'wp-admin/includes/file.php'; } + if ( ! function_exists( 'wp_read_image_metadata' ) ) { + include_once ABSPATH . 'wp-admin/includes/image.php'; + } if ( ! function_exists( 'media_handle_sideload' ) ) { include_once ABSPATH . 'wp-admin/includes/media.php'; } diff --git a/includes/class-ccb-core-requirements.php b/includes/class-ccb-core-requirements.php index 611dd86..7fb58e8 100644 --- a/includes/class-ccb-core-requirements.php +++ b/includes/class-ccb-core-requirements.php @@ -37,6 +37,13 @@ class CCB_Core_Requirements { */ private $required_wordpress = '4.6.0'; + /** + * Required global constants defined in wp-config.php + * + * @var array + */ + private $required_keys = [ 'AUTH_KEY', 'AUTH_SALT' ]; + /** * Any applicable error messages * @@ -51,6 +58,8 @@ class CCB_Core_Requirements { */ public function __construct() { $this->validate_versions(); + $this->validate_keys(); + $this->validate_encryption_methods(); } /** @@ -109,4 +118,48 @@ private function validate_versions() { $this->register_disable_plugin(); } + /** + * Ensure the site has configured the required keys. + * + * @return void + */ + private function validate_keys() { + foreach ( $this->required_keys as $key ) { + if ( ! defined( $key ) || 32 > strlen( constant( $key ) ) ) { + $this->requirements_met = false; + $this->error_message = sprintf( + 'Church Community Builder Core API requires that you configure a random ' . + 'value for the %s constant that is at least 32 characters long. See ' . + 'https://codex.wordpress.org/Editing_wp-config.php#Security_Keys ' . + 'for more information.', + $key + ); + $this->register_disable_plugin(); + return; + } + } + } + + /** + * Ensure the site has an encryption module installed. + * + * @return void + */ + private function validate_encryption_methods() { + if ( + ! function_exists( 'sodium_crypto_secretbox' ) + && ! function_exists( 'sodium_crypto_secretbox_open' ) + && ! function_exists( 'mcrypt_encrypt' ) + && ! function_exists( 'mcrypt_decrypt' ) + ) { + $this->requirements_met = false; + $this->error_message = 'Church Community Builder Core API requires that you ' . + 'have an encryption library installed on your system. By default, ' . + 'you should have the mcrypt module installed for PHP versions less than 7.2 ' . + 'or the sodium module installed for PHP versions 7.2 or later. ' . + 'Please contact your hosting provider for more information.'; + $this->register_disable_plugin(); + return; + } + } } diff --git a/includes/class-ccb-core-settings.php b/includes/class-ccb-core-settings.php index b9333e6..6cc3185 100644 --- a/includes/class-ccb-core-settings.php +++ b/includes/class-ccb-core-settings.php @@ -103,7 +103,7 @@ public function validate_settings( $input ) { // For a brand new installation, if the option doesn't yet // exist, sanitize callback is called twice. // See https://core.trac.wordpress.org/ticket/21989. - if ( 200 < strlen( $input[ $field_id ]['password'] ) && ! isset( $current_options[ $field_id ]['password'] ) ) { + if ( 76 < strlen( $input[ $field_id ]['password'] ) && ! isset( $current_options[ $field_id ]['password'] ) ) { // Password was already encrypted on the previous sanitization call. $encrypted_password = $input[ $field_id ]['password']; } else { @@ -230,7 +230,8 @@ public function get_settings_definitions() { 'We keep a local copy (cache) of your Church Community Builder data for the best performance.%1$s How often (in minutes) should we check for new data?%2$s (90 minutes is recommended).', - 'ccb-core' ), + 'ccb-core' + ), '
', '
' ), diff --git a/includes/class-ccb-core-synchronizer.php b/includes/class-ccb-core-synchronizer.php index 909d682..2b62ea2 100644 --- a/includes/class-ccb-core-synchronizer.php +++ b/includes/class-ccb-core-synchronizer.php @@ -35,13 +35,6 @@ class CCB_Core_Synchronizer { */ private static $instance; - /** - * An instance of the CCB_Core_API class - * - * @var CCB_Core_API - */ - private $api; - /** * Unused constructor in the singleton pattern * @@ -76,7 +69,6 @@ private function setup() { // Wait to initialize the map until after the plugins / themes are fully // loaded so that all post types, taxonomies, and theme hooks have been registered. add_action( 'init', [ $this, 'initialize_map' ], 9, 1 ); // Cron runs on `init` priority `10`. - $this->api = CCB_Core_API::instance(); } /** @@ -175,7 +167,7 @@ public function synchronize() { */ $response = apply_filters( 'ccb_core_synchronizer_api_response', - $this->api->get( $settings['service'], $data ), + CCB_Core_API::instance()->get( $settings['service'], $data ), $settings, $post_type ); @@ -912,3 +904,4 @@ private function disable_optimizations() { } } +CCB_Core_Synchronizer::instance(); diff --git a/includes/class-ccb-core.php b/includes/class-ccb-core.php index 26adb4b..c135c38 100644 --- a/includes/class-ccb-core.php +++ b/includes/class-ccb-core.php @@ -47,8 +47,11 @@ public function __construct() { */ private function load_dependencies() { - // Encryption class to provide better security and ease of use. - require_once CCB_CORE_PATH . 'lib/class-ccb-core-vendor-encryption.php'; + // For environments that do not support Sodium (usually PHP < 7.2) use a legacy class. + if ( ! function_exists( 'sodium_crypto_secretbox' ) || ! function_exists( 'sodium_crypto_secretbox_open' ) ) { + // Encryption class to provide better security and ease of use. + require_once CCB_CORE_PATH . 'lib/class-ccb-core-vendor-encryption.php'; + } // A generic helper class with commonly used mehtods. require_once CCB_CORE_PATH . 'includes/class-ccb-core-helpers.php'; @@ -156,6 +159,11 @@ public function check_version() { $this->upgrade_to_1_0_0(); } + // Upgrade to version 1.0.7. + if ( version_compare( $current_version, '1.0.7', '<' ) ) { + $this->upgrade_to_1_0_7(); + } + // Update the DB version. update_option( 'ccb_core_version', CCB_CORE_VERSION ); } @@ -412,4 +420,41 @@ private function upgrade_to_1_0_0() { } } + /** + * Decrypts any existing API password using the old + * scheme and encrypts it back using the current method. + * + * @return void + */ + private function upgrade_to_1_0_7() { + // If mcrypt isn't installed it doesn't matter, we cannot + // decrypt any existing passwords. + if ( ! function_exists( 'mcrypt_decrypt' ) ) { + return; + } + + // Ensure the legacy encryption class is loaded regardless of + // the current version of PHP running. + require_once CCB_CORE_PATH . 'lib/class-ccb-core-vendor-encryption.php'; + + $current_options = CCB_Core_Helpers::instance()->get_options(); + $decrypted_value = false; + if ( ! empty( $current_options['credentials']['password'] ) ) { + $key = wp_salt() . md5( 'ccb-core' ); + try { + $e = new CCB_Core_Vendor_Encryption( MCRYPT_BlOWFISH, MCRYPT_MODE_CBC ); + $decrypted_value = $e->decrypt( base64_decode( $current_options['credentials']['password'] ), $key ); + } catch ( Exception $ex ) { + $decrypted_value = false; + } + + $encrypted_value = CCB_Core_Helpers::instance()->encrypt( $decrypted_value ); + if ( ! is_wp_error( $encrypted_value ) ) { + $current_options['credentials']['password'] = $encrypted_value; + } else { + $current_options['credentials']['password'] = ''; + } + update_option( 'ccb_core_settings', $current_options ); + } + } } diff --git a/includes/post-types/class-ccb-core-calendar.php b/includes/post-types/class-ccb-core-calendar.php index 43b47a5..77ed296 100644 --- a/includes/post-types/class-ccb-core-calendar.php +++ b/includes/post-types/class-ccb-core-calendar.php @@ -158,7 +158,8 @@ public function get_post_settings_definitions( $settings ) { This is the best setting for most churches.%2$s Specific: For example, only get events from "6/1/2018" to "12/1/2018".%3$s This setting is best if you want to tightly manage the events that get published.', - 'ccb-core' ), + 'ccb-core' + ), '
', '

', '
' @@ -199,7 +200,8 @@ public function get_post_settings_definitions( $settings ) { esc_html__( 'When synchronizing, we should get events that start after this date.%s (Leave empty to always start "today")', - 'ccb-core' ), + 'ccb-core' + ), '
' ), ], @@ -212,7 +214,8 @@ public function get_post_settings_definitions( $settings ) { esc_html__( 'When synchronizing, we should get events that start before this date.%s (Setting this too far into the future may cause the API to timeout)', - 'ccb-core' ), + 'ccb-core' + ), '
' ), ], diff --git a/includes/post-types/class-ccb-core-group.php b/includes/post-types/class-ccb-core-group.php index d0955fb..c981d63 100644 --- a/includes/post-types/class-ccb-core-group.php +++ b/includes/post-types/class-ccb-core-group.php @@ -148,7 +148,8 @@ public function get_post_settings_definitions( $settings ) { esc_html__( 'This will download the CCB Group Image and attach it as a Featured Image.%s If you don\'t need group images, then disabling this feature will speed up the synchronization.', - 'ccb-core' ), + 'ccb-core' + ), '
' ), ],