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'
+ ),
'
'
),
],