diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..100725f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,63 @@ +sudo: false +dist: trusty + +language: php + +notifications: + email: + on_success: never + on_failure: change + +branches: + only: + - master + +cache: + directories: + - vendor + - $HOME/.composer/cache + +matrix: + include: + - php: 7.1 + env: WP_VERSION=latest + - php: 7.0 + env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=4.6 + - php: 5.6 + env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=trunk + - php: 5.6 + env: WP_TRAVISCI=phpcs + +before_script: + - export PATH="$HOME/.composer/vendor/bin:$PATH" + - | + if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then + phpenv config-rm xdebug.ini + else + echo "xdebug.ini does not exist" + fi + - | + if [[ ! -z "$WP_VERSION" ]] ; then + bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + composer global require "phpunit/phpunit=4.8.*|5.7.*" + fi + - | + if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then + composer global require wp-coding-standards/wpcs + phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs + fi + +script: + - | + if [[ ! -z "$WP_VERSION" ]] ; then + phpunit + WP_MULTISITE=1 phpunit + fi + - | + if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then + phpcs + fi diff --git a/README.md b/README.md new file mode 100644 index 0000000..f232ee1 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Church Community Builder Core API + +## A WordPress Plugin that syncs your church data + +CCB Core API is a WordPress plugin that has one simple job: It **synchronizes** your church data from [Church Community Builder](https://www.churchcommunitybuilder.com/) into your WordPress database as [Custom Post Types](https://codex.wordpress.org/Post_Types#Custom_Post_Types), [Custom Taxonomies](https://codex.wordpress.org/Taxonomies#Custom_Taxonomies), and [Post Meta](https://codex.wordpress.org/Custom_Fields). + +## Who should use this? + +This plugin is geared toward Developers, Designers, and Site Administrators who are familiar with customizing WordPress templates. While it does a great job of synchronizing the data, you'll still need to alter your theme in order to take *advantage* of the data. + +## What's included? + +Out of the box, there are two complete integrations: + +### Public Groups + +This integration will synchronize any groups that are both _publicly listed_ and _active_ from the Church Community Builder `group_profiles` service to a Custom Post Type named `ccb_core_groups`. + +### Public Calendar (Events) + +This integration will synchronize all events from the Church Community Builder `public_calendar_listing` service to a Custom Post Type named `ccb_core_calendar`. + +## Features + +* **Auto Synchronize** - Set it and forget it! The plugin works in the background, never interrupting you or your visitors. +* **Secure** - Your credentials are encrypted, and so is the connection with the Church Community Builder API. +* **WordPress Standards** - The plugin follows WordPress coding standards and best practices, so it's easy to extend and build upon. +* **Free** - Free as in "speech" or free as in "beer"? Yes! It's [GPLv2 licensed](https://tldrlegal.com/license/gnu-general-public-license-v2). Don't you love open source? + +## Customizing & Extending + +* Setup additional integrations with other Church Community Builder API services. +* Write your own plugin that builds upon this one. +* Customize the existing integrations (Groups & Events). + +
**[The Wiki](https://github.com/jaredcobb/ccb-core/wiki) has more information and code samples.**
+ +## General Usage + +General usage information (setting up the plugin and customizing your theme) can be found in the [usage docs](https://www.wpccb.com/documentation/). \ No newline at end of file diff --git a/README.txt b/README.txt index 5faf81f..7cf0637 100644 --- a/README.txt +++ b/README.txt @@ -1,9 +1,9 @@ === Church Community Builder Core API === Contributors: jaredcobb Tags: ccb, church, api, chms -Requires at least: 3.0.1 -Tested up to: 4.3.1 -Stable tag: 0.9.6 +Requires at least: 4.6.0 +Tested up to: 4.9.1 +Stable tag: 1.0.0 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -12,15 +12,14 @@ Provides a core integration to the Church Community Builder API. == Description == Church Community Builder Core API *synchronizes* your church data to WordPress [custom post types](https://codex.wordpress.org/Custom_Post_Types). -This plugin is geared toward developers (or advanced WordPress users who aren't afraid to get into a little bit of code). -Find out more at [http://www.wpccb.com/](http://www.wpccb.com). +This plugin is geared toward Developers, Designers, and Site Administrators who aren't afraid to get into a little bit of code. + +Find out more at [https://www.wpccb.com](https://www.wpccb.com) and [https://github.com/jaredcobb/ccb-core](https://github.com/jaredcobb/ccb-core). = Why Use This Plugin? = -One of the biggest challenges with getting your Church Community Builder data onto your site is the actual API integration. -This plugin does all of the heavy lifting for you. Once your church data is securely synchronized you can use it freely in -your theme, widgets, or even your own plugins! +One of the biggest challenges with getting your Church Community Builder data onto your site is the actual API integration. This plugin does all of the heavy lifting for you. Once your church data is securely synchronized you can use it freely in your theme, widgets, or even your own plugins! = Features = @@ -34,7 +33,7 @@ your theme, widgets, or even your own plugins! = Documentation = -The [http://www.wpccb.com/documentation/](official documentation) has more information, including code samples, hooks, filters, and links to tutorials. +Extensive developer documentation is available on the [GitHub wiki](https://github.com/jaredcobb/ccb-core/wiki). == Installation == @@ -49,15 +48,11 @@ The [http://www.wpccb.com/documentation/](official documentation) has more infor = I installed this plugin and my site doesn't look any different = -This plugin has a very specific task: It gets some of your Church Community Builder data and imports it into your -WordPress database (as custom post types). A developer (or advanced WordPress administrator) will need to -alter your theme to *take advantage* of this data. +This plugin has a very specific task: It gets some of your Church Community Builder data and imports it into your WordPress database (as custom post types). A developer (or advanced WordPress administrator) will need to alter your theme to *take advantage* of this data. = Some of my groups in Church Community Builder aren't being synchronized = -You'll need to ensure your [group settings](https://support.churchcommunitybuilder.com/customer/portal/articles/361764-editing-groups) -allow the group to be publicly listed. A great way to cross reference if your group is publicly visible is to visit -*yoursubdomain*.ccbchurch.com/w_group_list.php and see if the missing group shows up there. +You'll need to ensure your [group settings](https://churchcommunitybuilder.force.com/s/article/2102903) allow the group to be publicly listed. A great way to cross reference if your group is publicly visible is to visit *yoursubdomain*.ccbchurch.com/w_group_list.php and see if the missing group shows up there. == Screenshots == @@ -66,6 +61,12 @@ allow the group to be publicly listed. A great way to cross reference if your gr == Changelog == += 1.0.0 = +* Official stable release +* *Breaking Changes* - Please note that post type and custom taxonomy names have changed (see [release notes](https://github.com/jaredcobb/ccb-core/wiki/1.0.0-Stable-Release) ) +* Fixed broken group images (CCB API query parameter `include_image_link=true`) +* Refactored code to be faster, simpler, and easier to extend + = 0.9.6 = * Added automatic flushing of rewrite rules when custom post type settings are changed * Added link to official documentation in README and About page @@ -89,7 +90,7 @@ allow the group to be publicly listed. A great way to cross reference if your gr = 0.9.2 = * Added tooltips to some settings to help explain the functionality * Added better defaults for date ranges -* Updated the plugin web site to http://www.wpccb.com +* Updated the plugin web site to https://www.wpccb.com = 0.9.1 = * Fixed an issue where some web hosts were not saving encypted passwords diff --git a/admin/class-ccb-core-admin.php b/admin/class-ccb-core-admin.php deleted file mode 100644 index d264ec7..0000000 --- a/admin/class-ccb-core-admin.php +++ /dev/null @@ -1,329 +0,0 @@ - - */ -class CCB_Core_Admin extends CCB_Core_Plugin { - - /** - * Initialize the class and set its properties. - * - * @since 0.9.0 - */ - public function __construct() { - parent::__construct(); - } - - /** - * Initialize the Settings Menu and Page - * - * @access public - * @since 0.9.0 - * @return void - */ - public function initialize_settings_menu() { - - $settings = new CCB_Core_Settings(); - $settings_definitions = $settings->get_settings_definitions(); - $settings_page = new CCB_Core_Settings_Page( $this->plugin_settings_name ); - - add_menu_page( $this->plugin_display_name, $this->plugin_short_display_name, 'manage_options', $this->plugin_settings_name, '__return_null', 'dashicons-update', '80.9' ); - - if ( is_array( $settings_definitions ) && ! empty( $settings_definitions ) ) { - foreach ( $settings_definitions as $page_id => $page ) { - $settings_page = new CCB_Core_Settings_Page( $page_id, $page ); - add_submenu_page( $this->plugin_settings_name, $page['page_title'], $page['page_title'], 'manage_options', $page_id, array( $settings_page, 'render_page' ) ); - } - } - } - - /** - * Initialize the Settings - * - * @access public - * @since 0.9.0 - * @return void - */ - public function initialize_settings() { - - $settings = new CCB_Core_Settings(); - $settings_definitions = $settings->get_settings_definitions(); - - if ( is_array( $settings_definitions ) && ! empty( $settings_definitions ) ) { - foreach ( $settings_definitions as $page_id => $page ) { - - register_setting( $page_id, $this->plugin_settings_name, array( $settings, 'validate_settings' ) ); - - if ( isset( $page['sections'] ) && ! empty( $page['sections'] ) ) { - foreach ( $page['sections'] as $section_id => $section ) { - - $settings_section = new CCB_Core_Settings_Section( $section_id, $section ); - add_settings_section( $section_id, $section['section_title'], array( $settings_section, 'render_section' ), $page_id ); - - if ( isset( $section['fields'] ) && ! empty( $section['fields'] ) ) { - foreach ( $section['fields'] as $field_id => $field ) { - - $settings_field = new CCB_Core_Settings_Field( $field_id, $field ); - add_settings_field( $field_id, $field['field_title'], array( $settings_field, 'render_field' ), $page_id, $section_id ); - - } - } - - } - } - - } - } - - } - - /** - * Just before the settings are saved, check for changes - * that would require us to flush the rewrite rules - * - * @param array $new_settings - * @param array $previous_settings - * @access public - * @since 0.9.6 - * @return array - */ - public function update_settings_callback( $new_settings, $previous_settings ) { - - // create a collection of settings that, if they change, should - // trigger a flush_rewrite_rules event - $setting_array = array( - 'groups-enabled', - 'groups-slug', - 'calendar-enabled', - 'calendar-slug', - ); - - foreach ( $setting_array as $setting ) { - if ( isset( $new_settings[ $setting ] ) ) { - if ( ! isset( $previous_settings[ $setting ] ) || $new_settings[ $setting ] !== $previous_settings[ $setting ] ) { - // schedule an event to flush the rewrite rules on the next page load because the settings aren't quite saved yet - wp_schedule_single_event( time(), 'schedule_flush_rewrite_rules' ); - } - } - } - - return $new_settings; - } - - /** - * Simple callback function for flushing the rewrite rules - * - * @access public - * @since 0.9.6 - * @return void - */ - public function flush_rewrite_rules_event() { - - flush_rewrite_rules(); - } - - /** - * Register the CCB custom post types if enabled - * - * @access public - * @since 0.9.0 - * @return void - */ - public function initialize_custom_post_types() { - $cpts = new CCB_Core_CPTs(); - $cpts->initialize(); - } - - /** - * Launches a synchronization from an ajax hook and will respond - * with a non-blocking ajax response - * - * @access public - * @since 0.9.0 - * @return void - */ - public function ajax_sync() { - - $nonce = $_POST['nextNonce']; - - if ( ! wp_verify_nonce( $nonce, $this->plugin_name . '-nonce' ) ) { - wp_send_json( array('success' => false) ); - } - - // tell the user to move along and go about their business... - $this->send_non_blocking_json_response( array( 'success' => true ) ); - - $sync = new CCB_Core_Sync(); - $sync->sync(); - - } - - /** - * Checks for an active synchronization from an ajax hook - * and responds with the transient value - * - * @access public - * @since 0.9.0 - * @return void - */ - public function ajax_poll_sync() { - - $nonce = $_POST['nextNonce']; - if ( ! wp_verify_nonce( $nonce, $this->plugin_name . '-nonce' ) ) { - wp_send_json( array('success' => false) ); - } - - $sync_in_progress = get_transient( $this->plugin_name . '-sync-in-progress' ); - wp_send_json( array( 'syncInProgress' => $sync_in_progress ) ); - - } - - /** - * Gets the latest synchronization results from an ajax hook - * - * @access public - * @since 0.9.0 - * @return void - */ - public function ajax_get_latest_sync() { - - $nonce = $_POST['nextNonce']; - if ( ! wp_verify_nonce( $nonce, $this->plugin_name . '-nonce' ) ) { - wp_send_json( array('success' => false) ); - } - - $latest_sync = $this->get_latest_sync_results(); - wp_send_json( $latest_sync ); - - } - - /** - * Checks the credentials for a user from an ajax hook - * - * @access public - * @since 0.9.0 - * @return void - */ - public function ajax_test_credentials() { - - $nonce = $_POST['nextNonce']; - if ( ! wp_verify_nonce( $nonce, $this->plugin_name . '-nonce' ) ) { - wp_send_json( array('success' => false) ); - } - - $sync = new CCB_Core_Sync(); - $validation_results = $sync->test_api_credentials(); - - wp_send_json( $validation_results ); - - } - - /** - * Create a helpful settings link on the plugin page - * - * @param array $links - * @access public - * @since 0.9.0 - * @return array - */ - public function add_settings_link( $links ) { - $links[] = 'Settings'; - return $links; - } - - /** - * Check if we should schedule a synchronization based on - * the options set by the user - * - * @access public - * @since 0.9.0 - * @return void - */ - public function check_auto_refresh() { - - $settings = get_option( $this->plugin_settings_name ); - - if ( isset( $settings['auto-sync'] ) && $settings['auto-sync'] == 1 ) { - $latest_sync = get_option( $this->plugin_name . '-latest-sync' ); - - if ( ! empty( $latest_sync ) ) { - $auto_sync_timeout = $settings['auto-sync-timeout']; - $now = time(); - $diff = $now - $latest_sync['timestamp']; - - if ( $diff > $auto_sync_timeout * 60 ) { - wp_schedule_single_event( time(), 'schedule_auto_refresh' ); - } - - } - else { - wp_schedule_single_event( time(), 'schedule_auto_refresh' ); - } - } - } - - /** - * Callback function to kick off a synchronization - * - * @access public - * @since 0.9.0 - * @return void - */ - public function auto_sync() { - $sync = new CCB_Core_Sync(); - $sync->sync(); - } - - /** - * Register the stylesheets for the dashboard. - * - * @since 0.9.0 - */ - public function enqueue_styles( $hook ) { - - if ( stristr( $hook, $this->plugin_settings_name ) !== false ) { - wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/ccb-core-admin.css', array(), $this->version, 'all' ); - wp_enqueue_style( 'switchery', plugin_dir_url( __FILE__ ) . 'css/vendor/switchery.min.css', array(), $this->version, 'all' ); - wp_enqueue_style( 'powerange', plugin_dir_url( __FILE__ ) . 'css/vendor/powerange.min.css', array(), $this->version, 'all' ); - wp_enqueue_style( 'picker', plugin_dir_url( __FILE__ ) . 'css/vendor/default.css', array(), $this->version, 'all' ); - wp_enqueue_style( 'picker-date', plugin_dir_url( __FILE__ ) . 'css/vendor/default.date.css', array(), $this->version, 'all' ); - wp_enqueue_style( 'tipr', plugin_dir_url( __FILE__ ) . 'css/vendor/tipr.css', array(), $this->version, 'all' ); - } - - } - - /** - * Register the scripts for the dashboard. - * - * @since 0.9.0 - */ - public function enqueue_scripts( $hook ) { - - if ( stristr( $hook, $this->plugin_settings_name ) !== false ) { - wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/ccb-core-admin.js', array( 'jquery' ), $this->version, false ); - wp_enqueue_script( 'switchery', plugin_dir_url( __FILE__ ) . 'js/vendor/switchery.min.js', array( 'jquery' ), $this->version, false ); - wp_enqueue_script( 'powerange', plugin_dir_url( __FILE__ ) . 'js/vendor/powerange.min.js', array( 'jquery' ), $this->version, false ); - wp_enqueue_script( 'picker', plugin_dir_url( __FILE__ ) . 'js/vendor/picker.js', array( 'jquery' ), $this->version, false ); - wp_enqueue_script( 'picker-date', plugin_dir_url( __FILE__ ) . 'js/vendor/picker.date.js', array( 'picker' ), $this->version, false ); - wp_enqueue_script( 'tipr', plugin_dir_url( __FILE__ ) . 'js/vendor/tipr.min.js', array( 'jquery' ), $this->version, false ); - wp_localize_script( $this->plugin_name, strtoupper( $this->plugin_settings_name ), array( - 'nextNonce' => wp_create_nonce( $this->plugin_name . '-nonce' )) - ); - } - - } - -} diff --git a/admin/class-ccb-core-cpts.php b/admin/class-ccb-core-cpts.php deleted file mode 100644 index a7a0cab..0000000 --- a/admin/class-ccb-core-cpts.php +++ /dev/null @@ -1,400 +0,0 @@ - - */ -class CCB_Core_CPTs extends CCB_Core_Plugin { - - /** - * The options we should use to register the groups CPT - * - * @since 0.9.0 - * @access protected - * @var array $groups_cpt_options - */ - protected $groups_cpt_options = array(); - - /** - * The options we should use to register the calendar CPT - * - * @since 0.9.0 - * @access protected - * @var array $calendar_cpt_options - */ - protected $calendar_cpt_options = array(); - - /** - * Initialize the class and set its properties. - * - * @since 0.9.0 - */ - public function __construct() { - - parent::__construct(); - - } - - /** - * Determine which CCB custom post types should be registered - * - * @access public - * @since 0.9.0 - * @return void - */ - public function initialize() { - - $settings = get_option( $this->plugin_settings_name ); - - if ( isset( $settings['groups-enabled'] ) && $settings['groups-enabled'] == 1 ) { - - $this->groups_cpt_options['name'] = ( empty( $settings['groups-name'] ) ? 'Groups' : $settings['groups-name'] ); - $this->groups_cpt_options['slug'] = ( empty( $settings['groups-slug'] ) ? 'groups' : $settings['groups-slug'] ); - $this->groups_cpt_options['singular_name'] = rtrim( $this->groups_cpt_options['name'], 's' ); // this is ghetto - $this->groups_cpt_options['exclude_from_search'] = ( $settings['groups-exclude-from-search'] == 'yes' ? true : false ); - $this->groups_cpt_options['publicly_queryable'] = ( $settings['groups-publicly-queryable'] == 'yes' ? true : false ); - $this->groups_cpt_options['show_ui'] = ( $settings['groups-show-ui'] == 'yes' ? true : false ); - $this->groups_cpt_options['show_in_nav_menus'] = ( $settings['groups-show-in-nav-menus'] == 'yes' ? true : false ); - - $this->register_groups(); - - } - - if ( isset( $settings['calendar-enabled'] ) && $settings['calendar-enabled'] == 1 ) { - - $this->calendar_cpt_options['name'] = ( empty( $settings['calendar-name'] ) ? 'Events' : $settings['calendar-name'] ); - $this->calendar_cpt_options['slug'] = ( empty( $settings['calendar-slug'] ) ? 'events' : $settings['calendar-slug'] ); - $this->calendar_cpt_options['singular_name'] = rtrim( $this->calendar_cpt_options['name'], 's' ); // this is ghetto - $this->calendar_cpt_options['exclude_from_search'] = ( $settings['calendar-exclude-from-search'] == 'yes' ? true : false ); - $this->calendar_cpt_options['publicly_queryable'] = ( $settings['calendar-publicly-queryable'] == 'yes' ? true : false ); - $this->calendar_cpt_options['show_ui'] = ( $settings['calendar-show-ui'] == 'yes' ? true : false ); - $this->calendar_cpt_options['show_in_nav_menus'] = ( $settings['calendar-show-in-nav-menus'] == 'yes' ? true : false ); - - $this->register_calendar(); - - } - } - - /** - * Setup the CCB Groups custom post type and its taxonomies - * - * @access protected - * @since 0.9.0 - * @return void - */ - protected function register_groups() { - - register_post_type( $this->plugin_name . '-groups', - array( 'labels' => - array( - 'name' => $this->groups_cpt_options['name'], - 'singular_name' => $this->groups_cpt_options['singular_name'], - 'all_items' => __( 'All ' . $this->groups_cpt_options['name'], $this->plugin_name ), - 'add_new' => __( 'Add New', $this->plugin_name ), - 'add_new_item' => __( 'Add New ' . $this->groups_cpt_options['singular_name'], $this->plugin_name ), - 'edit' => __( 'Edit', $this->plugin_name ), - 'edit_item' => __( 'Edit ' . $this->groups_cpt_options['name'], $this->plugin_name ), - 'new_item' => __( 'New ' . $this->groups_cpt_options['singular_name'], $this->plugin_name ), - 'view_item' => __( 'View ' . $this->groups_cpt_options['singular_name'], $this->plugin_name ), - 'search_items' => __( 'Search ' . $this->groups_cpt_options['singular_name'], $this->plugin_name ), - 'not_found' => __( 'Nothing found in the Database.', $this->plugin_name ), - 'not_found_in_trash' => __( 'Nothing found in Trash', $this->plugin_name ), - 'parent_item_colon' => '' - ), - 'description' => __( 'These are the groups that are synchronized with your Church Community Builder software.', $this->plugin_name ), - 'public' => true, - 'publicly_queryable' => $this->groups_cpt_options['publicly_queryable'], - 'exclude_from_search' => $this->groups_cpt_options['exclude_from_search'], - 'show_ui' => $this->groups_cpt_options['show_ui'], - 'show_in_nav_menus' => $this->groups_cpt_options['show_in_nav_menus'], - 'query_var' => true, - 'menu_position' => 8, - 'menu_icon' => 'dashicons-groups', - 'rewrite' => array( 'slug' => $this->groups_cpt_options['slug'] ), - 'has_archive' => $this->groups_cpt_options['slug'], - 'capability_type' => 'post', - 'hierarchical' => false, - 'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'custom-fields', 'sticky' ), - ) - ); - - $groups_taxonomies = self::get_groups_taxonomy_map(); - foreach ( $groups_taxonomies as $taxonomy_name=>$taxonomy ) { - $taxonomy_options = array( - 'hierarchical' => $taxonomy['hierarchical'], - 'labels' => array( - 'name' => $taxonomy['name_plural'], - 'singular_name' => $taxonomy['name'], - 'search_items' => "Search {$taxonomy['name_plural']}", - 'all_items' => "All {$taxonomy['name_plural']}", - 'parent_item' => "Parent {$taxonomy['name']}", - 'parent_item_colon' => "Parent {$taxonomy['name']}:", - 'edit_item' => "Edit {$taxonomy['name']}", - 'update_item' => "Update {$taxonomy['name']}", - 'add_new_item' => "Add New {$taxonomy['name']}", - 'new_item_name' => "New {$taxonomy['name']}" - ), - 'show_admin_column' => true, - 'show_ui' => true, - 'query_var' => true, - ); - - register_taxonomy( $taxonomy_name, "{$this->plugin_name}-groups", $taxonomy_options ); - } - - } - - /** - * Setup the CCB Events custom post type and its taxonomies - * - * @access protected - * @since 0.9.0 - * @return void - */ - protected function register_calendar() { - - register_post_type( $this->plugin_name . '-calendar', - array( 'labels' => - array( - 'name' => $this->calendar_cpt_options['name'], - 'singular_name' => $this->calendar_cpt_options['singular_name'], - 'all_items' => __( 'All ' . $this->calendar_cpt_options['name'], $this->plugin_name ), - 'add_new' => __( 'Add New', $this->plugin_name ), - 'add_new_item' => __( 'Add New ' . $this->calendar_cpt_options['singular_name'], $this->plugin_name ), - 'edit' => __( 'Edit', $this->plugin_name ), - 'edit_item' => __( 'Edit ' . $this->calendar_cpt_options['name'], $this->plugin_name ), - 'new_item' => __( 'New ' . $this->calendar_cpt_options['singular_name'], $this->plugin_name ), - 'view_item' => __( 'View ' . $this->calendar_cpt_options['singular_name'], $this->plugin_name ), - 'search_items' => __( 'Search ' . $this->calendar_cpt_options['singular_name'], $this->plugin_name ), - 'not_found' => __( 'Nothing found in the Database.', $this->plugin_name ), - 'not_found_in_trash' => __( 'Nothing found in Trash', $this->plugin_name ), - 'parent_item_colon' => '' - ), - 'description' => __( 'These are the calendar that are synchronized with your Church Community Builder software.', $this->plugin_name ), - 'public' => true, - 'publicly_queryable' => $this->calendar_cpt_options['publicly_queryable'], - 'exclude_from_search' => $this->calendar_cpt_options['exclude_from_search'], - 'show_ui' => $this->calendar_cpt_options['show_ui'], - 'show_in_nav_menus' => $this->calendar_cpt_options['show_in_nav_menus'], - 'query_var' => true, - 'menu_position' => 8, - 'menu_icon' => 'dashicons-calendar', - 'rewrite' => array( 'slug' => $this->calendar_cpt_options['slug'] ), - 'has_archive' => $this->calendar_cpt_options['slug'], - 'capability_type' => 'post', - 'hierarchical' => false, - 'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'custom-fields', 'sticky' ), - ) - ); - - $calendar_taxonomies = self::get_calendar_taxonomy_map(); - foreach ( $calendar_taxonomies as $taxonomy_name=>$taxonomy ) { - $taxonomy_options = array( - 'hierarchical' => $taxonomy['hierarchical'], - 'labels' => array( - 'name' => $taxonomy['name_plural'], - 'singular_name' => $taxonomy['name'], - 'search_items' => "Search {$taxonomy['name_plural']}", - 'all_items' => "All {$taxonomy['name_plural']}", - 'parent_item' => "Parent {$taxonomy['name']}", - 'parent_item_colon' => "Parent {$taxonomy['name']}:", - 'edit_item' => "Edit {$taxonomy['name']}", - 'update_item' => "Update {$taxonomy['name']}", - 'add_new_item' => "Add New {$taxonomy['name']}", - 'new_item_name' => "New {$taxonomy['name']}" - ), - 'show_admin_column' => true, - 'show_ui' => true, - 'query_var' => true, - ); - - register_taxonomy( $taxonomy_name, "{$this->plugin_name}-calendar", $taxonomy_options ); - } - - } - - /** - * Helper method to hold a map of structure from the groups custom post - * type custom fields to the API schema - * - * @static - * @access public - * @return array - */ - public static function get_groups_custom_fields_map() { - return array( - 'group_main_leader' => array( - 'api_mapping' => 'main_leader', - 'data_type' => 'object', - 'child_object' => array( - 'leader_full_name' => array( - 'api_mapping' => 'full_name', - 'data_type' => 'string' - ), - 'leader_email' => array( - 'api_mapping' => 'email', - 'data_type' => 'string' - ) - ) - ), - 'group_calendar_feed' => array( - 'api_mapping' => 'calendar_feed', - 'data_type' => 'string', - ), - 'addresses' => array( - 'api_mapping' => 'addresses', - 'data_type' => 'object', - 'child_object' => array( - 'address' => array( - 'api_mapping' => 'address', - 'data_type' => 'object', - 'child_object' => array( - 'longitude' => array( - 'api_mapping' => 'longitude', - 'data_type' => 'string' - ), - 'latitude' => array( - 'api_mapping' => 'latitude', - 'data_type' => 'string' - ), - 'address_line_1' => array( - 'api_mapping' => 'line_1', - 'data_type' => 'string' - ), - 'address_line_2' => array( - 'api_mapping' => 'line_2', - 'data_type' => 'string' - ), - ) - ), - ), - ), - ); - } - - /** - * Helper method to hold a map of structure from the calendar custom post - * type custom fields to the API schema - * - * @static - * @access public - * @return array - */ - public static function get_calendar_custom_fields_map() { - return array( - 'calendar_date' => array( - 'api_mapping' => 'date', - 'data_type' => 'string', - ), - 'calendar_start_time' => array( - 'api_mapping' => 'start_time', - 'data_type' => 'string', - ), - 'calendar_end_time' => array( - 'api_mapping' => 'end_time', - 'data_type' => 'string', - ), - 'calendar_duration' => array( - 'api_mapping' => 'event_duration', - 'data_type' => 'int', - ), - ); - } - - /** - * Helper method to hold a map of structure from the groups custom post - * type taxonomies to the API schema - * - * @static - * @access public - * @return array - */ - public static function get_groups_taxonomy_map() { - - return array( - 'group_areas' => array( - 'name' => 'Area', - 'name_plural' => 'Areas', - 'hierarchical' => true, - 'api_mapping' => 'area' - ), - 'group_days' => array( - 'name' => 'Day', - 'name_plural' => 'Days', - 'hierarchical' => true, - 'api_mapping' => 'meeting_day' - ), - 'group_types' => array( - 'name' => 'Type', - 'name_plural' => 'Types', - 'hierarchical' => true, - 'api_mapping' => 'group_type' - ), - 'group_times' => array( - 'name' => 'Time', - 'name_plural' => 'Times', - 'hierarchical' => true, - 'api_mapping' => 'meeting_time' - ), - 'group_departments' => array( - 'name' => 'Department', - 'name_plural' => 'Departments', - 'hierarchical' => true, - 'api_mapping' => 'department' - ), - 'group_tags' => array( - 'name' => 'Group Tag', - 'name_plural' => 'Group Tags', - 'hierarchical' => false, - 'api_mapping' => array( - 'childcare_provided' => 'Childcare Provided' - ) - ), - ); - } - - /** - * Helper method to hold a map of structure from the events custom post - * type taxonomies to the API schema - * - * @static - * @access public - * @return array - */ - public static function get_calendar_taxonomy_map() { - - return array( - 'calendar_event_type' => array( - 'name' => 'Type', - 'name_plural' => 'Types', - 'hierarchical' => true, - 'api_mapping' => 'event_type' - ), - 'calendar_group_name' => array( - 'name' => 'Group Name', - 'name_plural' => 'Group Names', - 'hierarchical' => true, - 'api_mapping' => 'group_name' - ), - 'calendar_grouping_name' => array( - 'name' => 'Grouping Name', - 'name_plural' => 'Grouping Names', - 'hierarchical' => true, - 'api_mapping' => 'grouping_name' - ), - ); - } - -} diff --git a/admin/class-ccb-core-settings-field.php b/admin/class-ccb-core-settings-field.php deleted file mode 100644 index 37b25fe..0000000 --- a/admin/class-ccb-core-settings-field.php +++ /dev/null @@ -1,283 +0,0 @@ - - */ -class CCB_Core_Settings_Field extends CCB_Core_Plugin { - - /** - * The key for the field in the settings array - * - * @since 0.9.0 - * @access protected - * @var string $field_id - */ - protected $field_id; - - /** - * An array of field settings - * - * @since 0.9.0 - * @access protected - * @var array $field - */ - protected $field; - - /** - * The existing settings currently stored - * - * @since 0.9.0 - * @access protected - * @var array $existing_settings - */ - protected $existing_settings; - - /** - * Initialize the class and set its properties. - * - * @access public - * @since 0.9.0 - * @return void - */ - public function __construct( $field_id, $field ) { - - parent::__construct(); - - $this->field_id = $field_id; - $this->field = $field; - $this->existing_settings = get_option( $this->plugin_settings_name ); - - } - - /** - * General method that calls correct field render method based on config - * - * @access public - * @since 0.9.0 - * @return void - */ - public function render_field() { - if ( isset( $this->field['field_render_function'] ) && is_callable( array( $this, $this->field['field_render_function'] ) ) ) { - call_user_func( array( $this, $this->field['field_render_function'] ) ); - if ( isset( $this->field['field_tooltip'] ) ) { - echo ''; - } - } - } - - /** - * Render a textfield - * - * @access protected - * @since 0.9.0 - * @return void - */ - protected function render_text() { - $value = ''; - $attributes = $this->build_attributes_string(); - - if ( isset( $this->existing_settings[ $this->field_id ] ) ) { - $value = $this->existing_settings[ $this->field_id ]; - } - - echo "field['field_placeholder']}\" name=\"{$this->plugin_settings_name}[{$this->field_id}]\" value=\"{$value}\" {$attributes} />"; - } - - /** - * Render a switch button (checkbox) - * - * @access protected - * @since 0.9.0 - * @return void - */ - protected function render_switch() { - $value = ''; - $attributes = $this->build_attributes_string(); - - if ( isset( $this->existing_settings[ $this->field_id ] ) ) { - $value = $this->existing_settings[ $this->field_id ]; - } - - echo "plugin_settings_name}[{$this->field_id}]\" value=\"1\" " . checked( $value, '1', false ) . "{$attributes} />"; - } - - /** - * Render a slider widget (textfield) - * - * @access protected - * @since 0.9.0 - * @return void - */ - protected function render_slider() { - $value = $this->field['field_default']; - $attributes = $this->build_attributes_string(); - - if ( isset( $this->existing_settings[ $this->field_id ] ) ) { - $value = $this->existing_settings[ $this->field_id ]; - } - - echo "
plugin_settings_name}[{$this->field_id}]\" value=\"{$value}\"{$attributes} data-sibling=\"{$this->field_id}-readonly\" data-min=\"{$this->field['field_options']['min']}\" data-max=\"{$this->field['field_options']['max']}\" />
"; - echo ' ' . $this->field['field_options']['units'] . ''; - } - - /** - * Render a jQuery date picker - * - * @access protected - * @since 0.9.0 - * @return void - */ - protected function render_date_picker() { - $value = ''; - $attributes = $this->build_attributes_string(); - - if ( isset( $this->existing_settings[ $this->field_id ] ) ) { - $value = $this->existing_settings[ $this->field_id ]; - } - - echo "
plugin_settings_name}[{$this->field_id}]\" data-value=\"{$value}\" {$attributes} />
"; - } - - /** - * Render radio buttons - * - * @access protected - * @since 0.9.0 - * @return void - */ - protected function render_radio() { - - $value = $this->field['field_default']; - $attributes = $this->build_attributes_string(); - - if ( isset( $this->existing_settings[ $this->field_id ] ) ) { - $value = $this->existing_settings[ $this->field_id ]; - } - - echo '
'; - foreach ( (array) $this->field['field_options'] as $option_value => $option_label ) { - echo "
"; - } - echo '
'; - - } - - /** - * Render a username and password field - * - * @access protected - * @since 0.9.0 - * @return void - */ - protected function render_credentials() { - $value = array(); - if ( isset( $this->existing_settings[ $this->field_id ] ) ) { - $value['username'] = $this->existing_settings[ $this->field_id ]['username']; - $value['password'] = $this->decrypt( $this->existing_settings[ $this->field_id ]['password'] ); - } - else { - $value['username'] = ''; - $value['password'] = ''; - } - echo << - -HTML; - } - - /** - * Render a test credentials button - * - * @access protected - * @since 0.9.0 - * @return void - */ - protected function render_test_credentials() { - if ( ! isset( $this->existing_settings['credentials']['username'] ) || empty( $this->existing_settings['credentials']['username'] ) || ! isset( $this->existing_settings['credentials']['password'] ) || empty( $this->existing_settings['credentials']['password'] ) ) { - echo '

Please enter your API Credentials

'; - } - elseif ( ! isset( $this->existing_settings['subdomain'] ) || empty( $this->existing_settings['subdomain'] ) ) { - echo '

Please enter your Church Community Builder subdomain.

'; - } - else { - echo << - -
- -HTML; - } - } - - /** - * Render a manual sync button - * - * @access protected - * @since 0.9.0 - * @return void - */ - protected function render_manual_sync() { - $sync_in_progress = get_transient( $this->plugin_name . '-sync-in-progress' ); - $sync_message = ''; - $button_disabled = ''; - $spinner_active = ''; - - if ( $sync_in_progress ) { - $sync_message = '
Syncronization in progress... You can safely navigate away from this page while we work hard in the background. (It should be just a moment).
'; - $button_disabled = 'disabled'; - $spinner_active = 'is-active'; - } - echo << - -
- {$sync_message} - -HTML; - } - - /** - * Render an area to display the latest sync results - * - * @access protected - * @since 0.9.0 - * @return void - */ - protected function render_latest_results() { - echo << -
- -HTML; - } - - /** - * Helper method to build HTML attributes from the config - * - * @access protected - * @since 0.9.0 - * @return string - */ - protected function build_attributes_string() { - $attributes = ''; - if ( isset( $this->field['field_attributes'] ) && ! empty( $this->field['field_attributes'] ) ) { - foreach ( $this->field['field_attributes'] as $attr_name => $attr_value ) { - $attributes .= " {$attr_name}='{$attr_value}'"; - } - } - return $attributes; - } -} diff --git a/admin/class-ccb-core-settings.php b/admin/class-ccb-core-settings.php deleted file mode 100644 index b8cd58a..0000000 --- a/admin/class-ccb-core-settings.php +++ /dev/null @@ -1,462 +0,0 @@ - - */ -class CCB_Core_Settings extends CCB_Core_Plugin { - - /** - * Initialize the class and set its properties. - * - * @access public - * @since 0.9.0 - * @return void - */ - public function __construct() { - - parent::__construct(); - - } - - /** - * Validate the settings fields based on the settings config - * - * @access public - * @since 0.9.0 - * @param array $input - * @return array $current_settings - */ - public function validate_settings( $input ) { - - $current_settings = get_option( $this->plugin_settings_name ); - $validation_hash = $this->generate_validation_hash(); - - if ( is_array( $validation_hash ) && ! empty( $validation_hash ) ) { - - foreach ( $validation_hash as $field_id => $validation ) { - - if ( isset( $validation['field_validation'] ) ) { - switch ( $validation['field_validation'] ) { - - case 'alphanumeric': - if ( empty( $input[ $field_id ] ) || ctype_alnum( $input[ $field_id ] ) ) { - $current_settings[ $field_id ] = $input[ $field_id ]; - } - else { - add_settings_error( $field_id, $field_id, "Oops! {$validation['field_title']} can only contain letters and numbers." ); - } - break; - - case 'numeric': - if ( empty( $input[ $field_id ] ) || ctype_digit( $input[ $field_id ] ) ) { - $current_settings[ $field_id ] = $input[ $field_id ]; - } - else { - add_settings_error( $field_id, $field_id, "Oops! {$validation['field_title']} can only contain numbers." ); - } - break; - - case 'slug': - $input[ $field_id ] = strtolower( str_replace( ' ', '_', $input[ $field_id ] ) ); - // continue onto alphanumeric_extended validation - - case 'alphanumeric_extended': - if ( empty( $input[ $field_id ] ) || ! preg_match( '/[^\w\s-_]/', $input[ $field_id ] ) ) { - $current_settings[ $field_id ] = $input[ $field_id ]; - } - else { - add_settings_error( $field_id, $field_id, "Oops! {$validation['field_title']} can only contain letters, numbers, spaces, dashes, or underscores." ); - } - break; - - case 'encrypt': - - if ( ! empty( $input[ $field_id ]['password'] ) ) { - $encrypted_password = $this->encrypt( $input[ $field_id ]['password'] ); - if ( $encrypted_password ) { - $current_settings[ $field_id ]['password'] = $encrypted_password; - } - else { - add_settings_error( $field_id, $field_id, "Oops! We couldn't encrypt your password." ); - } - } - $current_settings[ $field_id ]['username'] = $input[ $field_id ]['username']; - - break; - - case 'switch': - - $current_settings[ $field_id ] = ( isset( $input[ $field_id ] ) && $input[ $field_id ] == '1' ? '1' : '' ); - break; - - default: - $current_settings[ $field_id ] = $input[ $field_id ]; - break; - - } - } - - } - - } - - return $current_settings; - } - - /** - * Helper function to create a name/value hash for quick validation - * - * @access protected - * @since 0.9.0 - * @return array $mapping - */ - protected function generate_validation_hash() { - $mapping = array(); - $page_id = $_POST['option_page']; - $settings_definitions = $this->get_settings_definitions(); - - if ( is_array( $settings_definitions ) && isset( $settings_definitions[ $page_id ] ) ) { - if ( isset( $settings_definitions[ $page_id ]['sections'] ) && ! empty( $settings_definitions[ $page_id ]['sections'] ) ) { - foreach ( $settings_definitions[ $page_id ]['sections'] as $section ) { - if ( isset( $section['fields'] ) && ! empty( $section['fields'] ) ) { - foreach ( $section['fields'] as $field_id => $field ) { - if ( isset( $field['field_validation'] ) ) { - $mapping[ $field_id ] = array( - 'field_title' => $field['field_title'], - 'field_validation' => $field['field_validation'], - ); - } - else { - $mapping[ $field_id ] = false; - } - } - } - } - } - } - return $mapping; - } - - /** - * The whopper config used to create all the settings - * - * @access public - * @since 0.9.0 - * @return array - */ - public function get_settings_definitions() { - return array( - $this->plugin_settings_name => array( - 'page_title' => 'About', - 'sections' => array( - 'about' => array( - 'section_title' => 'About', - // no fields needed for the about page - ), - ), - ), - $this->plugin_settings_name . '_api_settings' => array( - 'page_title' => 'API Settings', - 'sections' => array( - 'api_settings' => array( - 'section_title' => 'API Settings', - 'fields' => array( - 'subdomain' => array( - 'field_title' => 'Software Subdomain', - 'field_render_function' => 'render_text', - 'field_placeholder' => 'subdomain', - 'field_validation' => 'alphanumeric', - 'field_tooltip' => 'We just need the first part of your software URL (without "http://" and without ".ccbchurch.com").', - ), - 'credentials' => array( - 'field_title' => 'API Credentials', - 'field_render_function' => 'render_credentials', - 'field_validation' => 'encrypt', - 'field_tooltip' => 'This is the username and password for the API user in your Church Community Builder software.', - ), - 'test_credentials' => array( - 'field_title' => 'Test Credentials', - 'field_render_function' => 'render_test_credentials', - ), - ), - ), - ), - ), - $this->plugin_settings_name . '_groups' => array( - 'page_title' => 'Groups', - 'sections' => array( - 'groups' => array( - 'section_title' => 'Groups', - 'fields' => array( - 'groups-enabled' => array( - 'field_title' => 'Enable Groups', - 'field_render_function' => 'render_switch', - 'field_validation' => 'switch', - ), - 'groups-name' => array( - 'field_title' => 'Groups Display Name', - 'field_render_function' => 'render_text', - 'field_placeholder' => 'Groups', - 'field_validation' => 'alphanumeric_extended', - 'field_attributes' => array( 'data-requires' => '{"groups-enabled":1}' ), - 'field_tooltip' => 'This is what you call the groups in your church (i.e. Home Groups, Connections, Life Groups, etc.).', - ), - 'groups-slug' => array( - 'field_title' => 'Groups URL Name', - 'field_render_function' => 'render_text', - 'field_placeholder' => 'groups', - 'field_validation' => 'slug', - 'field_attributes' => array( 'data-requires' => '{"groups-enabled":1}' ), - 'field_tooltip' => 'This is typically where your theme will display all the groups. WordPress calls this a "slug".', - ), - 'groups-import-images' => array( - 'field_title' => 'Also Import Group Images?', - 'field_render_function' => 'render_radio', - 'field_options' => array( - 'yes' => 'Yes', - 'no' => 'No' - ), - 'field_validation' => '', - 'field_default' => 'no', - 'field_attributes' => array( 'data-requires' => '{"groups-enabled":1}' ), - 'field_tooltip' => "This will download the CCB Group Image and attach it as a Featured Image.
If you don't need group images, then disabling this feature will speed up the synchronization.", - ), - 'groups-advanced' => array( - 'field_title' => 'Enable Advanced Settings (Optional)', - 'field_render_function' => 'render_switch', - 'field_validation' => 'switch', - 'field_attributes' => array( 'data-requires' => '{"groups-enabled":1}' ), - ), - 'groups-exclude-from-search' => array( - 'field_title' => 'Exclude From Search?', - 'field_render_function' => 'render_radio', - 'field_options' => array( - 'yes' => 'Yes', - 'no' => 'No' - ), - 'field_validation' => '', - 'field_default' => 'no', - 'field_attributes' => array( 'data-requires' => '{"groups-enabled":1,"groups-advanced":1}' ), - ), - 'groups-publicly-queryable' => array( - 'field_title' => 'Publicly Queryable?', - 'field_render_function' => 'render_radio', - 'field_options' => array( - 'yes' => 'Yes', - 'no' => 'No' - ), - 'field_validation' => '', - 'field_default' => 'yes', - 'field_attributes' => array( 'data-requires' => '{"groups-enabled":1,"groups-advanced":1}' ), - ), - 'groups-show-ui' => array( - 'field_title' => 'Show In Admin UI?', - 'field_render_function' => 'render_radio', - 'field_options' => array( - 'yes' => 'Yes', - 'no' => 'No' - ), - 'field_validation' => '', - 'field_default' => 'yes', - 'field_attributes' => array( 'data-requires' => '{"groups-enabled":1,"groups-advanced":1}' ), - ), - 'groups-show-in-nav-menus' => array( - 'field_title' => 'Show In Navigation Menus?', - 'field_render_function' => 'render_radio', - 'field_options' => array( - 'yes' => 'Yes', - 'no' => 'No' - ), - 'field_validation' => '', - 'field_default' => 'no', - 'field_attributes' => array( 'data-requires' => '{"groups-enabled":1,"groups-advanced":1}' ), - ), - ), - ), - ), - ), - $this->plugin_settings_name . '_calendar' => array( - 'page_title' => 'Public Events', - 'sections' => array( - 'calendar' => array( - 'section_title' => 'Public Events', - 'fields' => array( - 'calendar-enabled' => array( - 'field_title' => 'Enable Events', - 'field_render_function' => 'render_switch', - 'field_validation' => 'switch', - ), - 'calendar-name' => array( - 'field_title' => 'Event Display Name', - 'field_render_function' => 'render_text', - 'field_placeholder' => 'Events', - 'field_validation' => 'alphanumeric_extended', - 'field_attributes' => array( 'data-requires' => '{"calendar-enabled":1}' ), - 'field_tooltip' => 'This is what you call the events in your church (i.e. Meetups, Hangouts, etc.).', - ), - 'calendar-slug' => array( - 'field_title' => 'Events URL Name', - 'field_render_function' => 'render_text', - 'field_placeholder' => 'events', - 'field_validation' => 'slug', - 'field_attributes' => array( 'data-requires' => '{"calendar-enabled":1}' ), - 'field_tooltip' => 'This is typically where your theme will display all the events. WordPress calls this a "slug".', - ), - 'calendar-advanced' => array( - 'field_title' => 'Enable Advanced Settings (Optional)', - 'field_render_function' => 'render_switch', - 'field_validation' => 'switch', - 'field_attributes' => array( 'data-requires' => '{"calendar-enabled":1}' ), - ), - 'calendar-date-range-type' => array( - 'field_title' => 'Date Range Type', - 'field_render_function' => 'render_radio', - 'field_options' => array( - 'relative' => 'Relative Range', - 'specific' => 'Specific Range' - ), - 'field_validation' => '', - 'field_default' => 'relative', - 'field_attributes' => array( 'class' => 'date-range-type', 'data-requires' => '{"calendar-enabled":1,"calendar-advanced":1}' ), - 'field_tooltip' => 'Relative: For example, always get the events from \'One week ago\', up to \'Eight weeks from now\'.
This is the best setting for most churches.

Specific: For example, only get events from \'6/1/2015\' to \'12/1/2015\'.
This setting is best if you want to tightly manage the events that get published.', - ), - 'calendar-relative-weeks-past' => array( - 'field_title' => 'How Far Back?', - 'field_render_function' => 'render_slider', - 'field_options' => array( - 'min' => '0', - 'max' => '26', - 'units' => 'weeks', - ), - 'field_default' => 1, - 'field_validation' => '', - 'field_attributes' => array( 'data-requires' => '{"calendar-enabled":1,"calendar-advanced":1,"calendar-date-range-type":"relative"}' ), - 'field_tooltip' => 'Every time we synchronize, how many weeks in the past should we look?(0 would be "today")', - ), - 'calendar-relative-weeks-future' => array( - 'field_title' => 'How Into The Future?', - 'field_render_function' => 'render_slider', - 'field_options' => array( - 'min' => '1', - 'max' => '52', - 'units' => 'weeks', - ), - 'field_default' => 16, - 'field_validation' => '', - 'field_attributes' => array( 'data-requires' => '{"calendar-enabled":1,"calendar-advanced":1,"calendar-date-range-type":"relative"}' ), - 'field_tooltip' => 'Every time we synchronize, how many weeks in the future should we look?', - ), - 'calendar-specific-start' => array( - 'field_title' => 'Specific Start Date', - 'field_render_function' => 'render_date_picker', - 'field_validation' => '', - 'field_attributes' => array( 'data-requires' => '{"calendar-enabled":1,"calendar-advanced":1,"calendar-date-range-type":"specific"}' ), - 'field_tooltip' => 'When synchronizing, we should get events that start after this date.
(Leave empty to always start "today")', - ), - 'calendar-specific-end' => array( - 'field_title' => 'Specific End Date', - 'field_render_function' => 'render_date_picker', - 'field_validation' => '', - 'field_attributes' => array( 'data-requires' => '{"calendar-enabled":1,"calendar-advanced":1,"calendar-date-range-type":"specific"}' ), - 'field_tooltip' => 'When synchronizing, we should get events that start before this date.
(Setting this too far into the future may cause the API to timeout)', - ), - 'calendar-exclude-from-search' => array( - 'field_title' => 'Exclude From Search?', - 'field_render_function' => 'render_radio', - 'field_options' => array( - 'yes' => 'Yes', - 'no' => 'No' - ), - 'field_validation' => '', - 'field_default' => 'no', - 'field_attributes' => array( 'data-requires' => '{"calendar-enabled":1,"calendar-advanced":1}' ), - ), - 'calendar-publicly-queryable' => array( - 'field_title' => 'Publicly Queryable?', - 'field_render_function' => 'render_radio', - 'field_options' => array( - 'yes' => 'Yes', - 'no' => 'No' - ), - 'field_validation' => '', - 'field_default' => 'yes', - 'field_attributes' => array( 'data-requires' => '{"calendar-enabled":1,"calendar-advanced":1}' ), - ), - 'calendar-show-ui' => array( - 'field_title' => 'Show In Admin UI?', - 'field_render_function' => 'render_radio', - 'field_options' => array( - 'yes' => 'Yes', - 'no' => 'No' - ), - 'field_validation' => '', - 'field_default' => 'yes', - 'field_attributes' => array( 'data-requires' => '{"calendar-enabled":1,"calendar-advanced":1}' ), - ), - 'calendar-show-in-nav-menus' => array( - 'field_title' => 'Show In Navigation Menus?', - 'field_render_function' => 'render_radio', - 'field_options' => array( - 'yes' => 'Yes', - 'no' => 'No' - ), - 'field_validation' => '', - 'field_default' => 'no', - 'field_attributes' => array( 'data-requires' => '{"calendar-enabled":1,"calendar-advanced":1}' ), - ), - ), - ), - ), - ), - $this->plugin_settings_name . '_sync' => array( - 'page_title' => 'Synchronize', - 'sections' => array( - 'synchronize' => array( - 'section_title' => 'Synchronize', - 'fields' => array( - 'auto-sync' => array( - 'field_title' => 'Enable Auto Sync', - 'field_render_function' => 'render_switch', - 'field_validation' => 'switch', - ), - 'auto-sync-timeout' => array( - 'field_title' => 'Cache Expiration', - 'field_render_function' => 'render_slider', - 'field_options' => array( - 'min' => '10', - 'max' => '180', - 'units' => 'minutes', - ), - 'field_default' => 90, - 'field_validation' => '', - 'field_attributes' => array( 'data-requires' => '{"auto-sync":1}' ), - 'field_tooltip' => 'We keep a local copy (cache) of your Church Community Builder data for the best performance.
How often (in minutes) should we check for new data?
90 minutes is recommended.', - ), - 'manual-sync' => array( - 'field_title' => 'Manual Sync', - 'field_render_function' => 'render_manual_sync', - ), - 'latest-results' => array( - 'field_title' => 'Latest Sync Results', - 'field_render_function' => 'render_latest_results', - ), - ), - ), - ), - ), - ); - } -} diff --git a/admin/class-ccb-core-sync.php b/admin/class-ccb-core-sync.php deleted file mode 100644 index 988e824..0000000 --- a/admin/class-ccb-core-sync.php +++ /dev/null @@ -1,963 +0,0 @@ - - */ -class CCB_Core_Sync extends CCB_Core_Plugin { - - /** - * The subdomain of the ccb church installation - * - * @since 0.9.0 - * @access protected - * @var string $subdomain - */ - protected $subdomain; - - /** - * The ccb api username - * - * @since 0.9.0 - * @access protected - * @var string $username - */ - protected $username; - - /** - * The ccb api password - * - * @since 0.9.0 - * @access protected - * @var string $password - */ - protected $password; - - /** - * The CCB APIs we want to sync with - * - * @since 0.9.0 - * @access protected - * @var array $enabled_apis - */ - protected $enabled_apis = array(); - - /** - * The start date range for calendar events - * - * @since 0.9.0 - * @access protected - * @var string $calendar_start_date - */ - protected $calendar_start_date; - - /** - * The end date range for calendar events - * - * @since 0.9.0 - * @access protected - * @var string $calendar_end_date - */ - protected $calendar_end_date; - - /** - * Any valid service that the core API might integrate with - * - * @since 0.9.0 - * @access protected - * @var array $valid_services - */ - protected $valid_services; - - /** - * Whether or not to additionally import group images - * - * @since 0.9.5 - * @access protected - * @var array $valid_services - */ - protected $import_group_images; - - /** - * Initialize the class and set its properties. - * - * @since 0.9.0 - */ - public function __construct() { - - parent::__construct(); - - $settings = get_option( $this->plugin_settings_name ); - - $this->subdomain = $settings['subdomain']; - $this->username = $settings['credentials']['username']; - $this->password = $this->decrypt( $settings['credentials']['password'] ); - - if ( isset( $settings['groups-enabled'] ) && $settings['groups-enabled'] == 1 ) { - - $this->enabled_apis['group_profiles'] = true; - - if ( isset( $settings['groups-import-images'] ) && $settings['groups-import-images'] == 'yes' ) { - $this->import_group_images = true; - } - else { - $this->import_group_images = false; - } - - } - if ( isset( $settings['calendar-enabled'] ) && $settings['calendar-enabled'] == 1 ) { - - $this->enabled_apis['public_calendar_listing'] = true; - - // use sane defaults if this advanced setting isn't set - if ( ! isset( $settings['calendar-date-range-type'] ) ) { - - $this->calendar_start_date = date( 'Y-m-d', strtotime( '1 weeks ago') ); - $this->calendar_end_date = date( 'Y-m-d', strtotime( '+16 weeks' ) ); - - } - elseif ( $settings['calendar-date-range-type'] == 'relative' ) { - - $this->calendar_start_date = date( 'Y-m-d', strtotime( $settings['calendar-relative-weeks-past'] . ' weeks ago') ); - $this->calendar_end_date = date( 'Y-m-d', strtotime( '+' . $settings['calendar-relative-weeks-future'] . ' weeks' ) ); - - } - elseif ( $settings['calendar-date-range-type'] == 'specific' ) { - - // TODO: Use localization for date formats other than U.S. - - if ( $settings['calendar-specific-start'] ) { - - $last_year = strtotime( '1 year ago' ); - $start_timestamp = strtotime( $settings['calendar-specific-start'] ); - - if ( abs( $start_timestamp - $last_year ) > 0 ) { - $this->calendar_start_date = date( 'Y-m-d', $start_timestamp ); - } - else { - $this->calendar_start_date = date( 'Y-m-d', $last_year ); - } - - } - else { - $this->calendar_start_date = date( 'Y-m-d' ); - } - - if ( $settings['calendar-specific-end'] ) { - - $next_year = strtotime( '+1 year' ); - $end_timestamp = strtotime( $settings['calendar-specific-end'] ); - - if ( abs( $next_year - $end_timestamp ) > 0 ) { - $this->calendar_end_date = date( 'Y-m-d', $end_timestamp ); - } - else { - $this->calendar_end_date = date( 'Y-m-d', $next_year ); - } - - } - else { - $this->calendar_end_date = date( 'Y-m-d', strtotime( '+1 year' ) ); - } - } - - } - - $this->valid_services = array( - array( - 'service_name' => 'api_status', - 'service_friendly_name' => 'Credentials', - ), - array( - 'service_name' => 'group_profiles', - 'params' => array( - 'describe_api' => '1' - ), - 'service_friendly_name' => 'Group Profiles API', - ), - array( - 'service_name' => 'public_calendar_listing', - 'params' => array( - 'describe_api' => '1' - ), - 'service_friendly_name' => 'Public Calendar Listing API', - ), - ); - - } - - /** - * Make a service call to the CCB API - * - * The $services array is in the format: - * $services = array( - * array ( - * 'service_name' => 'group_profiles', - * 'params' => array( - * 'modified_since' => '2015-06-01', - * 'include_participants' => 'false' - * ) - * ), - * array ( - * 'service_name' => 'public_calendar_listing', - * 'params' => array( - * 'date_start' => '2015-06-01', - * ) - * ), - * ) - * - * @since 0.9.0 - * @param array $services An array of services and parameters to call - * @access protected - * @return void - */ - protected function call_ccb_api( $services = array() ) { - - set_time_limit(600); - $full_response = array(); - - // for debugging purposes, set a constant and serialize an array like so: - // define( 'RESPONSE_FILE', serialize( array( 'filename' => 'some_file.xml', 'service_name' => 'group_profiles' ) ) ); - // file must be located in the /uploads/ccb-core/ folder - // this will prevent a real api call and will use an xml file - if ( WP_DEBUG == true && defined( 'RESPONSE_FILE' ) ) { - - $service = unserialize( RESPONSE_FILE ); - $upload_dir = wp_upload_dir(); - $filepath = trailingslashit( trailingslashit( $upload_dir['basedir'] ) . $this->plugin_name ) . $service['filename']; - - if ( file_exists( $filepath ) ) { - $response_body = file_get_contents( $filepath ); - libxml_use_internal_errors(true); - $response_xml = simplexml_load_string( $response_body ); - - if ( is_object( $response_xml ) ) { - $full_response['success'] = true; - $full_response[ $service['service_name'] ] = $response_xml; - } - - return $full_response; - } - - } - - if ( ! empty( $services ) && is_array( $services ) ) { - - foreach ( $services as $service ) { - - $params = ''; - if ( isset( $service['params'] ) && ! empty( $service['params'] ) ) { - $params = http_build_query($service['params']); - } - - $api_url = "https://{$this->subdomain}.ccbchurch.com/api.php?srv={$service['service_name']}&{$params}"; - $post_args = array( - 'body' => array(), - 'timeout' => '600', - 'redirection' => '15', - 'httpversion' => '1.0', - 'blocking' => true, - 'headers' => array( - 'Authorization' => 'Basic ' . base64_encode( "{$this->username}:{$this->password}" ) - ), - 'cookies' => array() - ); - - $response = wp_remote_post( $api_url, $post_args ); - $response_code = wp_remote_retrieve_response_code( $response ); - - if ( $response_code != 200 ) { - $full_response['success'] = false; - $full_response['message'] = "There was a problem connecting with the Church Community Builder API - Response Code: {$response_code}"; - break; - } - else { - try { - libxml_use_internal_errors(true); - $response_body = wp_remote_retrieve_body( $response ); - $response_xml = simplexml_load_string( $response_body ); - - if ( is_object( $response_xml ) ) { - $full_response['success'] = true; - $full_response[ $service['service_name'] ] = $response_xml; - } - else { - $full_response['success'] = false; - $full_response['message'] = 'Oops, something went wrong while trying to read the API response. Is your subdomain correct?'; - break; - } - } - catch ( Exception $ex ) { - $full_response['success'] = false; - $full_response['message'] = 'Oops, something went wrong while trying to read the API response. Is your subdomain correct?'; - break; - } - } - - // cache the xml response to the uploads folder if debug mode is on (testing purposes) - if ( WP_DEBUG == true ) { - - $now = new DateTime(); - $cache_filename = $service['service_name'] . '_' . $now->format( 'Y-m-d_His' ) . '.xml'; - $upload_dir = wp_upload_dir(); - - if ( wp_mkdir_p( trailingslashit( $upload_dir['basedir'] ) . $this->plugin_name ) ) { - // first delete any files that weren't created "today" so we don't spam the server over time - $files = preg_grep( '/' . $now->format( 'Y-m-d' ) . '/', glob( trailingslashit( trailingslashit( $upload_dir['basedir'] ) . $this->plugin_name ) . '*' ), PREG_GREP_INVERT ); - foreach ( $files as $file ) { - if ( is_file( $file ) ) { - @unlink( $file ); - } - } - - $upload_file_path = trailingslashit( $upload_dir['basedir'] ) . trailingslashit( $this->plugin_name ) . $cache_filename; - file_put_contents( $upload_file_path, $response_body ); - } - - } - } - } - else { - $full_response['success'] = false; - $full_response['message'] = 'You tried to kick off a syncronization on ' . date( 'F j, Y @ h:i:s a (e)' ) . " but didn't have any integrations enabled (see each service tab)."; - } - - return $full_response; - } - - /** - * Perform a synchronization - * - * @access public - * @since 0.9.0 - * @return void - */ - public function sync() { - - // check for a transient that assumes a sync in in progress - if ( get_transient( $this->plugin_name . '-sync-in-progress' ) ) { - return; - } - else { - set_transient( $this->plugin_name . '-sync-in-progress', true, 60*20 ); - } - - $services = array(); - $current_time = time(); - - // GROUP PROFILES - if ( $this->enabled_apis['group_profiles'] ) { - - $include_participants = false; - $include_participants = apply_filters( 'ccb_include_group_participants', $include_participants ); - - $services[] = array( - 'service_name' => 'group_profiles', - 'params' => array( - 'include_participants' => $include_participants, - ), - ); - - } - - // PUBLIC CALENDAR LISTING - if ( $this->enabled_apis['public_calendar_listing'] ) { - - $services[] = array( - 'service_name' => 'public_calendar_listing', - 'params' => array( 'date_start' => $this->calendar_start_date, 'date_end' => $this->calendar_end_date ), - ); - - } - - $full_response = $this->call_ccb_api( $services ); - $validation_results = $this->validate_response( $full_response ); - // a data structure to hold a unique status of the latest sync results, stored in the db - $latest_sync = array(); - - if ( $validation_results['success'] == true ) { - - // check if any services failed so we can abort the sync and show different messaging - $service_failure = false; - foreach ( $validation_results['services'] as $service ) { - if ( $service['success'] == false ) { - $service_failure = true; - break; - } - } - - if ( $service_failure ) { - - $messages = array(); - - foreach ( $validation_results['services'] as $service ) { - - if ( $service['success'] == false ) { - $messages[] = 'We were not able to successfully synchronize with the ' . $service['service_name'] . ' service on ' . date( 'F j, Y @ h:i:s a (e)', $current_time ) . '. ' . $service['message']; - } - else { - $messages[] = 'We were able to successfully contact the ' . $service['service_name'] . ' service on ' . date( 'F j, Y @ h:i:s a (e)', $current_time ) . ', however we cancelled the synchronization because of other service errors.'; - } - } - - $message = implode( '

', $messages ); - $latest_sync = array( - 'success' => false, - 'message' => $message, - ); - } - else { - - $this->import_cpts( $full_response ); - - $latest_sync = array( - 'success' => true, - 'message' => 'We last successfully synchronized with the Church Community Builder API on ' . date( 'F j, Y @ h:i:s a (e)', $current_time ), - ); - } - } - else { - $latest_sync = array( - 'success' => false, - 'message' => $validation_results['message'] . ' We made the last attempt on ' . date( 'F j, Y @ h:i:s a (e)', $current_time ), - ); - } - - $latest_sync['timestamp'] = $current_time; - delete_transient( $this->plugin_name . '-sync-in-progress' ); - update_option( $this->plugin_name . '-latest-sync', $latest_sync ); - - } - - /** - * Tests API connection, credentials, and specific services - * as defined in the constructor - * - * @access public - * @since 0.9.0 - * @return string - */ - public function test_api_credentials() { - - $full_response = $this->call_ccb_api( $this->valid_services ); - delete_transient( $this->plugin_name . '-sync-in-progress' ); - return $this->validate_response( $full_response ); - - } - - /** - * Takes a CCB API response and parses it for basic business rules. - * Returns an array of successes, failures, and messages - * - * @param mixed $full_response - * @access protected - * @since 0.9.0 - * @return array - */ - protected function validate_response( $full_response ) { - - $validation_results = array(); - - if ( $full_response['success'] ) { - - $validation_results['success'] = true; - $validation_results['services'] = array(); - - foreach ( $this->valid_services as $service ) { - - if ( ! empty ( $full_response[ $service['service_name'] ] ) ) { - - $result_array = array(); - - if ( isset( $full_response[ $service['service_name'] ]->response->errors ) ) { - if ( isset( $full_response[ $service['service_name'] ]->response->errors->error ) && ! empty( $full_response[ $service['service_name'] ]->response->errors->error ) ) { - $result_array = array( - 'success' => false, - 'label' => $service['service_friendly_name'], - 'service_name' => $service['service_name'], - 'message' => 'The API responded with the message:
"' . $full_response[ $service['service_name'] ]->response->errors->error . '"', - ); - } - else { - $result_array = array( - 'success' => false, - 'label' => $service['service_friendly_name'], - 'service_name' => $service['service_name'], - 'message' => 'The API did not provide any other information', - ); - } - } - else { - $result_array = array( - 'success' => true, - 'label' => $service['service_friendly_name'], - 'service_name' => $service['service_name'], - 'message' => 'Success', - ); - } - - $validation_results['services'][] = $result_array; - } - - } - - } - else { - // the entire call was a failure. a sad sad failure. - $validation_results['success'] = false; - $validation_results['message'] = $full_response['message']; - } - - return $validation_results; - } - - /** - * Parses the XML response, deletes existing CPTs, and imports CCB data - * - * @param mixed $full_response - * @since 0.9.0 - * @access protected - * @return void - */ - protected function import_cpts( $full_response ) { - - global $wpdb; - // temporarily disable counting for performance - wp_defer_term_counting( true ); - wp_defer_comment_counting( true ); - // temporarily disable autocommit - $wpdb->query( 'SET autocommit = 0;' ); - - - // GROUP PROFILES - if ( $this->enabled_apis['group_profiles'] == true && isset( $full_response['group_profiles']->response->groups->group ) && ! empty( $full_response['group_profiles']->response->groups->group ) ) { - - $groups_taxonomy_map = CCB_Core_CPTs::get_groups_taxonomy_map(); - $groups_taxonomy_map = apply_filters( 'ccb_get_groups_taxonomy_map', $groups_taxonomy_map ); - - $groups_custom_fields_map = CCB_Core_CPTs::get_groups_custom_fields_map(); - $groups_custom_fields_map = apply_filters( 'ccb_get_groups_custom_fields_map', $groups_custom_fields_map ); - - // delete the existing taxonomy terms - foreach ( $groups_taxonomy_map as $taxonomy_name => $taxonomy ) { - $terms = get_terms( $taxonomy_name, array( 'fields' => 'ids', 'hide_empty' => false ) ); - if ( ! empty( $terms ) ) { - foreach ( $terms as $term_value ) { - wp_delete_term( $term_value, $taxonomy_name ); - } - } - } - - // delete existing custom posts - $custom_posts = get_posts( array( 'post_type' => $this->plugin_name . '-groups', 'posts_per_page' => -1 ) ); - foreach( $custom_posts as $custom_post ) { - - // delete the post thumbnail if it exists before deleting the post - $thumbnail_id = get_post_thumbnail_id( $custom_post->ID ); - if ( $thumbnail_id ) { - wp_delete_attachment( $thumbnail_id, true ); - } - - wp_delete_post( $custom_post->ID, true); - } - - // commit the deletes now - $wpdb->query( 'COMMIT;' ); - - // keep track of whether or not a default image has already been imported - $default_attachment = 0; - - foreach ( $full_response['group_profiles']->response->groups->group as $group ) { - - // only allow publicly listed and active groups to be imported - if ( $group->inactive == 'false' && $group->public_search_listed == 'true' ) { - - $group_id = 0; - foreach( $group->attributes() as $key => $value ) { - if ( $key == 'id' ) { - $group_id = (int) $value; - break; - } - } - - // insert group post - $group_post_atts = array( - 'post_title' => $group->name, - 'post_name' => $group->name, - 'post_content' => $group->description, - 'post_status' => 'publish', - 'post_type' => $this->plugin_name . '-groups', - ); - $post_id = wp_insert_post( $group_post_atts ); - - // insert hierarchial taxonomy values (categories) and non-hierarchial taxonomy values (tags) - $taxonomy_atts = $this->get_taxonomy_atts( $group, $groups_taxonomy_map ); - if ( ! empty( $taxonomy_atts ) ) { - foreach ( $taxonomy_atts as $taxonomy_attribute ) { - wp_set_post_terms( $post_id, $taxonomy_attribute['terms'], $taxonomy_attribute['taxonomy'], true ); - } - } - - // insert custom fields - $custom_fields_atts = $this->get_custom_fields_atts( $group, $groups_custom_fields_map ); - if ( ! empty( $custom_fields_atts ) ) { - foreach ( $custom_fields_atts as $field_key => $custom_fields_attribute ) { - add_post_meta( $post_id, $custom_fields_attribute['field_name'], $custom_fields_attribute['field_value'] ); - } - } - - // download and attach the group image as the featured image - if ( isset( $group->image ) && $this->import_group_images == true ) { - - $group_image_url = esc_url_raw( $group->image ); - - if ( ! empty( $group_image_url ) ) { - - // handle default images - if ( strpos( $group_image_url, 'default' ) ) { - if ( ! $default_attachment ) { - $attachment_result = $this->create_media_image( 'default', 0, $group_image_url ); - if ( $attachment_result ) { - $default_attachment = $attachment_result; - set_post_thumbnail( $post_id, $default_attachment ); - } - } - else { - set_post_thumbnail( $post_id, $default_attachment ); - } - } - else { - $attachment_result = $this->create_media_image( $group->name, $post_id, $group_image_url ); - if ( $attachment_result ) { - set_post_thumbnail( $post_id, $attachment_result ); - } - } - } - - } - - } - - } - - // commit the inserts now - $wpdb->query( 'COMMIT;' ); - - } - - // PUBLIC CALENDAR LISTING - if ( $this->enabled_apis['public_calendar_listing'] == true && isset( $full_response['public_calendar_listing']->response->items->item ) && ! empty( $full_response['public_calendar_listing']->response->items->item ) ) { - - $calendar_taxonomy_map = CCB_Core_CPTs::get_calendar_taxonomy_map(); - $calendar_taxonomy_map = apply_filters( 'ccb_get_calendar_taxonomy_map', $calendar_taxonomy_map ); - - $calendar_custom_fields_map = CCB_Core_CPTs::get_calendar_custom_fields_map(); - $calendar_custom_fields_map = apply_filters( 'ccb_get_calendar_custom_fields_map', $calendar_custom_fields_map ); - - // delete the existing taxonomy terms - foreach ( $calendar_taxonomy_map as $taxonomy_name => $taxonomy ) { - $terms = get_terms( $taxonomy_name, array( 'fields' => 'ids', 'hide_empty' => false ) ); - if ( ! empty( $terms ) ) { - foreach ( $terms as $term_value ) { - wp_delete_term( $term_value, $taxonomy_name ); - } - } - } - - // delete existing custom posts - $custom_posts = get_posts( array( 'post_type' => $this->plugin_name . '-calendar', 'posts_per_page' => -1 ) ); - foreach( $custom_posts as $custom_post ) { - wp_delete_post( $custom_post->ID, true); - } - - // commit the deletes now - $wpdb->query( 'COMMIT;' ); - - foreach ( $full_response['public_calendar_listing']->response->items->item as $event ) { - - // insert event post - $event_post_atts = array( - 'post_title' => $event->event_name, - 'post_name' => $event->event_name, - 'post_content' => $event->event_description, - 'post_status' => 'publish', - 'post_type' => $this->plugin_name . '-calendar', - ); - $post_id = wp_insert_post( $event_post_atts ); - - // insert hierarchial taxonomy values (categories) and non-hierarchial taxonomy values (tags) - $taxonomy_atts = $this->get_taxonomy_atts( $event, $calendar_taxonomy_map ); - if ( ! empty( $taxonomy_atts ) ) { - foreach ( $taxonomy_atts as $taxonomy_attribute ) { - wp_set_post_terms( $post_id, $taxonomy_attribute['terms'], $taxonomy_attribute['taxonomy'], true ); - } - } - - // insert custom fields - $custom_fields_atts = $this->get_custom_fields_atts( $event, $calendar_custom_fields_map ); - if ( ! empty( $custom_fields_atts ) ) { - foreach ( $custom_fields_atts as $field_key => $custom_fields_attribute ) { - add_post_meta( $post_id, $custom_fields_attribute['field_name'], $custom_fields_attribute['field_value'] ); - } - } - - } - - // commit the inserts now - $wpdb->query( 'COMMIT;' ); - - } - - // re-enable autocommit - $wpdb->query( 'SET autocommit = 1;' ); - // re-enable counting - wp_defer_term_counting( false ); - wp_defer_comment_counting( false ); - - } - - /** - * Uses a taxonomy map to build out the categories and tags - * for a CCB custom post type - * - * @param mixed $post_data - * @param array $taxonomy_map - * @access protected - * @since 0.9.0 - * @return void - */ - protected function get_taxonomy_atts( $post_data, $taxonomy_map ) { - - foreach( $taxonomy_map as $taxonomy_name => $taxonomy ) { - - if ( $taxonomy['hierarchical'] ) { - - $taxonomy_value = (string) $post_data->$taxonomy['api_mapping']; - - if ( ! empty( $taxonomy_value ) ) { - - $term_id = term_exists( $taxonomy_value, $taxonomy_name ); - if ( $term_id ) { - $terms_collection[] = array( - 'terms' => $term_id['term_id'], - 'taxonomy' => $taxonomy_name, - ); - } - else { - $new_term = wp_insert_term( $taxonomy_value, $taxonomy_name ); - $terms_collection[] = array( - 'terms' => $new_term['term_id'], - 'taxonomy' => $taxonomy_name, - ); - } - - } - - } - else { - - if ( isset( $taxonomy['api_mapping'] ) && ! empty( $taxonomy['api_mapping'] ) ) { - - foreach ( $taxonomy['api_mapping'] as $api_mapping => $tag_name ) { - - $tag_value = ( $post_data->$api_mapping == 'true' ? $tag_name : false ); - if ( $tag_value ) { - $terms_collection[] = array( - 'terms' => $tag_value, - 'taxonomy' => $taxonomy_name, - ); - } - - } - - } - } - - } - - return $terms_collection; - - } - - /** - * Uses a custom fields map to build out the custom fields - * for a CCB custom post type - * - * @param mixed $post_data - * @param array $custom_fields_map - * @param string $parent_field_name - * @access protected - * @since 0.9.0 - * @return void - */ - protected function get_custom_fields_atts( $post_data, $custom_fields_map, $parent_field_name = '' ) { - - $custom_fields_collection = array(); - - foreach( $custom_fields_map as $field_name => $field_data ) { - - switch ( $field_data['data_type'] ) { - case 'string': - $field_value = (string) $post_data->$field_data['api_mapping']; - $custom_fields_collection[] = array( - 'field_name' => $field_name, - 'field_value' => $field_value, - ); - break; - case 'int': - $field_value = (int) $post_data->$field_data['api_mapping']; - $custom_fields_collection[] = array( - 'field_name' => $field_name, - 'field_value' => $field_value, - ); - break; - case 'object': - if ( isset( $field_data['child_object'] ) && ! empty( $field_data['child_object'] ) ) { - // some child objects may be collections of objects - if ( count( $post_data->$field_data['api_mapping'] ) > 1 ) { - $collection_grouping = array(); - foreach ( $post_data->$field_data['api_mapping'] as $key => $child_field_data ) { - if ( is_object ( $child_field_data ) ) { - $collection_grouping[] = $this->get_custom_fields_atts( $child_field_data, $field_data['child_object'], $field_name ); - } - } - $prepared_field_collection = $this->prepare_field_collection( $parent_field_name, $collection_grouping ); - $custom_fields_collection[] = $prepared_field_collection; - } - else { - $child_custom_fields_collection = $this->get_custom_fields_atts( $post_data->$field_data['api_mapping'], $field_data['child_object'], $field_name ); - $custom_fields_collection = array_merge( $custom_fields_collection, $child_custom_fields_collection ); - } - } - break; - } - - } - - return $custom_fields_collection; - - } - - /** - * Takes a multidimensional array of field collections and formats - * them into groups that are compatible with storage in a - * single custom field - * - * @param string $field_name - * @param array $collection_grouping - * @access protected - * @since 0.9.4 - * @return array - */ - protected function prepare_field_collection( $field_name, $collection_grouping ) { - - $flat_collection = array( - 'field_name' => $field_name, - 'field_value' => array(), - ); - - foreach( $collection_grouping as $grouping_value ) { - - $grouping_array = array(); - - foreach ( $grouping_value as $value_pair ) { - - $grouping_array[] = array( - $value_pair['field_name'] => $value_pair['field_value'] - ); - - } - - $flat_collection['field_value'][] = $grouping_array; - - } - - return $flat_collection; - } - - /** - * Downloads an image from a URL, uploads it to the Media Library, - * and then optionally attaches it to a post - * - * @param string $group_name - * @param int $post_id - * @param string $image_url - * @access protected - * @since 0.9.5 - * @return mixed Returns a media id or false on failure - */ - protected function create_media_image( $group_name, $post_id, $image_url ) { - - // fetch the image from the cdn and store temporarily - $temp_file = download_url( $image_url ); - - if ( is_wp_error( $temp_file ) ) { - return false; - } - - // attempt to detect the mimetype based on the available functions - $extension = false; - if ( function_exists( 'exif_imagetype' ) && function_exists( 'image_type_to_extension' ) ) { - // open with exif - $image_type = exif_imagetype( $temp_file ); - if ( $image_type ) { - $extension = image_type_to_extension( $image_type ); - } - } - elseif ( function_exists( 'getimagesize' ) && function_exists( 'image_type_to_extension' ) ) { - // open with gd - $file_size = getimagesize( $temp_file ); - if ( isset( $file_size[2] ) ) { - $extension = image_type_to_extension( $file_size[2] ); - } - } - elseif ( function_exists( 'finfo_open' ) ) { - // open with fileinfo - $resource = finfo_open( FILEINFO_MIME_TYPE ); - $mimetype = finfo_file( $resource, $temp_file ); - finfo_close( $resource ); - if ( $mimetype ) { - $mimetype_array = explode( '/', $mimetype ); - $extension = '.' . $mimetype_array[1]; - } - } - - if ( $extension ) { - - $filename = 'ccb-' . sanitize_file_name( strtolower( $group_name ) ) . $extension; - - $file_array = array( - 'name' => $filename, - 'tmp_name' => $temp_file, - ); - - $media_id = media_handle_sideload( $file_array, $post_id ); - @unlink( $temp_file ); - - if ( is_wp_error( $media_id ) ) { - return false; - } - - return $media_id; - - } - else { - return false; - } - } - -} diff --git a/admin/index.php b/admin/index.php deleted file mode 100644 index e71af0e..0000000 --- a/admin/index.php +++ /dev/null @@ -1 +0,0 @@ - [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +TMPDIR=${TMPDIR-/tmp} +TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") +WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then + WP_TESTS_TAG="branches/$WP_VERSION" +elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + WP_TESTS_TAG="tags/${WP_VERSION%??}" + else + WP_TESTS_TAG="tags/$WP_VERSION" + fi +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi + +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p $TMPDIR/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip + unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ + mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then + # https serves multiple offers, whereas http serves single. + download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + LATEST_VERSION=${WP_VERSION%??} + else + # otherwise, scan the releases and get the most up to date minor version of the major release + local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` + LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) + fi + if [[ -z "$LATEST_VERSION" ]]; then + local ARCHIVE_NAME="wordpress-$WP_VERSION" + else + local ARCHIVE_NAME="wordpress-$LATEST_VERSION" + fi + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i .bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + fi + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db diff --git a/ccb-core.php b/ccb-core.php index 30470b6..1238846 100644 --- a/ccb-core.php +++ b/ccb-core.php @@ -2,48 +2,49 @@ /** * Church Community Builder Core API * - * @link http://www.wpccb.com + * @link https://www.wpccb.com * @since 0.9.0 * @package CCB_Core * * @wordpress-plugin * Plugin Name: Church Community Builder Core API - * Plugin URI: http://www.wpccb.com + * 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: 0.9.6 + * Version: 1.0.0 * Author: Jared Cobb - * Author URI: http://jaredcobb.com/ + * Author URI: https://www.jaredcobb.com/ * License: GPL-2.0+ * License URI: http://www.gnu.org/licenses/gpl-2.0.txt * Text Domain: ccb-core * Domain Path: /languages */ -// do not allow direct access to this file +// Do not allow direct access to this file. if ( ! defined( 'WPINC' ) ) { die; } -// parent class for entire plugin (name, version, other helpful properties and utility methods) -require_once plugin_dir_path( __FILE__ ) . 'includes/class-ccb-core-plugin.php'; +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.0' ); -// code that runs during plugin activation -require_once plugin_dir_path( __FILE__ ) . 'includes/class-ccb-core-activator.php'; -register_activation_hook( __FILE__, array( 'CCB_Core_Activator', 'activate' ) ); +// Code that runs during plugin activation and deactivation. +require_once CCB_CORE_PATH . 'includes/class-ccb-core-activator.php'; +register_activation_hook( __FILE__, [ 'CCB_Core_Activator', 'activate' ] ); +register_deactivation_hook( __FILE__, [ 'CCB_Core_Activator', 'deactivate' ] ); -// internationalization, dashboard-specific hooks, and public-facing site hooks. -require_once plugin_dir_path( __FILE__ ) . 'includes/class-ccb-core.php'; +// Internationalization, dashboard-specific hooks, and public-facing site hooks. +require_once CCB_CORE_PATH . 'includes/class-ccb-core.php'; /** * Begin execution of the plugin. * - * @since 0.9.0 + * @access public + * @return void */ function run_ccb_core() { - - $plugin_basename = plugin_basename( __FILE__ ); - $plugin = new CCB_Core( $plugin_basename ); - $plugin->run(); + $plugin = new CCB_Core(); } run_ccb_core(); diff --git a/admin/css/ccb-core-admin.css b/css/ccb-core-admin.css similarity index 64% rename from admin/css/ccb-core-admin.css rename to css/ccb-core-admin.css index 1a7cdb3..ba93755 100644 --- a/admin/css/ccb-core-admin.css +++ b/css/ccb-core-admin.css @@ -1,21 +1,6 @@ -.test-login-wrapper .button, .test-login-wrapper .spinner, .sync-wrapper .button, .sync-wrapper .spinner { - float: left; - margin-bottom: 20px; -} - -.ccb_core_settings-wrapper div.ajax-message { - width: 50%; - padding: 10px; - clear:both; - font-size: 13px; -} - -.ccb_core_settings-wrapper code { - background: none; - padding: 0; - font-size: 12px; - font-weight: bold; - color: #666666; +.ccb_core_settings-wrapper .spinner { + float: left; + margin-top: 5px; } .ccb_core_settings ul { diff --git a/admin/css/vendor/default.css b/css/vendor/default.css similarity index 100% rename from admin/css/vendor/default.css rename to css/vendor/default.css diff --git a/admin/css/vendor/default.date.css b/css/vendor/default.date.css similarity index 100% rename from admin/css/vendor/default.date.css rename to css/vendor/default.date.css diff --git a/admin/css/vendor/powerange.min.css b/css/vendor/powerange.min.css similarity index 100% rename from admin/css/vendor/powerange.min.css rename to css/vendor/powerange.min.css diff --git a/admin/css/vendor/switchery.min.css b/css/vendor/switchery.min.css similarity index 100% rename from admin/css/vendor/switchery.min.css rename to css/vendor/switchery.min.css diff --git a/admin/css/vendor/tipr.css b/css/vendor/tipr.css similarity index 100% rename from admin/css/vendor/tipr.css rename to css/vendor/tipr.css diff --git a/includes/class-ccb-core-activator.php b/includes/class-ccb-core-activator.php index 9677cd6..42462bf 100644 --- a/includes/class-ccb-core-activator.php +++ b/includes/class-ccb-core-activator.php @@ -2,7 +2,7 @@ /** * Fired during plugin activation * - * @link http://jaredcobb.com/ccb-core + * @link https://www.wpccb.com * @since 0.9.0 * * @package CCB_Core @@ -19,7 +19,7 @@ * @subpackage CCB_Core/includes * @author Jared Cobb */ -class CCB_Core_Activator extends CCB_Core_Plugin { +class CCB_Core_Activator { /** * Activation code @@ -27,7 +27,20 @@ class CCB_Core_Activator extends CCB_Core_Plugin { * @since 0.9.0 */ public static function activate() { - // TODO: check dependencies like mcrypt and memory limits + // TODO: check dependencies like mcrypt and memory limits. + } + + /** + * Deactivation code + * + * @since 1.0.0 + */ + public static function deactivate() { + // Ensure we do not have a scheduled hook. + $timestamp = wp_next_scheduled( 'ccb_core_auto_sync_hook' ); + if ( $timestamp ) { + wp_unschedule_event( $timestamp, 'ccb_core_auto_sync_hook' ); + } } } diff --git a/includes/class-ccb-core-admin-ajax.php b/includes/class-ccb-core-admin-ajax.php new file mode 100644 index 0000000..268a316 --- /dev/null +++ b/includes/class-ccb-core-admin-ajax.php @@ -0,0 +1,215 @@ + + */ +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 + * + * @since 1.0.0 + */ + public function __construct() { + add_action( 'wp_ajax_sync', [ $this, 'ajax_sync' ] ); + 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(); + } + + /** + * Launches a synchronization from an ajax hook and will respond + * with a non-blocking ajax response + * + * @access public + * @since 1.0.0 + * @return void + */ + public function ajax_sync() { + + check_ajax_referer( 'ccb_core_nonce', 'nonce' ); + + // Tell the user to move along and go about their business... + CCB_Core_Helpers::instance()->send_non_blocking_json_success(); + $result = $this->synchronizer->synchronize(); + + } + + /** + * Checks for an active synchronization from an ajax hook + * and responds with the transient value + * + * @access public + * @since 1.0.0 + * @return void + */ + public function ajax_poll_sync() { + + check_ajax_referer( 'ccb_core_nonce', 'nonce' ); + $sync_in_progress = get_transient( CCB_Core_Helpers::SYNC_STATUS_KEY ); + wp_send_json_success( $sync_in_progress ); + + } + + /** + * Gets the latest synchronization results from an ajax hook + * and encodes / echoes a standardized result array. + * + * @access public + * @since 1.0.0 + * @return void + */ + public function ajax_get_latest_sync() { + + check_ajax_referer( 'ccb_core_nonce', 'nonce' ); + + $message = ''; + $result = []; + + // Latest sync results are always stored as an option after a sync takes place. + $latest_sync = get_option( 'ccb_core_latest_sync_result' ); + + if ( ! empty( $latest_sync ) ) { + // Set the success result to the same result as the latest sync. + $result['success'] = $latest_sync['success']; + + if ( true === $latest_sync['success'] ) { + + $message .= esc_html( + sprintf( + // Translators: A formatted date/time. + __( 'The latest synchronization was successful on %s.', 'ccb-core' ), + get_date_from_gmt( + date( 'Y-m-d H:i:s', $latest_sync['timestamp'] ), + get_option( 'date_format' ) . ' \a\t ' . get_option( 'time_format' ) + ) + ) + ) . '
'; + + // Send detailed results for each service that has information. + if ( ! empty( $latest_sync['services'] ) ) { + foreach ( $latest_sync['services'] as $service => $service_result ) { + + $message .= esc_html( + sprintf( + // Translators: The service name. + __( 'Results from the %s service: ', 'ccb-core' ), + $service + ) + ); + + if ( isset( $service_result['insert_update']['processed'] ) ) { + $message .= esc_html( + sprintf( + // Translators: The number of records processed. + __( '%s records inserted / updated. ', 'ccb-core' ), + absint( $service_result['insert_update']['processed'] ) + ) + ); + } + + if ( isset( $service_result['delete']['processed'] ) ) { + $message .= esc_html( + sprintf( + // Translators: The number of records processed. + __( '%s records deleted. ', 'ccb-core' ), + absint( $service_result['delete']['processed'] ) + ) + ); + } + + $message .= '
'; + } + } + + } else { + $message .= esc_html( + sprintf( + __( '%1$s on %2$s', 'ccb-core' ), + $latest_sync['message'], + get_date_from_gmt( + date( 'Y-m-d H:i:s', $latest_sync['timestamp'] ), + get_option( 'date_format' ) . ' \a\t ' . get_option( 'time_format' ) + ) + ) + ) . '
'; + if ( ! empty( $latest_sync['services'] ) ) { + foreach ( $latest_sync['services'] as $service => $service_result ) { + if ( ! empty( $service_result['insert_update']['message'] ) ) { + $message .= $service_result['insert_update']['message'] . '
'; + } + if ( ! empty( $service_result['delete']['message'] ) ) { + $message .= $service_result['delete']['message'] . '
'; + } + } + } + } + } else { + $message .= esc_html__( 'We do not have any recent synchronizations', 'ccb-core' ); + } + + /** + * Filters the message that gets output to the user + * after a synchronization is finished. + * + * @since 1.0.0 + * + * @param string $message The message with the results. + * @param array $latest_sync The latest synchronization results. + */ + $result['message'] = apply_filters( 'ccb_core_ajax_results_message', $message, $latest_sync ); + wp_send_json_success( $result ); + + } + + /** + * Checks the CCB API credentials for a user from an ajax hook + * + * @access public + * @since 1.0.0 + * @return void + */ + public function ajax_test_credentials() { + check_ajax_referer( 'ccb_core_nonce', 'nonce' ); + $response = $this->api->get( 'api_status' ); + + if ( 'SUCCESS' === $response['status'] ) { + wp_send_json_success(); + } else { + wp_send_json_error( $response['error'] ); + } + } + +} + +new CCB_Core_Admin_AJAX(); diff --git a/includes/class-ccb-core-api.php b/includes/class-ccb-core-api.php new file mode 100644 index 0000000..4b52bb5 --- /dev/null +++ b/includes/class-ccb-core-api.php @@ -0,0 +1,279 @@ + + */ +class CCB_Core_API { + + /** + * Whether or not the API is ready for use + * + * @since 1.0.0 + * @access public + * @var bool $initialized + */ + public $initialized = false; + + /** + * Instance of the class + * + * @var CCB_Core_API + * @access private + * @static + */ + private static $instance; + + /** + * The subdomain of the ccb church installation + * + * @since 1.0.0 + * @access private + * @var string $subdomain + */ + private $subdomain; + + /** + * The ccb api username + * + * @since 1.0.0 + * @access private + * @var string $username + */ + private $username; + + /** + * The ccb api password + * + * @since 1.0.0 + * @access private + * @var string $password + */ + private $password; + + /** + * Unused constructor in the singleton pattern + * + * @access public + * @return void + */ + public function __construct() { + // Initialize this class with the instance() method. + } + + /** + * Returns the instance of the class + * + * @access public + * @static + * @return CCB_Core_API + */ + public static function instance() { + if ( ! isset( static::$instance ) ) { + static::$instance = new CCB_Core_API(); + static::$instance->setup(); + } + return static::$instance; + } + + /** + * Initial setup of the singleton + * + * @access private + * @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' ] ); + } + + /** + * Once the plugin is loaded, initialize the credentials + * + * @return void + */ + public function initialize_credentials() { + $settings = CCB_Core_Helpers::instance()->get_options(); + + if ( + ! empty( $settings['subdomain'] ) + && ! empty( $settings['credentials']['username'] ) + && ! empty( $settings['credentials']['password'] ) + ) { + $this->subdomain = $settings['subdomain']; + $this->username = $settings['credentials']['username']; + $this->password = CCB_Core_Helpers::instance()->decrypt( $settings['credentials']['password'] ); + if ( ! empty( $this->password ) && ! is_wp_error( $this->password ) ) { + $this->initialized = true; + } + } + } + + /** + * Sends a GET request to the CCB API + * + * @param string $service The service to request. + * @param array $data Optional parameters to send with the request. + * + * @since 1.0.0 + * @access public + * @return array The API response data. + */ + public function get( $service, $data = [] ) { + // Get the API response for the service. + return $this->request( 'GET', $service, $data ); + } + + /** + * Sends a POST request to the CCB API + * + * @param string $service The service to request. + * @param array $data Optional parameters to send with the request. + * + * @since 1.0.0 + * @access public + * @return array The API response data. + */ + public function post( $service, $data = [] ) { + // Get the API response for the service. + return $this->request( 'POST', $service, $data ); + } + + /** + * Executes a request against the Optimizely X API. + * + * @param string $method GET or POST. + * @param string $service The API service to execute against. + * @param array $data An optional array of data to include with the request. + * + * @since 1.0.0 + * @access private + * @return array + */ + private function request( $method, $service, $data = [] ) { + + if ( ! $this->initialized ) { + return [ + 'code' => 401, + 'error' => esc_html__( 'You are missing a subdomain, username, or password in the settings.', 'ccb-core' ), + 'status' => 'ERROR', + ]; + } + + $url = esc_url_raw( sprintf( 'https://%s.ccbchurch.com/api.php?srv=%s', $this->subdomain, $service ) ); + if ( empty( $url ) ) { + return [ + 'code' => 404, + 'error' => esc_html__( 'Invalid API URL.', 'ccb-core' ), + 'status' => 'ERROR', + ]; + } + + // Add authentication header to the request object. + $request = [ + 'timeout' => 60, + 'headers' => [ + 'Authorization' => 'Basic ' . base64_encode( + sprintf( + '%s:%s', + sanitize_text_field( $this->username ), + sanitize_text_field( $this->password ) + ) + ), + ], + ]; + + if ( 'POST' === $method ) { + $request['body'] = $data; + } elseif ( 'GET' === $method && ! empty( $data ) ) { + $url .= '&' . http_build_query( $data ); + } + + switch ( $method ) { + case 'GET': + $response = wp_safe_remote_get( $url, $request ); + break; + case 'POST': + $response = wp_safe_remote_post( $url, $request ); + break; + default: + return [ + 'code' => 403, + 'error' => esc_html__( 'Invalid request method.', 'ccb-core' ), + 'status' => 'ERROR', + ]; + } + + // Check for WordPress HTTP errors. + if ( is_wp_error( $response ) ) { + return [ + 'code' => 500, + 'error' => esc_html( $response->get_error_message() ), + 'status' => 'ERROR', + ]; + } + + // Build result object. + $result = [ + 'xml' => wp_remote_retrieve_body( $response ), + 'code' => absint( wp_remote_retrieve_response_code( $response ) ), + 'headers' => wp_remote_retrieve_headers( $response ), + ]; + + // Verify there are no HTTP errors. + if ( empty( $response ) + || $result['code'] < 200 + || $result['code'] > 204 + ) { + $result['status'] = 'ERROR'; + $result['error'] = esc_html( sprintf( __( 'The API returned an empty response or an error code: %s', 'ccb-core' ), $result['code'] ) ); + return $result; + } + + // Serialize the response XML into a SimpleXML object. + try { + libxml_use_internal_errors( true ); + $parsed_response = simplexml_load_string( $result['xml'] ); + if ( false === $parsed_response ) { + $result['error'] = esc_html__( 'Could not parse the XML response', 'ccb-core' ); + $result['status'] = 'ERROR'; + } else { + + $result['body'] = $parsed_response; + + // 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['status'] = 'ERROR'; + } else { + $result['status'] = 'SUCCESS'; + } + + } + } catch ( Exception $ex ) { + $result['error'] = esc_html( sprintf( __( 'Could not parse the XML response: %s', 'ccb-core' ), $ex->getMessage() ) ); + $result['status'] = 'ERROR'; + } + + return $result; + } + +} diff --git a/includes/class-ccb-core-cron.php b/includes/class-ccb-core-cron.php new file mode 100644 index 0000000..4d254fc --- /dev/null +++ b/includes/class-ccb-core-cron.php @@ -0,0 +1,117 @@ + + */ +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. + * + * @since 1.0.0 + */ + public function __construct() { + // Setup a custom cron schedule based on the user preferences. + // phpcs:ignore + add_filter( 'cron_schedules', [ $this, 'custom_cron_schedule' ] ); + // Setup the action and callback for the hook. + 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(); + } + + /** + * Create a custom cron schedule based on the + * timeout interval set by the user. + * + * @param array $schedules An array of cron schedules. + * @return array + */ + public function custom_cron_schedule( $schedules ) { + $settings = CCB_Core_Helpers::instance()->get_options(); + 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'] ) + ) ), + ]; + } + return $schedules; + } + + /** + * Callback method to detect when the settings have changed. + * + * We check for whether or not the auto sync was turned on / off and + * whether or not the user changed the timeout interval. This is + * how we register cron events and clean up invalid events. + * + * @param array $old_value The old settings array. + * @param array $new_value The new settings array. + * @return void + */ + public function cron_settings_changed( $old_value, $new_value ) { + // If the cron was enabled OR the timeout was changed. + if ( + ( empty( $old_value['auto_sync'] ) && ! empty( $new_value['auto_sync'] ) ) + || ( + ! empty( $old_value['auto_sync_timeout'] ) + && ! empty( $new_value['auto_sync_timeout'] ) + && $old_value['auto_sync_timeout'] !== $new_value['auto_sync_timeout'] + ) + ) { + $this->remove_existing_cron_events(); + wp_schedule_event( time(), 'ccb_core_schedule', 'ccb_core_auto_sync_hook' ); + } elseif ( ! empty( $old_value['auto_sync'] ) && empty( $new_value['auto_sync'] ) ) { + $this->remove_existing_cron_events(); + } + } + + /** + * Removes all CCB Core cron events. + * + * @return void + */ + private function remove_existing_cron_events() { + $timestamp = wp_next_scheduled( 'ccb_core_auto_sync_hook' ); + if ( $timestamp ) { + wp_unschedule_event( $timestamp, 'ccb_core_auto_sync_hook' ); + } + } + + /** + * The callback method of the cron event that kicks off a synchronization + * + * @return void + */ + public function auto_sync_callback() { + $this->synchronizer->synchronize(); + } + +} + +new CCB_Core_Cron(); diff --git a/includes/class-ccb-core-helpers.php b/includes/class-ccb-core-helpers.php new file mode 100644 index 0000000..8669b87 --- /dev/null +++ b/includes/class-ccb-core-helpers.php @@ -0,0 +1,297 @@ + + */ +class CCB_Core_Helpers { + + const SYNC_STATUS_KEY = 'ccb_core_sync_in_progress'; + + /** + * Instance of the Helper class + * + * @var CCB_Core_Helpers + * @access private + * @static + */ + private static $instance; + + /** + * The options set by the user + * + * @var array + */ + private $plugin_options = []; + + /** + * Unused constructor in the singleton pattern + * + * @access public + * @return void + */ + public function __construct() { + // Initialize this class with the instance() method. + } + + /** + * Returns the instance of the class + * + * @access public + * @static + * @return CCB_Core_Helpers + */ + public static function instance() { + if ( ! isset( static::$instance ) ) { + static::$instance = new CCB_Core_Helpers(); + static::$instance->setup(); + } + return static::$instance; + } + + /** + * Initial setup of the singleton + * + * @access private + * @return void + */ + private function setup() { + // Get any options the user may have set. + $this->plugin_options = get_option( 'ccb_core_settings' ); + // Ensure we refresh this singleton's options whenever the options + // get updated (so that other callbacks have accurate values). + add_action( 'update_option_ccb_core_settings', [ $this, 'refresh_options' ], 5, 2 ); + } + + /** + * Get any options stored by the user + * + * @return array + */ + public function get_options() { + return $this->plugin_options; + } + + /** + * Callback method to detect when the settings have changed. + * + * Ensures that this singleton's `get_options()` method always + * returns accurate settings based on the latest changes. + * + * @param array $old_value The old settings array. + * @param array $new_value The new settings array. + * @return void + */ + public function refresh_options( $old_value, $new_value ) { + $this->plugin_options = $new_value; + } + + /** + * Encrypts and base64_encodes a string safe for serialization in WordPress + * + * @since 1.0.0 + * @access public + * @param string $data The data to be encrypted. + * @return string + */ + public function encrypt( $data ) { + + $encrypted_value = false; + $key = wp_salt() . md5( 'ccb-core' ); + + if ( ! empty( $data ) ) { + try { + $e = new CCB_Core_Vendor_Encryption( MCRYPT_BlOWFISH, MCRYPT_MODE_CBC ); + $encrypted_value = base64_encode( $e->encrypt( $data, $key ) ); + } catch ( Exception $ex ) { + return new WP_Error( 'encrypt_failure', __( 'The string could not be encrypted', 'ccb-core' ) ); + } + + } + + return $encrypted_value; + } + + /** + * Decrypts and base64_decodes a string + * + * @since 1.0.0 + * @access public + * @param string $data The data to be decrypted. + * @return string + */ + public function decrypt( $data ) { + + $decrypted_value = false; + $key = wp_salt() . md5( 'ccb-core' ); + + if ( ! empty( $data ) ) { + try { + $e = new CCB_Core_Vendor_Encryption( MCRYPT_BlOWFISH, MCRYPT_MODE_CBC ); + $decrypted_value = $e->decrypt( base64_decode( $data ), $key ); + } catch ( Exception $ex ) { + return new WP_Error( 'decrypt_failure', __( 'The string could not be decrypted', 'ccb-core' ) ); + } + + } + + return $decrypted_value; + } + + /** + * Responds to the client with a json response + * but allows the script to continue + * + * @access public + * @since 1.0.0 + * @param array $data Optional data to send back. + * @return bool + */ + public function send_non_blocking_json_success( $data = [] ) { + + ignore_user_abort( true ); + ob_start(); + + header( 'Content-Type: application/json' ); + header( 'Content-Encoding: none' ); + + echo wp_json_encode( [ + 'success' => true, + 'data' => $data, + ] ); + + header( 'Connection: close' ); + header( 'Content-Length: ' . ob_get_length() ); + + ob_end_flush(); + ob_flush(); + flush(); + + // Some environments may be running PHP-FPM. + if ( function_exists( 'fastcgi_finish_request' ) ) { + fastcgi_finish_request(); + } + + return true; + + } + + /** + * Downloads an image from a URL, uploads it to the Media Library, + * and then optionally attaches it to a post. + * + * We are using this custom function instead of media_sideload_image + * because images with dynamic URLs (like those on S3) do not have + * file extensions and core ticket #18730 will never be resolved. + * https://core.trac.wordpress.org/ticket/18730 + * + * @param string $image_url The URL of the image. + * @param string $filename Optional. + * @param int $post_id Optional. + * @access public + * @return mixed Returns a media id or false on failure + */ + public function download_image( $image_url, $filename = '', $post_id = 0 ) { + + // Fetch the image and store temporarily. + $temp_file = download_url( $image_url ); + + if ( is_wp_error( $temp_file ) ) { + return false; + } + + // Attempt to detect the mimetype based on the available functions. + $extension = false; + if ( function_exists( 'exif_imagetype' ) && function_exists( 'image_type_to_extension' ) ) { + // Open with exif. + $image_type = exif_imagetype( $temp_file ); + if ( $image_type ) { + $extension = image_type_to_extension( $image_type ); + } + } elseif ( function_exists( 'getimagesize' ) && function_exists( 'image_type_to_extension' ) ) { + // Open with gd. + $file_size = getimagesize( $temp_file ); + if ( isset( $file_size[2] ) ) { + $extension = image_type_to_extension( $file_size[2] ); + } + } elseif ( function_exists( 'finfo_open' ) ) { + // Open with fileinfo. + $resource = finfo_open( FILEINFO_MIME_TYPE ); + $mimetype = finfo_file( $resource, $temp_file ); + finfo_close( $resource ); + if ( $mimetype ) { + $mimetype_array = explode( '/', $mimetype ); + $extension = '.' . $mimetype_array[1]; + } + } + + // If we were able to determine the extension, move it + // to the Media Library. + if ( $extension ) { + + $filename = ! empty( $filename ) ? sanitize_file_name( $filename ) : 'ccb_' . crc32( $image_url ); + + $file_array = [ + 'name' => $filename . $extension, + 'tmp_name' => $temp_file, + ]; + + add_filter( 'upload_dir', [ $this, 'custom_uploads_directory' ] ); + $media_id = media_handle_sideload( $file_array, $post_id ); + remove_filter( 'upload_dir', [ $this, 'custom_uploads_directory' ] ); + + if ( $post_id ) { + set_post_thumbnail( $post_id, $media_id ); + } + + // phpcs:ignore + @unlink( $temp_file ); + + if ( ! is_wp_error( $media_id ) ) { + return $media_id; + } + } + + return false; + } + + /** + * Override the default uploads directory location + * for CCB images. Allows for a convenient was to + * isolate the CCB uploads so they are not mixed in + * with other media assets. + * + * @param array $upload An array of upload paths. + * @return array + */ + public function custom_uploads_directory( $upload ) { + /** + * Allow for the ability to enable / disable custom upload path. + * + * @since 1.0.0 + * + * @param bool $allowed Whether this plugin is allowed to use custom upload paths. + */ + if ( apply_filters( 'ccb_core_allow_custom_uploads_directory', true ) ) { + $upload['path'] = trailingslashit( $upload['basedir'] ) . 'ccb'; + $upload['url'] = $upload['baseurl'] . '/ccb'; + $upload['subdir'] = '/ccb'; + } + return $upload; + } + +} diff --git a/includes/class-ccb-core-i18n.php b/includes/class-ccb-core-i18n.php deleted file mode 100644 index 3e45f32..0000000 --- a/includes/class-ccb-core-i18n.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -class CCB_Core_i18n extends CCB_Core_Plugin { - - /** - * Load the plugin text domain for translation. - * - * @since 0.9.0 - */ - public function load_plugin_textdomain() { - - load_plugin_textdomain( - $this->plugin_name, - false, - dirname( dirname( plugin_basename( __FILE__ ) ) ) . '/languages/' - ); - - } - -} diff --git a/includes/class-ccb-core-loader.php b/includes/class-ccb-core-loader.php deleted file mode 100644 index 09bea74..0000000 --- a/includes/class-ccb-core-loader.php +++ /dev/null @@ -1,128 +0,0 @@ - - */ -class CCB_Core_Loader extends CCB_Core_Plugin { - - /** - * The array of actions registered with WordPress. - * - * @since 0.9.0 - * @access protected - * @var array $actions The actions registered with WordPress to fire when the plugin loads. - */ - protected $actions; - - /** - * The array of filters registered with WordPress. - * - * @since 0.9.0 - * @access protected - * @var array $filters The filters registered with WordPress to fire when the plugin loads. - */ - protected $filters; - - /** - * Initialize the collections used to maintain the actions and filters. - * - * @since 0.9.0 - */ - public function __construct() { - - $this->actions = array(); - $this->filters = array(); - - } - - /** - * Add a new action to the collection to be registered with WordPress. - * - * @since 0.9.0 - * @var string $hook The name of the WordPress action that is being registered. - * @var object $component A reference to the instance of the object on which the action is defined. - * @var string $callback The name of the function definition on the $component. - * @var int Optional $priority The priority at which the function should be fired. - * @var int Optional $accepted_args The number of arguments that should be passed to the $callback. - */ - public function add_action( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) { - $this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args ); - } - - /** - * Add a new filter to the collection to be registered with WordPress. - * - * @since 0.9.0 - * @var string $hook The name of the WordPress filter that is being registered. - * @var object $component A reference to the instance of the object on which the filter is defined. - * @var string $callback The name of the function definition on the $component. - * @var int Optional $priority The priority at which the function should be fired. - * @var int Optional $accepted_args The number of arguments that should be passed to the $callback. - */ - public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) { - $this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args ); - } - - /** - * A utility function that is used to register the actions and hooks into a single - * collection. - * - * @since 0.9.0 - * @access private - * @var array $hooks The collection of hooks that is being registered (that is, actions or filters). - * @var string $hook The name of the WordPress filter that is being registered. - * @var object $component A reference to the instance of the object on which the filter is defined. - * @var string $callback The name of the function definition on the $component. - * @var int Optional $priority The priority at which the function should be fired. - * @var int Optional $accepted_args The number of arguments that should be passed to the $callback. - * @return type The collection of actions and filters registered with WordPress. - */ - private function add( $hooks, $hook, $component, $callback, $priority, $accepted_args ) { - - $hooks[] = array( - 'hook' => $hook, - 'component' => $component, - 'callback' => $callback, - 'priority' => $priority, - 'accepted_args' => $accepted_args - ); - - return $hooks; - - } - - /** - * Register the filters and actions with WordPress. - * - * @since 0.9.0 - */ - public function run() { - - foreach ( $this->filters as $hook ) { - add_filter( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] ); - } - - foreach ( $this->actions as $hook ) { - add_action( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] ); - } - - } - -} diff --git a/includes/class-ccb-core-plugin.php b/includes/class-ccb-core-plugin.php deleted file mode 100644 index cce6326..0000000 --- a/includes/class-ccb-core-plugin.php +++ /dev/null @@ -1,229 +0,0 @@ - - */ -class CCB_Core_Plugin { - - /** - * The unique identifier of this plugin. - * - * @since 0.9.0 - * @access protected - * @var string $plugin_name The string used to uniquely identify this plugin. - */ - protected $plugin_name; - - /** - * The settings variable name used to access the plugin settings - * - * @since 0.9.0 - * @access protected - * @var string $plugin_settings_name - */ - protected $plugin_settings_name; - - /** - * The display name of this plugin. - * - * @since 0.9.0 - * @access protected - * @var string $plugin_display_name The display name of this plugin. - */ - protected $plugin_display_name; - - /** - * The short display name of this plugin. - * - * @since 0.9.0 - * @access protected - * @var string $plugin_short_display_name The short display name of this plugin. - */ - protected $plugin_short_display_name; - - /** - * The current version of the plugin. - * - * @since 0.9.0 - * @access protected - * @var string $version The current version of the plugin. - */ - protected $version; - - /** - * Define the core properties of the plugin - * - * Set the plugin name and the plugin version that can be used throughout the plugin. - * - * @since 0.9.0 - */ - public function __construct() { - - $this->plugin_name = 'ccb-core'; - $this->plugin_settings_name = 'ccb_core_settings'; - $this->plugin_display_name = __( 'Church Community Builder Core API', $this->plugin_name ); - $this->plugin_short_display_name = __( 'CCB Core API', $this->plugin_name ); - $this->version = '0.9.6'; - add_theme_support( 'post-thumbnails' ); - - } - - /** - * Encrypts and base64_encodes a string safe for serialization in WordPress - * - * @since 0.9.0 - * @access protected - * @param string $data - * @return string - */ - protected function encrypt( $data ) { - - $encrypted_value = false; - $key = wp_salt() . md5( $this->plugin_name ); - - if ( ! empty( $data ) ) { - try { - $e = new CCB_Core_Vendor_Encryption( MCRYPT_BlOWFISH, MCRYPT_MODE_CBC ); - $encrypted_value = base64_encode( $e->encrypt( $data, $key ) ); - } - catch ( Exception $ex ) { - // TODO: Better exception handling - } - - } - - return $encrypted_value; - } - - /** - * Decrypts and base64_decodes a string - * - * @since 0.9.0 - * @access protected - * @param string $data - * @return string - */ - protected function decrypt( $data ) { - - $decrypted_value = false; - $key = wp_salt() . md5( $this->plugin_name ); - - if ( ! empty( $data ) ) { - try { - $e = new CCB_Core_Vendor_Encryption( MCRYPT_BlOWFISH, MCRYPT_MODE_CBC ); - $decrypted_value = $e->decrypt( base64_decode( $data ), $key ); - } - catch ( Exception $ex ) { - // TODO: Better exception handling - } - - } - - return $decrypted_value; - } - - /** - * Responds to the client with a json response - * but allows the script to continue - * - * @param array $response - * @access protected - * @since 0.9.0 - * @return bool - */ - protected function send_non_blocking_json_response( $response ) { - - ignore_user_abort(true); - ob_start(); - - header( 'Content-Type: application/json' ); - - echo json_encode( $response ); - - header( 'Connection: close' ); - header( 'Content-Length: ' . ob_get_length() ); - - ob_end_flush(); - ob_flush(); - flush(); - - return true; - - } - - /** - * Helper function to check if a date is valid - * - * @param string $date - * @param string $format - * @access protected - * @since 0.9.0 - * @return bool - */ - protected function valid_date( $date, $format = 'Y-m-d H:i:s' ) { - $version = explode('.', phpversion()); - if ( (int) $version[0] >= 5 && (int) $version[1] >= 2 && (int) $version[2] > 17 ) { - $d = DateTime::createFromFormat( $format, $date ); - } else { - $d = new DateTime( date( $format, strtotime( $date ) ) ); - } - return $d && $d->format( $format ) == $date; - } - - /** - * Gets the most recent synchronization results in the form - * of an array with a style class and message - * - * @access protected - * @since 0.9.0 - * @return array - */ - protected function get_latest_sync_results() { - - $latest_sync = get_option( $this->plugin_name . '-latest-sync' ); - - if ( is_array( $latest_sync ) && ! empty( $latest_sync ) ) { - - if ( $latest_sync['success'] == true ) { - - $latest_sync_message = array( - 'style' => 'updated', - 'description' => $latest_sync['message'], - ); - - } - else { - $latest_sync_message = array( - 'style' => 'error', - 'description' => $latest_sync['message'], - ); - } - } - else { - $latest_sync_message = array( - 'style' => 'notice', - 'description' => "It looks like you haven't synchronized anything yet." - ); - } - - return $latest_sync_message; - - } - -} - diff --git a/includes/class-ccb-core-settings-field.php b/includes/class-ccb-core-settings-field.php new file mode 100644 index 0000000..ecb3cd8 --- /dev/null +++ b/includes/class-ccb-core-settings-field.php @@ -0,0 +1,339 @@ + + */ +class CCB_Core_Settings_Field { + + /** + * The key for the field in the settings array + * + * @since 0.9.0 + * @access protected + * @var string $field_id + */ + protected $field_id; + + /** + * An array of field settings + * + * @since 0.9.0 + * @access protected + * @var array $field + */ + protected $field; + + /** + * The existing settings currently stored + * + * @since 0.9.0 + * @access protected + * @var array $existing_settings + */ + protected $existing_settings; + + /** + * Initialize the class and set its properties. + * + * @param string $field_id Slug of the field. + * @param array $field Array of field settings. + * @return void + */ + public function __construct( $field_id, $field ) { + + $this->field_id = $field_id; + $this->field = $field; + $this->existing_settings = CCB_Core_Helpers::instance()->get_options(); + + } + + /** + * General method that calls correct field render method based on config + * + * @access public + * @since 0.9.0 + * @return void + */ + public function render_field() { + if ( isset( $this->field['field_render_function'] ) && is_callable( [ $this, $this->field['field_render_function'] ] ) ) { + call_user_func( [ $this, $this->field['field_render_function'] ] ); + if ( isset( $this->field['field_tooltip'] ) ) { + echo ''; + } + } + } + + /** + * Render a textfield + * + * @access protected + * @since 0.9.0 + * @return void + */ + protected function render_text() { + $value = ''; + + if ( isset( $this->existing_settings[ $this->field_id ] ) ) { + $value = $this->existing_settings[ $this->field_id ]; + } + + echo sprintf( + 'field['field_placeholder'] ), + esc_attr( $this->field_id ), + esc_attr( $value ) + ); + + $this->output_attributes(); + + echo '/>'; + } + + /** + * Render a switch button (checkbox) + * + * @access protected + * @since 0.9.0 + * @return void + */ + protected function render_switch() { + $value = ''; + + if ( isset( $this->existing_settings[ $this->field_id ] ) ) { + $value = $this->existing_settings[ $this->field_id ]; + } + + echo sprintf( + 'field_id ), + checked( $value, '1', false ) + ); + + $this->output_attributes(); + + echo '/>'; + } + + /** + * Render a slider widget (textfield) + * + * @access protected + * @since 0.9.0 + * @return void + */ + protected function render_slider() { + $value = $this->field['field_default']; + + if ( isset( $this->existing_settings[ $this->field_id ] ) ) { + $value = $this->existing_settings[ $this->field_id ]; + } + + echo sprintf( + '
+ field_id ), + esc_attr( $value ), + esc_attr( $this->field['field_options']['min'] ), + esc_attr( $this->field['field_options']['max'] ) + ); + + $this->output_attributes(); + + echo ' />
'; + + echo sprintf( + '%2$s', + esc_attr( $this->field_id ), + esc_html( $this->field['field_options']['units'] ) + ); + } + + /** + * Render a jQuery date picker + * + * @access protected + * @since 0.9.0 + * @return void + */ + protected function render_date_picker() { + $value = ''; + + if ( isset( $this->existing_settings[ $this->field_id ] ) ) { + $value = $this->existing_settings[ $this->field_id ]; + } + + echo sprintf( + '
+ field_id ), + esc_attr( $value ) + ); + + $this->output_attributes(); + + echo '/>
'; + } + + /** + * Render radio buttons + * + * @access protected + * @since 0.9.0 + * @return void + */ + protected function render_radio() { + + $value = $this->field['field_default']; + + if ( isset( $this->existing_settings[ $this->field_id ] ) ) { + $value = $this->existing_settings[ $this->field_id ]; + } + + echo '
'; + foreach ( (array) $this->field['field_options'] as $option_value => $option_label ) { + echo sprintf( + '
', + esc_attr( $option_label ) + ); + } + echo '
'; + + } + + /** + * Render a username and password field + * + * @access protected + * @since 0.9.0 + * @return void + */ + protected function render_credentials() { + $value = []; + if ( isset( $this->existing_settings[ $this->field_id ] ) ) { + $value['username'] = $this->existing_settings[ $this->field_id ]['username']; + $value['password'] = CCB_Core_Helpers::instance()->decrypt( $this->existing_settings[ $this->field_id ]['password'] ); + } else { + $value['username'] = ''; + $value['password'] = ''; + } + + echo sprintf( + '', + esc_attr__( 'Username', 'ccb-core' ), + esc_attr( $this->field_id ), + esc_attr( $value['username'] ) + ); + echo sprintf( + '', + esc_attr__( 'Password', 'ccb-core' ), + esc_attr( $this->field_id ), + esc_attr( $value['password'] ) + ); + } + + /** + * Render a test credentials button + * + * @access protected + * @since 0.9.0 + * @return void + */ + protected function render_test_credentials() { + if ( empty( $this->existing_settings['credentials']['username'] ) || empty( $this->existing_settings['credentials']['password'] ) ) { + echo sprintf( + '

%s

', + esc_html__( 'Please enter your API Credentials' ) + ); + } elseif ( empty( $this->existing_settings['subdomain'] ) ) { + echo sprintf( + '

%s

', + esc_html__( 'Please enter your Church Community Builder subdomain.' ) + ); + } else { + echo sprintf( + '
+ +
', + esc_attr__( 'Test Credentials', 'ccb-core' ) + ); + } + } + + /** + * Render a manual sync button + * + * @access protected + * @since 0.9.0 + * @return void + */ + protected function render_manual_sync() { + if ( CCB_Core_API::instance()->initialized ) { + echo sprintf( + '
+ +
', + esc_attr__( 'Synchronize', 'ccb-core' ) + ); + } else { + echo '

' . esc_html__( 'Please enter your credentials under the API Settings page', 'ccb-core' ) . '

'; + } + } + + /** + * Render an area to display the latest sync results + * + * @access protected + * @since 0.9.0 + * @return void + */ + protected function render_latest_results() { + echo '
+ +
'; + } + + /** + * Helper method to echo HTML attributes from the config + * + * @access protected + * @since 0.9.0 + * @return void + */ + protected function output_attributes() { + $attributes = ''; + if ( ! empty( $this->field['field_attributes'] ) ) { + foreach ( $this->field['field_attributes'] as $attr_name => $attr_value ) { + echo sprintf( + '%1$s="%2$s" ', + esc_attr( $attr_name ), + esc_attr( $attr_value ) + ); + } + } + } +} diff --git a/admin/class-ccb-core-settings-page.php b/includes/class-ccb-core-settings-page.php similarity index 53% rename from admin/class-ccb-core-settings-page.php rename to includes/class-ccb-core-settings-page.php index 391625d..8117278 100644 --- a/admin/class-ccb-core-settings-page.php +++ b/includes/class-ccb-core-settings-page.php @@ -2,21 +2,21 @@ /** * Everything related to the plugin settings pages * - * @link http://jaredcobb.com/ccb-core + * @link https://www.wpccb.com * @since 0.9.0 * * @package CCB_Core - * @subpackage CCB_Core/admin + * @subpackage CCB_Core/includes */ /** * Object to manage the plugin settings pages * * @package CCB_Core - * @subpackage CCB_Core/admin + * @subpackage CCB_Core/includes * @author Jared Cobb */ -class CCB_Core_Settings_Page extends CCB_Core_Plugin { +class CCB_Core_Settings_Page { /** * The key for the page in the settings array @@ -30,16 +30,11 @@ class CCB_Core_Settings_Page extends CCB_Core_Plugin { /** * Initialize the class and set its properties. * - * @access public - * @since 0.9.0 - * @return void + * @param string $page_id The slug of the page. + * @return void */ public function __construct( $page_id ) { - - parent::__construct(); - $this->page_id = $page_id; - } /** @@ -50,24 +45,34 @@ public function __construct( $page_id ) { * @return void */ public function render_page() { - if ( ! current_user_can( 'manage_options' ) ) { - wp_die( __( 'You do not have sufficient permissions to access this page.', $this->plugin_name ) ); + + /** + * Defines the capability that is required for the user + * to access the settings page. + * + * @since 1.0.0 + * + * @param string $capability The capability required to access the page. + */ + if ( ! current_user_can( apply_filters( 'ccb_core_settings_capability', 'manage_options' ) ) ) { + wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'ccb-core' ) ); } ?> -
-

plugin_display_name; ?>

+
+

page_id ); ?> page_id ); ?> + page_id != 'ccb_core_settings' ) { + if ( 'ccb_core_settings' !== $this->page_id ) { ?>

- +

*/ -class CCB_Core_Settings_Section extends CCB_Core_Plugin { +class CCB_Core_Settings_Section { /** * The key for the section in the settings array @@ -39,17 +39,13 @@ class CCB_Core_Settings_Section extends CCB_Core_Plugin { /** * Initialize the class and set its properties. * - * @access public - * @since 0.9.0 - * @return void + * @param string $section_id The slug of the section. + * @param array $section An array of section settings. + * @return void */ public function __construct( $section_id, $section ) { - - parent::__construct(); - $this->section_id = $section_id; $this->section = $section; - } /** @@ -64,24 +60,15 @@ public function __construct( $section_id, $section ) { */ public function render_section_about() { - // if the user has set their subdomain, use it for the url to w_group_list.php - $settings = get_option( $this->plugin_settings_name ); - if ( isset( $settings['subdomain'] ) && ! empty( $settings['subdomain'] ) ) { - $w_group_list = "https://{$settings['subdomain']}.ccbchurch.com/w_group_list.php"; - } - else { - $w_group_list = 'https://[yoursite].ccbchurch.com/w_group_list.php'; - } - - // this unfortunately includes a dirty hack to prevent chrome from autopopulating username/password - // so this will inject a fake login panel because chrome ignores autocomplete="off" - echo <<

Church Community Builder Core API synchronizes your church data to WordPress custom post types. - This plugin is geared toward developers (or advanced WordPress users who aren't afraid to get into a little bit of code). + This plugin is geared toward developers (or advanced WordPress users who aren\'t afraid to get into a little bit of code).

Why Use This Plugin?

@@ -121,7 +108,7 @@ public function render_section_about() {

Frequently Asked Questions

- I installed this plugin and my site doesn't look any different + I installed this plugin and my site doesn\'t look any different

@@ -132,27 +119,41 @@ public function render_section_about() {

- Some of my groups in Church Community Builder aren't being synchronized + Some of my groups in Church Community Builder aren\'t being synchronized -

+

'; + echo '
- You'll need to ensure your group settings - allow the group to be publicly listed. A great way to cross reference if your group is publicly visible is to visit - {$w_group_list} and see if the missing group shows up there. + You\'ll need to ensure your group settings + have Public Search enabled (see the Options tab). A great way to cross reference if your group is publicly visible is to visit '; + + // If the user has set their subdomain, use it for the url to w_group_list.php. + $options = CCB_Core_Helpers::instance()->get_options(); + if ( ! empty( $options['subdomain'] ) ) { + echo sprintf( + ' + your public search page + ', + esc_url( "https://{$options['subdomain']}.ccbchurch.com/w_group_list.php" ) + ); + } else { + echo 'https://[yoursite].ccbchurch.com/w_group_list.php'; + } + + echo ' and see if the missing group shows up there.

Documentation

- The official documentation has more information, including code samples, hooks & filters, and links to tutorials. + The official documentation has more information, including code samples, hooks & filters, and links to tutorials.

Support

Support is limited, but if you have questions as a user of the plugin you can submit them on the official WordPress plugin support forum. - If you're a Developer and would like to submit a bug report or pull request, you can do that on GitHub. -

-HTML; + If you\'re a Developer and would like to submit a bug report or pull request, you can do that on GitHub. +

'; } /** @@ -163,8 +164,8 @@ public function render_section_about() { * @return void */ public function render_section() { - if ( $this->section_id == 'about' ) { - echo $this->render_section_about(); + if ( 'about' === $this->section_id ) { + $this->render_section_about(); } } diff --git a/includes/class-ccb-core-settings.php b/includes/class-ccb-core-settings.php new file mode 100644 index 0000000..01acbfb --- /dev/null +++ b/includes/class-ccb-core-settings.php @@ -0,0 +1,294 @@ + + */ +class CCB_Core_Settings { + + /** + * Validate the settings fields based on the settings config + * + * @access public + * @since 0.9.0 + * @param array $input An array of fields to sanitize. + * @return array $current_options + */ + public function validate_settings( $input ) { + + $current_options = CCB_Core_Helpers::instance()->get_options(); + if ( empty( $current_options ) ) { + $current_options = []; + } + + $validation_hash = $this->generate_validation_hash(); + + foreach ( $validation_hash as $field_id => $validation ) { + + if ( isset( $validation['field_validation'] ) ) { + + switch ( $validation['field_validation'] ) { + + case 'alphanumeric': + if ( empty( $input[ $field_id ] ) || ctype_alnum( $input[ $field_id ] ) ) { + $current_options[ $field_id ] = $input[ $field_id ]; + } else { + add_settings_error( + $field_id, + $field_id, + sprintf( + esc_html__( + 'Oops! %s can only contain letters and numbers', + 'ccb-core' + ), + esc_html( $validation['field_title'] ) + ) + ); + } + break; + + case 'numeric': + if ( empty( $input[ $field_id ] ) || ctype_digit( $input[ $field_id ] ) ) { + $current_options[ $field_id ] = $input[ $field_id ]; + } else { + add_settings_error( + $field_id, + $field_id, + sprintf( + esc_html__( + 'Oops! %s can only contain numbers', + 'ccb-core' + ), + esc_html( $validation['field_title'] ) + ) + ); + } + break; + + case 'slug': + $input[ $field_id ] = sanitize_key( $input[ $field_id ] ); + // Continue onto alphanumeric_extended validation because these are essentially the same. + case 'alphanumeric_extended': + if ( empty( $input[ $field_id ] ) || ! preg_match( '/[^\w\s-_]/', $input[ $field_id ] ) ) { + $current_options[ $field_id ] = $input[ $field_id ]; + } else { + add_settings_error( + $field_id, + $field_id, + sprintf( + esc_html__( + 'Oops! %s can only contain letters, numbers, spaces, dashes, or underscores.', + 'ccb-core' + ), + esc_html( $validation['field_title'] ) + ) + ); + } + break; + + case 'encrypt': + if ( ! empty( $input[ $field_id ]['password'] ) ) { + // 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'] ) ) { + // Password was already encrypted on the previous sanitization call. + $encrypted_password = $input[ $field_id ]['password']; + } else { + $encrypted_password = CCB_Core_Helpers::instance()->encrypt( $input[ $field_id ]['password'] ); + } + + if ( $encrypted_password ) { + $current_options[ $field_id ]['password'] = $encrypted_password; + } else { + add_settings_error( + $field_id, + $field_id, + 'Oops! We couldn\'t encrypt your password.' + ); + } + } + $current_options[ $field_id ]['username'] = $input[ $field_id ]['username']; + + break; + + case 'switch': + $current_options[ $field_id ] = ( isset( $input[ $field_id ] ) && '1' === $input[ $field_id ] ? '1' : '' ); + break; + + default: + $current_options[ $field_id ] = $input[ $field_id ]; + break; + + } + } + + } + + return $current_options; + } + + /** + * The whopper config used to create all the settings + * + * @access public + * @since 0.9.0 + * @return array + */ + public function get_settings_definitions() { + + // Initialize a settings array with an About page and Credentials page. + $settings = [ + 'ccb_core_settings' => [ + 'page_title' => esc_html__( 'About', 'ccb-core' ), + 'sections' => [ + 'about' => [ + 'section_title' => esc_html__( 'About', 'ccb-core' ), + // No fields needed for the about page. + ], + ], + ], + 'ccb_core_settings_api_settings' => [ + 'page_title' => esc_html__( 'API Settings', 'ccb-core' ), + 'sections' => [ + 'api_settings' => [ + 'section_title' => esc_html__( 'API Settings', 'ccb-core' ), + 'fields' => [ + 'subdomain' => [ + 'field_title' => esc_html__( 'Software Subdomain', 'ccb-core' ), + 'field_render_function' => 'render_text', + 'field_placeholder' => 'subdomain', + 'field_validation' => 'alphanumeric', + 'field_tooltip' => esc_html__( 'We just need the first part of your software URL (without "http://" and without ".ccbchurch.com").', 'ccb-core' ), + ], + 'credentials' => [ + 'field_title' => esc_html__( 'API Credentials', 'ccb-core' ), + 'field_render_function' => 'render_credentials', + 'field_validation' => 'encrypt', + 'field_tooltip' => esc_html__( 'This is the username and password for the API user in your Church Community Builder software.', 'ccb-core' ), + ], + 'test_credentials' => [ + 'field_title' => esc_html__( 'Test Credentials', 'ccb-core' ), + 'field_render_function' => 'render_test_credentials', + ], + ], + ], + ], + ], + ]; + + /** + * Allow custom post types to have their own settings pages. + * + * If you implement a custom post type and do not want to + * expose a settings page, simply `return $settings` from + * within your implementation of `get_post_settings_definitions()` + * + * @since 1.0.0 + * + * @param array $settings The current array of settings definitions. + */ + $settings = apply_filters( 'ccb_core_settings_post_definitions', $settings ); + + // Add a syncronization settings page. + $settings['ccb_core_settings_sync'] = [ + 'page_title' => esc_html__( 'Synchronize', 'ccb-core' ), + 'sections' => [ + 'synchronize' => [ + 'section_title' => esc_html__( 'Synchronize', 'ccb-core' ), + 'fields' => [ + 'auto_sync' => [ + 'field_title' => esc_html__( 'Enable Auto Sync', 'ccb-core' ), + 'field_render_function' => 'render_switch', + 'field_validation' => 'switch', + ], + 'auto_sync_timeout' => [ + 'field_title' => esc_html__( 'Cache Expiration', 'ccb-core' ), + 'field_render_function' => 'render_slider', + 'field_options' => [ + 'min' => '10', + 'max' => '180', + 'units' => 'minutes', + ], + 'field_default' => 90, + 'field_validation' => '', + 'field_attributes' => [ 'data-requires' => '{"auto_sync":1}' ], + 'field_tooltip' => sprintf( + esc_html__( + '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' ), + '
', + '
' + ), + ], + 'manual_sync' => [ + 'field_title' => esc_html__( 'Manual Sync', 'ccb-core' ), + 'field_render_function' => 'render_manual_sync', + ], + 'latest_results' => [ + 'field_title' => esc_html__( 'Latest Sync Results', 'ccb-core' ), + 'field_render_function' => 'render_latest_results', + ], + ], + ], + ], + ]; + + /** + * Allow filtering of the entire settings array. + * + * @since 1.0.0 + * + * @param array $settings The current array of settings definitions. + */ + return apply_filters( 'ccb_core_settings_definitions', $settings ); + + } + + /** + * Helper function to create a name/value hash for quick validation + * + * @access private + * @since 0.9.0 + * @return array $mapping + */ + private function generate_validation_hash() { + // Verify the nonce before processing field data. + check_admin_referer( 'update_settings', 'ccb_core_nonce' ); + + $mapping = []; + $page_id = isset( $_POST['option_page'] ) ? sanitize_text_field( wp_unslash( $_POST['option_page'] ) ) : false; // Input var okay. + $settings_definitions = $this->get_settings_definitions(); + + foreach ( $settings_definitions[ $page_id ]['sections'] as $section ) { + if ( ! empty( $section['fields'] ) ) { + foreach ( $section['fields'] as $field_id => $field ) { + if ( isset( $field['field_validation'] ) ) { + $mapping[ $field_id ] = [ + 'field_title' => $field['field_title'], + 'field_validation' => $field['field_validation'], + ]; + } else { + $mapping[ $field_id ] = false; + } + } + } + } + return $mapping; + } + +} diff --git a/includes/class-ccb-core-synchronizer.php b/includes/class-ccb-core-synchronizer.php new file mode 100644 index 0000000..8fdf5fd --- /dev/null +++ b/includes/class-ccb-core-synchronizer.php @@ -0,0 +1,907 @@ + + */ +class CCB_Core_Synchronizer { + + /** + * A complete mapping of CCB API data to post types and taxonomies + * + * @var array + */ + public $map; + + /** + * Instance of the class + * + * @var CCB_Core_Synchronizer + * @access private + * @static + */ + private static $instance; + + /** + * An instance of the CCB_Core_API class + * + * @var CCB_Core_API + */ + private $api; + + /** + * Unused constructor in the singleton pattern + * + * @access public + * @return void + */ + public function __construct() { + // Initialize this class with the instance() method. + } + + /** + * Returns the instance of the class + * + * @access public + * @static + * @return CCB_Core_Synchronizer + */ + public static function instance() { + if ( ! isset( static::$instance ) ) { + static::$instance = new CCB_Core_Synchronizer(); + static::$instance->setup(); + } + return static::$instance; + } + + /** + * Initial setup of the singleton + * + * @since 1.0.0 + */ + 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(); + } + + /** + * Setup the registered post type maps and taxonomy maps + * from the custom post type and taxonomy classes. + * + * @return void + */ + public function initialize_map() { + $post_type_maps = []; + $taxonomy_maps = []; + $this->map = array_merge_recursive( + /** + * Get a collection of all post type / API mappings. + * + * This is the main configuration for how the CCB API + * maps to a custom post type. + * + * @since 1.0.0 + * + * @param array $post_type_maps { + * A single map that defines the relationship between + * the CCB API entity node and the custom post type. + * + * $map[ {post_type} ] = [ + * 'service' => {ccb_service_name}, + * 'data' => [ {ccb_query_string_parameters} ], + * 'nodes' => [ {node list that maps to a single entity} ], + * 'fields' => [ + * {entity_name_node} => 'post_title', + * {entity_description_node} => 'post_content', + * {any_other_node} => 'post_meta', + * {any_other_node} => 'post_meta', + * {any_other_node} => 'post_meta', + * ], + * ]; + * } + */ + apply_filters( 'ccb_core_synchronizer_post_api_map', $post_type_maps ), + /** + * Get a collection of all taxonomy / API mappings. + * + * This is the main configuration for how the CCB API + * maps some of the nodes on an entity to custom taxonomies. + * + * @since 1.0.0 + * + * @param array $taxonomy_maps { + * A single map that defines the relationship between + * some of the nodes on a CCB entity to custom taxonomies. + * + * $map[ {post_type} ]['taxonomies']['hierarchical'][ {taxonomy} ] = [ + * 'api_mapping' => {node}, + * ]; + * + * $map[ {post_type} ]['taxonomies']['nonhierarchical'][ {taxonomy} ] = [ + * 'api_mapping' => [ {node} => {tag_name} ], + * ]; + * } + */ + apply_filters( 'ccb_core_synchronizer_taxonomy_api_map', $taxonomy_maps ) + ); + } + + /** + * Calls the CCB API and synchronizes post objects and + * taxonomies based on the mapping definitions from the + * custom post type and custom taxonomy classes. + * + * @return array + */ + public function synchronize() { + + $result = [ + 'success' => true, + ]; + + // Set a flag to globally signal that a sync is in progress. + set_transient( CCB_Core_Helpers::SYNC_STATUS_KEY, true, MINUTE_IN_SECONDS * 10 ); + + // For each registered custom post type, call the + // API and get a response object. + foreach ( $this->map as $post_type => $settings ) { + if ( ! empty( $settings['service'] ) ) { + + $data = ! empty( $settings['data'] ) ? $settings['data'] : []; + + /** + * Filters the API response for each service during the synchronization process. + * + * @since 1.0.0 + * + * @param array $response The API response for a specific service call. + * @param array $settings The settings used for the API call. + * @param string $post_type The current post type being synchronized. + */ + $response = apply_filters( + 'ccb_core_synchronizer_api_response', + $this->api->get( $settings['service'], $data ), + $settings, + $post_type + ); + + if ( 'SUCCESS' === $response['status'] ) { + + // A successful API request was made, update the WordPress content. + $update_result = $this->update_content( $response, $settings, $post_type ); + + if ( false === $update_result['success'] ) { + $result['success'] = false; + $result['message'] = esc_html__( 'At least one API synchronization failed', 'ccb-core' ); + } + $result['services'][ $settings['service'] ] = $update_result; + + } else { + + $result['success'] = false; + $result['message'] = esc_html( + sprintf( + // Translators: Error message and error code. + __( 'There was an API error: %1$s. Error code: %2$s', 'ccb-core' ), + $response['error'], + $response['code'] + ) + ); + break; + + } + + } + } + + $result['timestamp'] = time(); + + update_option( 'ccb_core_latest_sync_result', $result ); + // Delete the sync in progress flag. + delete_transient( CCB_Core_Helpers::SYNC_STATUS_KEY ); + + return $result; + } + + /** + * Takes an API response and will either Insert, Update, or Delete + * content based on the settings and any applicable existing content. + * + * @param array $response An API response. + * @param array $settings The settings for the mapping. + * @param string $post_type The post type being updated. + * @return array + */ + public function update_content( $response, $settings, $post_type ) { + + $result = [ + 'success' => true, + 'insert_update' => [ + 'success' => true, + 'processed' => 0, + ], + 'delete' => [ + 'success' => true, + 'processed' => 0, + ], + ]; + + // The nodes are mapped down from the parent(s) to the + // single child object. Get a collection of entities + // that will map to a single post type. + $entities = $this->get_entities( $response, $settings['nodes'] ); + + // Get a collection of existing posts (previously imported) from WordPress. + // This is organized by a key of an entitiy id (from CCB) and contains + // the WordPress post_id and optional ccb_modified_date. + $post_data = $this->get_existing_post_data( $post_type ); + + // Organize the entities and existing post data into their + // respective CRUD operations. This will return an array + // with entities to insert for the first time, entities + // to update (that already exist and have changed), and + // posts that no longer exist in CCB and should be deleted. + $organized_entities = $this->organize_entities( $entities, $post_data, $post_type ); + + $insert_update_result = false; + if ( ! empty( $organized_entities['insert_update'] ) ) { + $insert_update_result = $this->insert_update_entities( $organized_entities['insert_update'], $settings, $post_type ); + } + $delete_result = false; + if ( ! empty( $organized_entities['delete'] ) ) { + $delete_result = $this->delete_posts( $organized_entities['delete'] ); + } + + /** + * Whether or not we should clean up (delete) empty terms + * after a synchronization. Recommended to be true. + * + * @since 1.0.0 + * + * @param bool $delete_terms Whether or not to delete empty terms. + * @param array $settings The settings for the current sync. + * @param string $post_type The current post type. + */ + if ( apply_filters( 'ccb_core_synchronizer_delete_empty_terms', true, $settings, $post_type ) ) { + $this->delete_empty_terms( $settings ); + } + + // Setup the result array. + if ( ! empty( $insert_update_result ) ) { + $result['insert_update'] = $insert_update_result; + if ( false === $insert_update_result['success'] ) { + // Set the overall result as a failure also, then + // immediately return it. + $result['success'] = false; + return $result; + } + } + + if ( ! empty( $delete_result ) ) { + $result['delete'] = $delete_result; + if ( false === $delete_result['success'] ) { + // Set the overall result as a failure also, then + // immediately return it. + $result['success'] = false; + return $result; + } + } + + return $result; + + } + + /** + * Returns a collection of the CCB entities that will be processed. + * + * @param array $response The standardized response array. + * @param array $nodes A path to the single entity. + * + * @return SimpleXML A collection of entities. + */ + public function get_entities( $response, $nodes ) { + if ( ! empty( $nodes ) ) { + $depth = count( $nodes ) - 1; + $collection = $response['body']->response; + for ( $i = 0; $i < $depth; $i++ ) { + $collection = $collection->{$nodes[ $i ]}; + } + return $collection; + } + return false; + } + + /** + * Returns a collection of post data representing + * the existing (already imported) CCB entities. + * + * Return structure: + * + * array[ $entity_id ][ + * 'post_id' => $post_id, + * 'ccb_modified_date' => $ccb_modified_date, + * ] + * + * @param string $post_type The post type mapped to the CCB entitiy. + * @return array + */ + public function get_existing_post_data( $post_type ) { + // Batch the WP_Query for performance. + $posts_per_page = 100; + $offset = 0; + $collection = []; + + do { + $args = [ + 'post_type' => $post_type, + 'post_status' => 'any', + 'posts_per_page' => $posts_per_page, + 'offset' => $offset, + 'no_rows_found' => true, + 'fields' => 'ids', + ]; + + $posts = new WP_Query( $args ); + $have_posts = ! empty( $posts->posts ); + + if ( $have_posts ) { + foreach ( $posts->posts as $post_id ) { + // These are saved during the insert / update process (if possible) + // in order to attempt future updates. + $entity_id = get_post_meta( $post_id, 'entity_id', true ); + $ccb_modified_date = get_post_meta( $post_id, 'ccb_modified_date', true ); + + if ( ! empty( $entity_id ) ) { + $collection[ $entity_id ] = [ + 'post_id' => $post_id, + 'ccb_modified_date' => $ccb_modified_date, + ]; + } + } + } + + $offset = $offset + $posts_per_page; + + } while ( $have_posts ); + + return $collection; + } + + /** + * Returns an array where entities are seperated into + * different database operations. Currently organized into + * `insert_update` and `delete` collections. + * + * Return structure: + * + * array[ + * 'insert_update' => array[$entities], + * 'delete' => array[$entities], + * ] + * + * @param SimpleXML $entities A parent collection of entities. + * @param array $post_data Existing posts (may be empty). + * @param string $post_type The post type to map to the entities. + * @return array + */ + public function organize_entities( $entities, $post_data, $post_type ) { + + $collection = [ + 'insert_update' => [], + 'delete' => [], + ]; + + // Create a master collection of new entity ids + // that were either inserted or updated + // for quick filtering of existing posts + // so that we can find posts to delete. + $synced_entity_ids = []; + + foreach ( $entities->children() as $entity ) { + + $entity_id = $this->get_entity_id( $entity ); + + // If the entity_id is a 32 character hash it means we cannot + // map it to some known older post data because it either + // doesn't have a CCB ID or else it doesn't give us a modified + // date. So we hash its string value in order to get a unique + // signature. We use this to determine if we "already have this + // thing" so we don't need to insert / update it. + if ( 32 === strlen( $entity_id ) ) { + + /** + * Whether or not this specific entity is allowed to insert. + * + * Helpful if you need to inspect an entity for custom business + * rules before allowing an insert to happen. For example, `group_profiles` + * will send us inactive groups. We do not want to insert inactive groups. + * + * @since 1.0.0 + * + * @param bool $allowed Whether or not the entity is allowed. + * @param SimpleXML $entity The entity to insert. + * @param mixed $entity_id The unique identifier from CCB. + * @param string $post_type The current post type. + */ + if ( apply_filters( 'ccb_core_synchronizer_entity_insert_allowed', true, $entity, $entity_id, $post_type ) ) { + // If the unique id for the new entity couldn't be found + // in the existing collection, it is new. Add it + // to the insert collection. + if ( ! array_key_exists( $entity_id, $post_data ) ) { + $entity->addChild( 'post_id', 0 ); // This is a WordPress post ID, so this will be an insert. + $collection['insert_update'][] = $entity; + } + // Regardless of whether or not this unique entity already exists + // (i.e. regardless of an insert or update) we still record + // that it was synced so that it doesn't get deleted. + $synced_entity_ids[] = $entity_id; + } + } else { + if ( array_key_exists( $entity_id, $post_data ) && ! empty( $entity->modified ) ) { + /** + * Whether or not this specific entity is allowed to update. + * + * Helpful if you need to inspect an entity for custom business + * rules before allowing an update to happen. + * + * @since 1.0.0 + * + * @param bool $allowed Whether or not the entity is allowed. + * @param SimpleXML $entity The entity to insert. + * @param mixed $entity_id The unique identifier from CCB. + * @param string $post_type The current post type. + */ + if ( apply_filters( 'ccb_core_synchronizer_entity_update_allowed', true, $entity, $entity_id, $post_type ) ) { + // If an existing post has a newer modified date from the API + // add it to the insert_update collection with its existing post id. + if ( strtotime( $entity->modified ) > strtotime( $post_data[ $entity_id ]['ccb_modified_date'] ) ) { + $entity->addChild( 'post_id', $post_data[ $entity_id ]['post_id'] ); // This is a WordPress post ID, so this will be an update. + $collection['insert_update'][] = $entity; + } + // Even though we may not have made an update (i.e. it currently + // exists but hasn't changed), we still add this as a "synced" id + // so that it doesn't get deleted. + $synced_entity_ids[] = $entity_id; + } + } else { + // The unique id for the new entity couldn't be found + // in the existing collection, so it is new. Add it + // to the insert_update collection. + if ( apply_filters( 'ccb_core_synchronizer_entity_insert_allowed', true, $entity, $entity_id, $post_type ) ) { + $entity->addChild( 'post_id', 0 ); // This is a WordPress post ID, so this will be an insert. + $collection['insert_update'][] = $entity; + $synced_entity_ids[] = $entity_id; + } + } + } + + } + + // For each existing post, check to see if it was included + // in this entity id collection from this most recent + // API response. If it doesn't exist, it was deleted in CCB. + foreach ( $post_data as $entity_id => $data ) { + if ( ! in_array( $entity_id, $synced_entity_ids, true ) ) { + /** + * Whether or not this specific entity is allowed to delete. + * + * Helpful if you need to inspect an entity for custom business + * rules before allowing a delete to happen. + * + * @since 1.0.0 + * + * @param bool $allowed Whether or not the entity is allowed. + * @param array $data The WordPress post data. + * @param mixed $entity_id The unique identifier from CCB. + * @param string $post_type The current post type. + */ + if ( apply_filters( 'ccb_core_synchronizer_entity_delete_allowed', true, $data, $entity_id, $post_type ) ) { + $collection['delete'][] = $data['post_id']; + } + } + } + + return $collection; + } + + /** + * Returns a unique identifier for the CCB entitiy. + * + * If CCB included their own id (for example a group id) we + * use that. Otherwise we hash the string value of the entity. + * + * @param SimpleXML $entity A single entity object. + * @return mixed An integer id or string hash. + */ + public function get_entity_id( $entity ) { + $entity_id = ''; + // As part of the insert / update process we sometimes append + // a post_id to the entitiy. Ensure we remove it before hashing + // the string value so that it represents what we received from CCB. + if ( isset( $entity->post_id ) ) { + unset( $entity->post_id ); + } + + // If an entity doesn't have a CCB ID, we cannot match it during + // the sync process in order to perform an update. So instead, + // create a hash of the entity and store it as a unique identifier. + // + // If an entity has an actual CCB ID, but *doesn't* have a modified + // date, there's no point in storing the CCB ID. This is because + // without a modified date we cannot determine if the entity has + // changed since the previous sync. Instead, just create a hash of the entity. + if ( ! empty( $entity->attributes() ) && ! empty( $entity->modified ) ) { + foreach ( $entity->attributes() as $key => $value ) { + if ( false !== stristr( $key, 'id' ) ) { + $entity_id = (int) $value; + break; + } + } + } + + if ( ! $entity_id ) { + $entity_id = md5( $entity->asXML() ); + } + + /** + * Filter the unique entity_id that we attempt to + * auto detect from the entity object + * + * @since 1.0.0 + * + * @param mixed $entity_id Either an integer id or hash. + * @param SimpleXML $entity The current entity object. + */ + return apply_filters( 'ccb_core_synchronizer_get_entity_id', $entity_id, $entity ); + } + + /** + * Inserts / Updates entities into WordPress. + * + * @param array $entities A collection of SimpleXML entity objects. + * @param array $settings The settings for the import. + * @param string $post_type The current post type. + * @return array + */ + public function insert_update_entities( $entities, $settings, $post_type ) { + + // Allow this script to run longer. + set_time_limit( MINUTE_IN_SECONDS * 10 ); + $this->enable_optimizations(); + + $result = [ + 'success' => true, + 'processed' => 0, + 'message' => '', + ]; + + foreach ( $entities as $entity ) { + // Build a new $args array for each post insert + // based on the settings config. + $args = [ + 'ID' => (int) $entity->post_id, // Will be set to 0 if this is an insert. + 'post_title' => '', // Default to empty, expected to be overriden by the settings. + 'post_content' => '', // Default to empty, expected to be overriden by the settings. + 'post_status' => 'publish', + 'post_type' => $post_type, + 'meta_input' => [], + 'tax_input' => [], + ]; + + // Inspect each field defined in the settings. If it's a + // post_meta node, add it to the meta_input array, otherwise + // assume it's part of the parent post object. + foreach ( $settings['fields'] as $node => $field ) { + if ( 'post_meta' === $field ) { + $args['meta_input'][ $node ] = $this->auto_cast( $entity->{$node} ); + } else { + $args[ $field ] = $this->auto_cast( $entity->{$node} ); + } + } + + // Attempt to store additional meta related to synchronization. + // If CCB provided a CCB ID for this entitiy, save it. If CCB + // provided a `modified` node, save it. We use them to attempt + // updates to the entity when needed. + $args['meta_input']['entity_id'] = $this->get_entity_id( $entity ); + if ( isset( $entity->modified ) ) { + $args['meta_input']['ccb_modified_date'] = $this->auto_cast( $entity->modified ); + } + + // Prepare hierarchial taxonomies by ensuring the term id + // already exists and set terms ids. + $prepared_terms = $this->prepare_terms( $entity, $settings ); + if ( ! empty( $prepared_terms ) ) { + $args['tax_input'] = $prepared_terms; + } + + // If this is an update, we need to remove all term + // relationships in order to ensure we are in + // sync with the most recent entity. Otherwise if a + // relationship was removed by CCB, it'll be orphaned in WordPress. + if ( $args['ID'] ) { + if ( ! empty( $settings['taxonomies']['hierarchical'] ) ) { + foreach ( $settings['taxonomies']['hierarchical'] as $taxonomy => $node ) { + wp_delete_object_term_relationships( $args['ID'], $taxonomy ); + } + } + if ( ! empty( $settings['taxonomies']['nonhierarchical'] ) ) { + foreach ( $settings['taxonomies']['nonhierarchical'] as $taxonomy => $node ) { + wp_delete_object_term_relationships( $args['ID'], $taxonomy ); + } + } + } + + /** + * Filters the `wp_insert_post` $args for each entity. + * + * @since 1.0.0 + * + * @param array $args The `wp_insert_post` args. + * @param SimpleXML $entity The entity object. + * @param array $settings The current settings for the sync. + * @param string $post_type The current post type. + */ + $args = apply_filters( 'ccb_core_synchronizer_insert_post_args', $args, $entity, $settings, $post_type ); + + /** + * Before the insert / update is processed. + * + * @since 1.0.0 + * + * @param SimpleXML $entity The entity object to be inserted / updated. + * @param array $settings The current settings for the import. + * @param array $args The args that will be used for wp_insert_post. + * @param string $post_type The current post type. + */ + do_action( 'ccb_core_before_insert_update_post', $entity, $settings, $args, $post_type ); + + // Perform an insert (or update if we included a post id). + $post_id = wp_insert_post( $args, true ); + + if ( is_wp_error( $post_id ) ) { + $result['success'] = false; + $result['message'] = esc_html( + sprintf( + __( 'Inserting / Updating a post failed for the %1$s post type. Error: %2$s', 'ccb-core' ), + $post_type, + $post_id->get_error_message() + ) + ); + $this->disable_optimizations(); + return $result; + } + + /** + * After the insert / update is processed. + * + * Useful if you need to run custom business logic after a post is + * inserted / updated. For example, this is how we import and attach + * featured images for Groups. + * + * @since 1.0.0 + * + * @param SimpleXML $entity The entity object to be inserted / updated. + * @param array $settings The current settings for the import. + * @param array $args The args that will be used for wp_insert_post. + * @param string $post_type The current post type. + * @param int $post_id The new post id (or existing post id if update). + */ + do_action( 'ccb_core_after_insert_update_post', $entity, $settings, $args, $post_type, $post_id ); + + $result['processed'] += 1; + + } + + $this->disable_optimizations(); + + return $result; + } + + /** + * Deletes any posts that no longer exist in CCB. + * + * @param array $post_ids A collection of post ids to delete. + * @return array + */ + public function delete_posts( $post_ids ) { + $result = [ + 'success' => true, + 'processed' => 0, + 'message' => '', + ]; + + foreach ( $post_ids as $post_id ) { + $deleted = wp_delete_post( $post_id, true ); + if ( ! $deleted ) { + $result['success'] = false; + $result['message'] = esc_html__( 'There was an error while attempting to delete orhpaned posts', 'ccb-core' ); + return $result; + } + $result['processed'] += 1; + } + return $result; + } + + /** + * Cleans up any orphaned terms after a sync. + * + * @param array $settings Definitions of taxonomies. + * @return void + */ + public function delete_empty_terms( $settings ) { + // Ensure we get empty terms. + $args = [ + 'hide_empty' => false, + ]; + + // Build a collection of all taxonomies configured. + if ( ! empty( $settings['taxonomies']['hierarchical'] ) ) { + foreach ( $settings['taxonomies']['hierarchical'] as $taxonomy => $node ) { + $args['taxonomy'][] = $taxonomy; + } + } + if ( ! empty( $settings['taxonomies']['nonhierarchical'] ) ) { + foreach ( $settings['taxonomies']['nonhierarchical'] as $taxonomy => $node ) { + $args['taxonomy'][] = $taxonomy; + } + } + + // Delete any terms with a 0 count. + $terms_query = new WP_Term_Query( $args ); + foreach ( $terms_query->get_terms() as $term ) { + if ( 0 === $term->count ) { + wp_delete_term( $term->term_id, $term->taxonomy ); + } + } + } + + /** + * Returns an array of hierarchical and nonhierarchical terms + * that are ready for use by wp_insert_post. + * + * @param SimpleXML $entity An entity object. + * @param array $settings The taxonomy settings. + * @return array + */ + public function prepare_terms( $entity, $settings ) { + $categories = []; + $tags = []; + + if ( ! empty( $settings['taxonomies']['hierarchical'] ) ) { + foreach ( $settings['taxonomies']['hierarchical'] as $taxonomy => $node ) { + $term_name = $this->auto_cast( $entity->{$node} ); + if ( $term_name ) { + // phpcs:ignore + $term = term_exists( $term_name, $taxonomy ); + if ( ! $term ) { + $term = wp_insert_term( $term_name, $taxonomy ); + } + if ( $term && ! is_wp_error( $term ) ) { + $categories[ $taxonomy ][] = $term['term_taxonomy_id']; + } + } + } + } + + if ( ! empty( $settings['taxonomies']['nonhierarchical'] ) ) { + foreach ( $settings['taxonomies']['nonhierarchical'] as $taxonomy => $node_array ) { + foreach ( $node_array as $node => $tag ) { + $tag_is_set = $this->auto_cast( $entity->{$node} ); + if ( $tag_is_set ) { + $tags[ $taxonomy ][] = $tag; + } + } + } + } + + return array_merge( $categories, $tags ); + } + + /** + * Attempt to cast a SimpleXML node to a strong type. + * Will recursively process arrays. + * + * @param SimpleXML $node A single node on the entitiy. + * @return mixed + */ + public function auto_cast( $node ) { + // If the node has children, convert it to an array + // and recursively process the child nodes. + if ( $node->children()->count() ) { + $array = []; + // If the node happens to have an id attribute, + // transform it into a property so that it's not lost. + $id = false; + if ( ! empty( $node->attributes() ) ) { + foreach ( $node->attributes() as $key => $value ) { + if ( false !== stristr( $key, 'id' ) ) { + $id = (int) $value; + break; + } + } + } + if ( $id ) { + $array['id'] = $id; + } + foreach ( $node->children() as $child ) { + $array[ $child->getName() ] = $this->auto_cast( $child ); + } + return $array; + } elseif ( 'true' === (string) $node ) { + return true; + } elseif ( 'false' === (string) $node ) { + return false; + } elseif ( strlen( (int) $node ) === strlen( (string) $node ) ) { + return (int) $node; + } else { + return (string) $node; + } + } + + /** + * Configure some common optimizations for bulk + * insert / update processing. + * + * @return void + */ + private function enable_optimizations() { + // Remove expensive unneeded actions. + remove_action( 'do_pings', 'do_all_pings', 10, 1 ); + + // Temporarily disable counting for performance. + wp_defer_term_counting( true ); + wp_defer_comment_counting( true ); + wp_suspend_cache_addition( true ); + + // Unit tests rollback database transactions, do not + // alter commit settings for unit tests. + if ( ! defined( 'IS_UNIT_TEST' ) ) { + global $wpdb; + // Temporarily disable autocommit. + $wpdb->query( 'SET autocommit = 0;' ); // db call ok; no cache ok. + } + } + + /** + * Remove the optimizations after processing is complete. + * + * @return void + */ + private function disable_optimizations() { + + // Unit tests rollback database transactions, do not + // alter commit settings for unit tests. + if ( ! defined( 'IS_UNIT_TEST' ) ) { + global $wpdb; + // Commit the database operations now. + $wpdb->query( 'COMMIT;' ); // db call ok; no cache ok. + // Re-enable autocommit. + $wpdb->query( 'SET autocommit = 1;' ); // db call ok; no cache ok. + } + + // Re-enable counting. + wp_suspend_cache_addition( false ); + wp_defer_term_counting( false ); + wp_defer_comment_counting( false ); + + // Re-attach expensive actions. + add_action( 'do_pings', 'do_all_pings', 10, 1 ); + } + +} diff --git a/includes/class-ccb-core.php b/includes/class-ccb-core.php index 5eb59a6..26adb4b 100644 --- a/includes/class-ccb-core.php +++ b/includes/class-ccb-core.php @@ -5,11 +5,9 @@ * A class definition that includes attributes and functions used across both the * public-facing side of the site and the dashboard. * - * @link http://jaredcobb.com/ccb-core - * @since 0.9.0 - * - * @package CCB_Core - * @subpackage CCB_Core/includes + * @link https://www.wpccb.com + * @package CCB_Core + * @subpackage CCB_Core/includes */ /** @@ -21,154 +19,397 @@ * Also maintains the unique identifier of this plugin as well as the current * version of the plugin. * - * @since 0.9.0 * @package CCB_Core * @subpackage CCB_Core/includes * @author Jared Cobb */ -class CCB_Core extends CCB_Core_Plugin { +class CCB_Core { /** - * The loader that's responsible for maintaining and registering all hooks that power - * the plugin. + * Define the core functionality of the plugin. + * + * Set the plugin name and the plugin version that can be used throughout the plugin. + * Load the dependencies, define the locale, and set the hooks for the Dashboard and + * the public-facing side of the site. * * @since 0.9.0 - * @access protected - * @var CCB_Core_Loader $loader Maintains and registers all hooks for the plugin. */ - protected $loader; + public function __construct() { + $this->load_dependencies(); + $this->define_hooks(); + } /** - * A helper for getting the plugin_basename + * Load the required dependencies for this plugin. + * + * @since 0.9.0 + * @access private + */ + 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'; + + // A generic helper class with commonly used mehtods. + require_once CCB_CORE_PATH . 'includes/class-ccb-core-helpers.php'; + + // The classes that define options and settings for the plugin. + require_once CCB_CORE_PATH . 'includes/class-ccb-core-settings.php'; + require_once CCB_CORE_PATH . 'includes/class-ccb-core-settings-page.php'; + require_once CCB_CORE_PATH . 'includes/class-ccb-core-settings-section.php'; + require_once CCB_CORE_PATH . 'includes/class-ccb-core-settings-field.php'; + + // The class that handles communication with the CCB API. + require_once CCB_CORE_PATH . 'includes/class-ccb-core-api.php'; + + // The class that handles synchronization logic. + require_once CCB_CORE_PATH . 'includes/class-ccb-core-synchronizer.php'; + + // Custom Post Type classes. + require_once CCB_CORE_PATH . 'includes/post-types/class-ccb-core-cpt.php'; + require_once CCB_CORE_PATH . 'includes/post-types/class-ccb-core-group.php'; + require_once CCB_CORE_PATH . 'includes/post-types/class-ccb-core-calendar.php'; + + // Custom Taxonomy classes. + require_once CCB_CORE_PATH . 'includes/taxonomies/class-ccb-core-taxonomy.php'; + require_once CCB_CORE_PATH . 'includes/taxonomies/class-ccb-core-calendar-event-type.php'; + require_once CCB_CORE_PATH . 'includes/taxonomies/class-ccb-core-calendar-group-name.php'; + require_once CCB_CORE_PATH . 'includes/taxonomies/class-ccb-core-calendar-grouping-name.php'; + require_once CCB_CORE_PATH . 'includes/taxonomies/class-ccb-core-group-area.php'; + require_once CCB_CORE_PATH . 'includes/taxonomies/class-ccb-core-group-day.php'; + require_once CCB_CORE_PATH . 'includes/taxonomies/class-ccb-core-group-department.php'; + require_once CCB_CORE_PATH . 'includes/taxonomies/class-ccb-core-group-tag.php'; + require_once CCB_CORE_PATH . 'includes/taxonomies/class-ccb-core-group-time.php'; + require_once CCB_CORE_PATH . 'includes/taxonomies/class-ccb-core-group-type.php'; + + // Admin AJAX methods. + require_once CCB_CORE_PATH . 'includes/class-ccb-core-admin-ajax.php'; + + // Cron Management. + require_once CCB_CORE_PATH . 'includes/class-ccb-core-cron.php'; + + } + + /** + * Register all of the hooks related to the dashboard functionality + * of the plugin. * * @since 0.9.0 - * @access protected - * @var CCB_Core_Loader $loader Maintains and registers all hooks for the plugin. + * @access private */ - protected $plugin_basename; + private function define_hooks() { + + // Internationalization. + add_action( 'plugins_loaded', [ $this, 'load_plugin_textdomain' ] ); + + // Check the plugin / database version and run any required upgrades. + add_action( 'plugins_loaded', [ $this, 'check_version' ] ); + + // Plugin settings, menus, options. + add_filter( 'plugin_action_links_' . CCB_CORE_BASENAME, [ $this, 'add_settings_link' ] ); + + // Setup settings pages. + add_action( 'admin_menu', [ $this, 'initialize_settings_menu' ] ); + add_action( 'admin_init', [ $this, 'initialize_settings' ] ); + + // Callback for after the options are saved. + add_action( 'update_option_ccb_core_settings', [ $this, 'updated_options' ], 10, 2 ); + + // Determine if the rewrite rules need to be flushed. + add_action( 'init', [ $this, 'check_rewrite_rules' ] ); + + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_styles' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); + + } /** - * Define the core functionality of the plugin. + * Load the plugin text domain for translation. * - * Set the plugin name and the plugin version that can be used throughout the plugin. - * Load the dependencies, define the locale, and set the hooks for the Dashboard and - * the public-facing side of the site. + * @since 0.9.0 + */ + public function load_plugin_textdomain() { + + load_plugin_textdomain( + 'ccb-core', + false, + dirname( CCB_CORE_BASENAME ) . '/languages/' + ); + + } + + /** + * Check the current plugin version and kick off any applicable upgrades. * - * @since 0.9.0 + * @return void */ - public function __construct( $plugin_basename ) { + public function check_version() { + $current_version = get_option( 'ccb_core_version' ); - parent::__construct(); - $this->plugin_basename = $plugin_basename; - $this->load_dependencies(); - $this->set_locale(); - $this->define_admin_hooks(); + // We are currently up to date. + if ( version_compare( $current_version, CCB_CORE_VERSION, '>=' ) ) { + return; + } + // Upgrade to version 1.0.0. + if ( version_compare( $current_version, '1.0.0', '<' ) ) { + $this->upgrade_to_1_0_0(); + } + + // Update the DB version. + update_option( 'ccb_core_version', CCB_CORE_VERSION ); } /** - * Load the required dependencies for this plugin. + * Create a helpful settings link on the plugin page * - * Also create an instance of the loader which will be used to register the hooks - * with WordPress. + * @param array $links An array of links. + * @access public + * @since 0.9.0 + * @return array + */ + public function add_settings_link( $links ) { + $links[] = '' . esc_html__( 'Settings', 'ccb-core' ) . ''; + return $links; + } + + /** + * Initialize the Settings Menu and Page * + * @access public * @since 0.9.0 - * @access private + * @return void */ - private function load_dependencies() { + public function initialize_settings_menu() { + + $settings = new CCB_Core_Settings(); + $settings_page = new CCB_Core_Settings_Page( 'ccb_core_settings' ); + + add_menu_page( + __( 'Church Community Builder Core API', 'ccb-core' ), + __( 'CCB Core API', 'ccb-core' ), + /** + * Defines the capability that is required for the user + * to access the settings page. + * + * @since 1.0.0 + * + * @param string $capability The capability required to access the page. + */ + apply_filters( 'ccb_core_settings_capability', 'manage_options' ), + 'ccb_core_settings', + '__return_null', + 'dashicons-update', + '80.9' + ); + + foreach ( $settings->get_settings_definitions() as $page_id => $page ) { + $settings_page = new CCB_Core_Settings_Page( $page_id ); + add_submenu_page( + 'ccb_core_settings', + $page['page_title'], + $page['page_title'], + apply_filters( 'ccb_core_settings_capability', 'manage_options' ), + $page_id, + [ + $settings_page, + 'render_page', + ] + ); + } + } + + /** + * Initialize the Settings + * + * @access public + * @since 0.9.0 + * @return void + */ + public function initialize_settings() { + + $settings = new CCB_Core_Settings(); - // encryption class to provide better security and ease of use - require_once plugin_dir_path( dirname( __FILE__ ) ) . 'lib/Encryption/Encryption.php'; + foreach ( $settings->get_settings_definitions() as $page_id => $page ) { - // the class responsible for orchestrating the actions and filters of the core plugin. - require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-ccb-core-loader.php'; + register_setting( $page_id, 'ccb_core_settings', [ $settings, 'validate_settings' ] ); - // the class responsible for defining internationalization functionality of the plugin. - require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-ccb-core-i18n.php'; + foreach ( $page['sections'] as $section_id => $section ) { - // the class that defines options and settings for the plugin - require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-ccb-core-settings.php'; - require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-ccb-core-settings-page.php'; - require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-ccb-core-settings-section.php'; - require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-ccb-core-settings-field.php'; + $settings_section = new CCB_Core_Settings_Section( $section_id, $section ); + add_settings_section( + $section_id, + $section['section_title'], + [ + $settings_section, + 'render_section', + ], + $page_id + ); - // the class responsible for defining all actions that occur in the Dashboard. - require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-ccb-core-admin.php'; + if ( ! empty( $section['fields'] ) ) { + foreach ( $section['fields'] as $field_id => $field ) { - // the class that handles data synchronization between CCB and the local cache - require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-ccb-core-sync.php'; + $settings_field = new CCB_Core_Settings_Field( $field_id, $field ); + add_settings_field( + $field_id, + $field['field_title'], + [ + $settings_field, + 'render_field', + ], + $page_id, + $section_id + ); - // the class that handles data synchronization between CCB and the local cache - require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-ccb-core-cpts.php'; + } + } - // instantiate the loader - $this->loader = new CCB_Core_Loader(); + } + + } } /** - * Define the locale for this plugin for internationalization. - * - * Uses the CCB_Core_i18n class in order to set the domain and to register the hook - * with WordPress. + * After the options are saved, check to see if we + * should flush the rewrite rules. * - * @since 0.9.0 - * @access private + * @param array $old_value The previous option value. + * @param array $value The new option value. + * @access public + * @since 1.0.0 + * @return void */ - private function set_locale() { + public function updated_options( $old_value, $value ) { + + // Create a collection of settings that, if they change, should + // trigger a flush_rewrite_rules event. + $setting_array = [ + 'groups_enabled', + 'groups_slug', + 'calendar_enabled', + 'calendar_slug', + ]; - $plugin_i18n = new CCB_Core_i18n(); - $this->loader->add_action( 'plugins_loaded', $plugin_i18n, 'load_plugin_textdomain' ); + foreach ( $setting_array as $setting ) { + if ( isset( $value[ $setting ] ) ) { + if ( ! isset( $old_value[ $setting ] ) || $value[ $setting ] !== $old_value[ $setting ] ) { + // At least one option requires a flush, so set the transient and return. + set_transient( 'ccb_core_flush_rewrite_rules', true ); + return; + } + } + } } /** - * Register all of the hooks related to the dashboard functionality - * of the plugin. + * Checks for a flag that may have been previously + * set in order to flush the rewrite rules. * - * @since 0.9.0 - * @access private + * @return void + */ + public function check_rewrite_rules() { + if ( get_transient( 'ccb_core_flush_rewrite_rules' ) ) { + delete_transient( 'ccb_core_flush_rewrite_rules' ); + flush_rewrite_rules(); + } + } + + /** + * Register the stylesheets for the dashboard. + * + * @param string $hook Current admin page. + * @return void */ - private function define_admin_hooks() { - - $plugin_admin = new CCB_Core_Admin(); - - $this->loader->add_action( 'init', $plugin_admin, 'initialize_custom_post_types' ); - $this->loader->add_action( 'admin_menu', $plugin_admin, 'initialize_settings_menu' ); - $this->loader->add_action( 'admin_init', $plugin_admin, 'initialize_settings' ); - $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles' ); - $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' ); - $this->loader->add_filter( 'plugin_action_links_' . $this->plugin_basename, $plugin_admin, 'add_settings_link' ); - $this->loader->add_action( 'schedule_auto_refresh', $plugin_admin, 'auto_sync' ); - $this->loader->add_action( 'wp_loaded', $plugin_admin, 'check_auto_refresh' ); - $this->loader->add_action( 'pre_update_option_' . $this->plugin_settings_name, $plugin_admin, 'update_settings_callback', 10, 2 ); - $this->loader->add_action( 'schedule_flush_rewrite_rules', $plugin_admin, 'flush_rewrite_rules_event' ); - - // all backend ajax hooks - $this->loader->add_action( 'wp_ajax_sync', $plugin_admin, 'ajax_sync' ); - $this->loader->add_action( 'wp_ajax_poll_sync', $plugin_admin, 'ajax_poll_sync' ); - $this->loader->add_action( 'wp_ajax_test_credentials', $plugin_admin, 'ajax_test_credentials' ); - $this->loader->add_action( 'wp_ajax_get_latest_sync', $plugin_admin, 'ajax_get_latest_sync' ); + public function enqueue_styles( $hook ) { + + if ( false !== stristr( $hook, 'ccb_core_settings' ) ) { + wp_enqueue_style( 'ccb-core', CCB_CORE_URL . 'css/ccb-core-admin.css', [], CCB_CORE_VERSION, 'all' ); + wp_enqueue_style( 'switchery', CCB_CORE_URL . 'css/vendor/switchery.min.css', [], CCB_CORE_VERSION, 'all' ); + wp_enqueue_style( 'powerange', CCB_CORE_URL . 'css/vendor/powerange.min.css', [], CCB_CORE_VERSION, 'all' ); + wp_enqueue_style( 'picker', CCB_CORE_URL . 'css/vendor/default.css', [], CCB_CORE_VERSION, 'all' ); + wp_enqueue_style( 'picker-date', CCB_CORE_URL . 'css/vendor/default.date.css', [], CCB_CORE_VERSION, 'all' ); + wp_enqueue_style( 'tipr', CCB_CORE_URL . 'css/vendor/tipr.css', [], CCB_CORE_VERSION, 'all' ); + } } /** - * Run the loader to execute all of the hooks with WordPress. + * Register the scripts for the dashboard. * - * @since 0.9.0 + * @param string $hook Current admin page. + * @return void */ - public function run() { - $this->loader->run(); + public function enqueue_scripts( $hook ) { + + if ( false !== stristr( $hook, 'ccb_core_settings' ) ) { + wp_enqueue_script( 'ccb-core', CCB_CORE_URL . 'js/ccb-core-admin.js', [ 'jquery' ], CCB_CORE_VERSION, false ); + wp_enqueue_script( 'switchery', CCB_CORE_URL . 'js/vendor/switchery.min.js', [ 'jquery' ], CCB_CORE_VERSION, false ); + wp_enqueue_script( 'powerange', CCB_CORE_URL . 'js/vendor/powerange.min.js', [ 'jquery' ], CCB_CORE_VERSION, false ); + wp_enqueue_script( 'picker', CCB_CORE_URL . 'js/vendor/picker.js', [ 'jquery' ], CCB_CORE_VERSION, false ); + wp_enqueue_script( 'picker-date', CCB_CORE_URL . 'js/vendor/picker.date.js', [ 'picker' ], CCB_CORE_VERSION, false ); + wp_enqueue_script( 'tipr', CCB_CORE_URL . 'js/vendor/tipr.min.js', [ 'jquery' ], CCB_CORE_VERSION, false ); + wp_localize_script( + 'ccb-core', + 'CCB_CORE_SETTINGS', + [ + 'nonce' => wp_create_nonce( 'ccb_core_nonce' ), + 'translations' => [ + 'credentialsSuccessful' => esc_html__( 'The credentials were successfully authenticated.', 'ccb-core' ), + 'credentialsFailed' => esc_html__( 'The credentials failed authentication', 'ccb-core' ), + 'syncInProgress' => esc_html__( 'Syncronization in progress... You can safely navigate away from this page while we work in the background.', 'ccb-core' ), + ], + ] + ); + } + } /** - * The reference to the class that orchestrates the hooks with the plugin. + * Converts any legacy options to the new format * - * @since 0.9.0 - * @return CCB_Core_Loader Orchestrates the hooks of the plugin. + * @return void */ - public function get_loader() { - return $this->loader; + private function upgrade_to_1_0_0() { + $current_options = CCB_Core_Helpers::instance()->get_options(); + $updated_options = []; + $options_hash = [ + 'subdomain' => 'subdomain', + 'credentials' => 'credentials', + 'groups-enabled' => 'groups_enabled', + 'groups-name' => 'groups_name', + 'groups-slug' => 'groups_slug', + 'groups-import-images' => 'groups_import_images', + 'groups-advanced' => 'groups_advanced', + 'groups-exclude-from-search' => 'groups_exclude_from_search', + 'groups-publicly-queryable' => 'groups_publicly_queryable', + 'groups-show-ui' => 'groups_show_ui', + 'groups-show-in-nav-menus' => 'groups_show_in_nav_menus', + 'calendar-enabled' => 'calendar_enabled', + 'calendar-name' => 'calendar_name', + 'calendar-slug' => 'calendar_slug', + 'calendar-advanced' => 'calendar_advanced', + 'calendar-date-range-type' => 'calendar_date_range_type', + 'calendar-relative-weeks-past' => 'calendar_relative_weeks_past', + 'calendar-relative-weeks-future' => 'calendar_relative_weeks_future', + 'calendar-specific-start' => 'calendar_specific_start', + 'calendar-specific-end' => 'calendar_specific_end', + 'calendar-exclude-from-search' => 'calendar_exclude_from_search', + 'calendar-publicly-queryable' => 'calendar_publicly_queryable', + 'calendar-show-ui' => 'calendar_show_ui', + 'calendar-show-in-nav-menus' => 'calendar_show_in_nav_menus', + ]; + + if ( ! empty( $current_options ) ) { + foreach ( $options_hash as $old => $new ) { + if ( isset( $current_options[ $old ] ) ) { + $updated_options[ $new ] = $current_options[ $old ]; + } + } + update_option( 'ccb_core_settings', $updated_options ); + } } } diff --git a/includes/index.php b/includes/index.php deleted file mode 100644 index e71af0e..0000000 --- a/includes/index.php +++ /dev/null @@ -1 +0,0 @@ - + */ +class CCB_Core_Calendar extends CCB_Core_CPT { + + /** + * Name of the post type + * + * @var string + */ + public $name = 'ccb_core_calendar'; + + /** + * Initialize the class + */ + public function __construct() { + $options = CCB_Core_Helpers::instance()->get_options(); + $this->enabled = ! empty( $options['calendar_enabled'] ) ? true : false; + parent::__construct(); + } + + /** + * Setup the custom post type args + * + * @since 1.0.0 + * @return array $args for register_post_type + */ + public function get_post_args() { + + $options = CCB_Core_Helpers::instance()->get_options(); + $plural = ! empty( $options['calendar_name'] ) ? $options['calendar_name'] : __( 'Events', 'ccb-core' ); + $singular = ! empty( $options['calendar_name_singular'] ) ? $options['calendar_name_singular'] : __( 'Event', 'ccb-core' ); + $rewrite = ! empty( $options['calendar_slug'] ) ? [ 'slug' => sanitize_title( $options['calendar_slug'] ) ] : [ 'slug' => 'events' ]; + $has_archive = ! empty( $options['calendar_slug'] ) ? sanitize_title( $options['calendar_slug'] ) : 'events'; + $exclude_from_search = ! empty( $options['calendar_exclude_from_search'] ) && 'yes' === $options['calendar_exclude_from_search'] ? true : false; + $publicly_queryable = ! empty( $options['calendar_publicly_queryable'] ) && 'no' === $options['calendar_publicly_queryable'] ? false : true; + $show_ui = ! empty( $options['calendar_show_ui'] ) && 'no' === $options['calendar_show_ui'] ? false : true; + $show_in_nav_menus = ! empty( $options['calendar_show_in_nav_menus'] ) && 'yes' === $options['calendar_show_in_nav_menus'] ? true : false; + + return [ + 'labels' => [ + 'name' => $plural, + 'singular_name' => $singular, + 'all_items' => sprintf( __( 'All %s', 'ccb-core' ), $plural ), + 'add_new' => __( 'Add New', 'ccb-core' ), + 'add_new_item' => sprintf( __( 'Add New %s', 'ccb-core' ), $singular ), + 'edit' => __( 'Edit', 'ccb-core' ), + 'edit_item' => sprintf( __( 'Edit %s', 'ccb-core' ), $singular ), + 'new_item' => sprintf( __( 'New %s', 'ccb-core' ), $singular ), + 'view_item' => sprintf( __( 'View %s', 'ccb-core' ), $singular ), + 'search_items' => sprintf( __( 'Search %s', 'ccb-core' ), $plural ), + 'not_found' => __( 'Nothing found in the Database.', 'ccb-core' ), + 'not_found_in_trash' => __( 'Nothing found in Trash', 'ccb-core' ), + 'parent_item_colon' => '', + ], + 'description' => sprintf( __( 'These are the %s that are synchronized with your Church Community Builder software.', 'ccb-core' ), $plural ), + 'public' => true, + 'publicly_queryable' => $publicly_queryable, + 'exclude_from_search' => $exclude_from_search, + 'show_ui' => $show_ui, + 'show_in_nav_menus' => $show_in_nav_menus, + 'query_var' => true, + 'menu_position' => 8, + 'menu_icon' => 'dashicons-calendar', + 'rewrite' => $rewrite, + 'has_archive' => $has_archive, + 'capability_type' => 'post', + 'hierarchical' => false, + 'supports' => [ 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'custom-fields', 'sticky' ], + ]; + + } + + /** + * Configure the options that users are allowed to set + * + * @since 1.0.0 + * @param array $settings The settings definitions. + * @return array + */ + public function get_post_settings_definitions( $settings ) { + + $settings['ccb_core_settings_calendar'] = [ + 'page_title' => esc_html__( 'Public Events', 'ccb-core' ), + 'sections' => [ + 'calendar' => [ + 'section_title' => esc_html__( 'Public Events', 'ccb-core' ), + 'fields' => [ + 'calendar_enabled' => [ + 'field_title' => esc_html__( 'Enable Events', 'ccb-core' ), + 'field_render_function' => 'render_switch', + 'field_validation' => 'switch', + ], + 'calendar_name' => [ + 'field_title' => esc_html__( 'Event Display Name (Plural)', 'ccb-core' ), + 'field_render_function' => 'render_text', + 'field_placeholder' => esc_html__( 'Events', 'ccb-core' ), + 'field_validation' => 'alphanumeric_extended', + 'field_attributes' => [ 'data-requires' => '{"calendar_enabled":1}' ], + 'field_tooltip' => esc_html__( 'This is what you call the events in your church (i.e. Meetups, Hangouts, etc.).', 'ccb-core' ), + ], + 'calendar_name_singular' => [ + 'field_title' => esc_html__( 'Event Display Name (Singular)', 'ccb-core' ), + 'field_render_function' => 'render_text', + 'field_placeholder' => esc_html__( 'Event', 'ccb-core' ), + 'field_validation' => 'alphanumeric_extended', + 'field_attributes' => [ 'data-requires' => '{"calendar_enabled":1}' ], + 'field_tooltip' => esc_html__( 'This is the singular name of what you call the events in your church (i.e. Meetup, Hangout, etc.).', 'ccb-core' ), + ], + 'calendar_slug' => [ + 'field_title' => esc_html__( 'Events URL Name', 'ccb-core' ), + 'field_render_function' => 'render_text', + 'field_placeholder' => 'events', + 'field_validation' => 'slug', + 'field_attributes' => [ 'data-requires' => '{"calendar_enabled":1}' ], + 'field_tooltip' => esc_html__( 'This is typically where your theme will display all the events. WordPress calls this a "slug".', 'ccb-core' ), + ], + 'calendar_advanced' => [ + 'field_title' => esc_html__( 'Enable Advanced Settings (Optional)', 'ccb-core' ), + 'field_render_function' => 'render_switch', + 'field_validation' => 'switch', + 'field_attributes' => [ 'data-requires' => '{"calendar_enabled":1}' ], + ], + 'calendar_date_range_type' => [ + 'field_title' => esc_html__( 'Date Range Type', 'ccb-core' ), + 'field_render_function' => 'render_radio', + 'field_options' => [ + 'relative' => esc_html__( 'Relative Range', 'ccb-core' ), + 'specific' => esc_html__( 'Specific Range', 'ccb-core' ), + ], + 'field_validation' => '', + 'field_default' => 'relative', + 'field_attributes' => [ + 'class' => 'date-range-type', + 'data-requires' => '{"calendar_enabled":1,"calendar_advanced":1}', + ], + 'field_tooltip' => sprintf( + esc_html__( + 'Relative: For example, always get the events from "One week ago", up to "Eight weeks from now".%1$s + 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' ), + '
', + '

', + '
' + ), + ], + 'calendar_relative_weeks_past' => [ + 'field_title' => esc_html__( 'How Far Back?', 'ccb-core' ), + 'field_render_function' => 'render_slider', + 'field_options' => [ + 'min' => '0', + 'max' => '26', + 'units' => 'weeks', + ], + 'field_default' => 1, + 'field_validation' => '', + 'field_attributes' => [ 'data-requires' => '{"calendar_enabled":1,"calendar_advanced":1,"calendar_date_range_type":"relative"}' ], + 'field_tooltip' => esc_html__( 'Every time we synchronize, how many weeks in the past should we look? (0 would be "today")', 'ccb-core' ), + ], + 'calendar_relative_weeks_future' => [ + 'field_title' => esc_html__( 'How Into The Future?', 'ccb-core' ), + 'field_render_function' => 'render_slider', + 'field_options' => [ + 'min' => '1', + 'max' => '52', + 'units' => 'weeks', + ], + 'field_default' => 16, + 'field_validation' => '', + 'field_attributes' => [ 'data-requires' => '{"calendar_enabled":1,"calendar_advanced":1,"calendar_date_range_type":"relative"}' ], + 'field_tooltip' => esc_html__( 'Every time we synchronize, how many weeks in the future should we look?', 'ccb-core' ), + ], + 'calendar_specific_start' => [ + 'field_title' => esc_html__( 'Specific Start Date', 'ccb-core' ), + 'field_render_function' => 'render_date_picker', + 'field_validation' => '', + 'field_attributes' => [ 'data-requires' => '{"calendar_enabled":1,"calendar_advanced":1,"calendar_date_range_type":"specific"}' ], + 'field_tooltip' => sprintf( + esc_html__( + 'When synchronizing, we should get events that start after this date.%s + (Leave empty to always start "today")', + 'ccb-core' ), + '
' + ), + ], + 'calendar_specific_end' => [ + 'field_title' => esc_html__( 'Specific End Date', 'ccb-core' ), + 'field_render_function' => 'render_date_picker', + 'field_validation' => '', + 'field_attributes' => [ 'data-requires' => '{"calendar_enabled":1,"calendar_advanced":1,"calendar_date_range_type":"specific"}' ], + 'field_tooltip' => sprintf( + 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' ), + '
' + ), + ], + 'calendar_exclude_from_search' => [ + 'field_title' => esc_html__( 'Exclude From Search?', 'ccb-core' ), + 'field_render_function' => 'render_radio', + 'field_options' => [ + 'yes' => esc_html__( 'Yes', 'ccb-core' ), + 'no' => esc_html__( 'No', 'ccb-core' ), + ], + 'field_validation' => '', + 'field_default' => 'no', + 'field_attributes' => [ 'data-requires' => '{"calendar_enabled":1,"calendar_advanced":1}' ], + ], + 'calendar_publicly_queryable' => [ + 'field_title' => esc_html__( 'Publicly Queryable?', 'ccb-core' ), + 'field_render_function' => 'render_radio', + 'field_options' => [ + 'yes' => esc_html__( 'Yes', 'ccb-core' ), + 'no' => esc_html__( 'No', 'ccb-core' ), + ], + 'field_validation' => '', + 'field_default' => 'yes', + 'field_attributes' => [ 'data-requires' => '{"calendar_enabled":1,"calendar_advanced":1}' ], + ], + 'calendar_show_ui' => [ + 'field_title' => esc_html__( 'Show In Admin UI?', 'ccb-core' ), + 'field_render_function' => 'render_radio', + 'field_options' => [ + 'yes' => esc_html__( 'Yes', 'ccb-core' ), + 'no' => esc_html__( 'No', 'ccb-core' ), + ], + 'field_validation' => '', + 'field_default' => 'yes', + 'field_attributes' => [ 'data-requires' => '{"calendar_enabled":1,"calendar_advanced":1}' ], + ], + 'calendar_show_in_nav_menus' => [ + 'field_title' => esc_html__( 'Show In Navigation Menus?', 'ccb-core' ), + 'field_render_function' => 'render_radio', + 'field_options' => [ + 'yes' => esc_html__( 'Yes', 'ccb-core' ), + 'no' => esc_html__( 'No', 'ccb-core' ), + ], + 'field_validation' => '', + 'field_default' => 'no', + 'field_attributes' => [ 'data-requires' => '{"calendar_enabled":1,"calendar_advanced":1}' ], + ], + ], + ], + ], + ]; + + return $settings; + } + + /** + * Define the mapping of CCB API fields to the Post fields + * + * @since 1.0.0 + * @param array $maps A collection of mappings from the API to WordPress. + * @return array + */ + public function get_post_api_map( $maps ) { + if ( $this->enabled ) { + $calendar_options = $this->get_calendar_options(); + + $maps[ $this->name ] = [ + 'service' => 'public_calendar_listing', + 'data' => [ + 'date_start' => $calendar_options['date_start'], + 'date_end' => $calendar_options['date_end'], + ], + 'nodes' => [ 'items', 'item' ], + 'fields' => [ + 'event_name' => 'post_title', + 'event_description' => 'post_content', + 'date' => 'post_meta', + 'start_time' => 'post_meta', + 'end_time' => 'post_meta', + 'event_duration' => 'post_meta', + 'location' => 'post_meta', + ], + ]; + } + return $maps; + } + + /** + * Returns a standardized configuration array of + * start and end dates to be used by the API call. + * + * @return array + */ + private function get_calendar_options() { + $options = CCB_Core_Helpers::instance()->get_options(); + + // By default, set some sane limits. + $calendar_options = [ + 'date_start' => date( 'Y-m-d', strtotime( '1 weeks ago' ) ), + 'date_end' => date( 'Y-m-d', strtotime( '+8 weeks' ) ), + ]; + + // If the user has set a preferred date range type. + if ( ! empty( $options['calendar_date_range_type'] ) ) { + + if ( 'relative' === $options['calendar_date_range_type'] ) { + + $calendar_options['date_start'] = date( 'Y-m-d', strtotime( $options['calendar_relative_weeks_past'] . ' weeks ago' ) ); + $calendar_options['date_end'] = date( 'Y-m-d', strtotime( '+' . $options['calendar_relative_weeks_future'] . ' weeks' ) ); + + } elseif ( 'specific' === $options['calendar_date_range_type'] ) { + + // For each date range, do not let the user go further than + // 1 year in the past or 1 year into the future to prevent + // them from blowing up their server. + if ( ! empty( $options['calendar_specific_start'] ) ) { + $last_year = strtotime( '1 year ago' ); + $start_timestamp = strtotime( $options['calendar_specific_start'] ); + + if ( $last_year < $start_timestamp ) { + $calendar_options['date_start'] = date( 'Y-m-d', $start_timestamp ); + } else { + $calendar_options['date_start'] = date( 'Y-m-d', $last_year ); + } + } + + if ( ! empty( $options['calendar_specific_end'] ) ) { + $next_year = strtotime( '+1 year' ); + $end_timestamp = strtotime( $options['calendar_specific_end'] ); + + if ( $next_year > $end_timestamp ) { + $calendar_options['date_end'] = date( 'Y-m-d', $end_timestamp ); + } else { + $calendar_options['date_end'] = date( 'Y-m-d', $next_year ); + } + } + + } + } + + return $calendar_options; + } + +} + +new CCB_Core_Calendar(); diff --git a/includes/post-types/class-ccb-core-cpt.php b/includes/post-types/class-ccb-core-cpt.php new file mode 100644 index 0000000..cd9c447 --- /dev/null +++ b/includes/post-types/class-ccb-core-cpt.php @@ -0,0 +1,96 @@ + + */ +abstract class CCB_Core_CPT { + + /** + * Name of the post type + * + * @var string + */ + public $name; + + /** + * Whether or not this post type is enabled. (Overriden by child class). + * + * @var bool + */ + public $enabled = false; + + /** + * Initialize the class + */ + public function __construct() { + + add_filter( 'ccb_core_settings_post_definitions', [ $this, 'get_post_settings_definitions' ] ); + + // If this custom post type is enabled, merge the defaults and set the registration hook. + if ( $this->enabled ) { + add_action( 'init', [ $this, 'register_post_type' ] ); + add_filter( 'ccb_core_synchronizer_post_api_map', [ $this, 'get_post_api_map' ] ); + } + + } + + /** + * Register the custom post type + * + * @access public + * @since 1.0.0 + * @return void + */ + public function register_post_type() { + register_post_type( $this->name, $this->get_post_args() ); + } + + /** + * Get the post type object. + * + * @return object + */ + public function get_post_type_object() { + return get_post_type_object( $this->name ); + } + + /** + * Setup the custom post type $args + * + * @since 1.0.0 + * @return array $args for register_post_type + */ + abstract public function get_post_args(); + + /** + * Setup the default CPT options + * + * @since 1.0.0 + * @param array $settings The settings definitions. + * @return array The configuration for the options settable by the user + */ + abstract public function get_post_settings_definitions( $settings ); + + /** + * Define the mapping of CCB API fields to the Post fields + * + * @since 1.0.0 + * @param array $maps A collection of mappings from the API to WordPress. + * @return array + */ + abstract public function get_post_api_map( $maps ); + +} diff --git a/includes/post-types/class-ccb-core-group.php b/includes/post-types/class-ccb-core-group.php new file mode 100644 index 0000000..d0955fb --- /dev/null +++ b/includes/post-types/class-ccb-core-group.php @@ -0,0 +1,294 @@ + + */ +class CCB_Core_Group extends CCB_Core_CPT { + + /** + * Name of the post type + * + * @var string + */ + public $name = 'ccb_core_group'; + + /** + * Initialize the class + */ + public function __construct() { + add_filter( 'ccb_core_synchronizer_entity_insert_allowed', [ $this, 'entity_insert_update_allowed' ], 10, 4 ); + add_filter( 'ccb_core_synchronizer_entity_update_allowed', [ $this, 'entity_insert_update_allowed' ], 10, 4 ); + add_action( 'ccb_core_after_insert_update_post', [ $this, 'attach_group_image' ], 10, 5 ); + + $options = CCB_Core_Helpers::instance()->get_options(); + $this->enabled = ! empty( $options['groups_enabled'] ) ? true : false; + parent::__construct(); + } + + /** + * Setup the custom post type args + * + * @since 1.0.0 + * @return array $args for register_post_type + */ + public function get_post_args() { + + $options = CCB_Core_Helpers::instance()->get_options(); + $plural = ! empty( $options['groups_name'] ) ? $options['groups_name'] : __( 'Groups', 'ccb-core' ); + $singular = ! empty( $options['groups_name_singular'] ) ? $options['groups_name_singular'] : __( 'Group', 'ccb-core' ); + $rewrite = ! empty( $options['groups_slug'] ) ? [ 'slug' => sanitize_title( $options['groups_slug'] ) ] : [ 'slug' => 'groups' ]; + $has_archive = ! empty( $options['groups_slug'] ) ? sanitize_title( $options['groups_slug'] ) : 'groups'; + $exclude_from_search = ! empty( $options['groups_exclude_from_search'] ) && 'yes' === $options['groups_exclude_from_search'] ? true : false; + $publicly_queryable = ! empty( $options['groups_publicly_queryable'] ) && 'no' === $options['groups_publicly_queryable'] ? false : true; + $show_ui = ! empty( $options['groups_show_ui'] ) && 'no' === $options['groups_show_ui'] ? false : true; + $show_in_nav_menus = ! empty( $options['groups_show_in_nav_menus'] ) && 'yes' === $options['groups_show_in_nav_menus'] ? true : false; + + return [ + 'labels' => [ + 'name' => $plural, + 'singular_name' => $singular, + 'all_items' => sprintf( __( 'All %s', 'ccb-core' ), $plural ), + 'add_new' => __( 'Add New', 'ccb-core' ), + 'add_new_item' => sprintf( __( 'Add New %s', 'ccb-core' ), $singular ), + 'edit' => __( 'Edit', 'ccb-core' ), + 'edit_item' => sprintf( __( 'Edit %s', 'ccb-core' ), $singular ), + 'new_item' => sprintf( __( 'New %s', 'ccb-core' ), $singular ), + 'view_item' => sprintf( __( 'View %s', 'ccb-core' ), $singular ), + 'search_items' => sprintf( __( 'Search %s', 'ccb-core' ), $plural ), + 'not_found' => __( 'Nothing found in the Database.', 'ccb-core' ), + 'not_found_in_trash' => __( 'Nothing found in Trash', 'ccb-core' ), + 'parent_item_colon' => '', + ], + 'description' => sprintf( __( 'These are the %s that are synchronized with your Church Community Builder software.', 'ccb-core' ), $plural ), + 'public' => true, + 'publicly_queryable' => $publicly_queryable, + 'exclude_from_search' => $exclude_from_search, + 'show_ui' => $show_ui, + 'show_in_nav_menus' => $show_in_nav_menus, + 'query_var' => true, + 'menu_position' => 8, + 'menu_icon' => 'dashicons-groups', + 'rewrite' => $rewrite, + 'has_archive' => $has_archive, + 'capability_type' => 'post', + 'hierarchical' => false, + 'supports' => [ 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'custom-fields', 'sticky' ], + ]; + + } + + /** + * Configure the options that users are allowed to set + * + * @since 1.0.0 + * @param array $settings The settings definitions. + * @return array + */ + public function get_post_settings_definitions( $settings ) { + + $settings['ccb_core_settings_groups'] = [ + 'page_title' => esc_html__( 'Groups', 'ccb-core' ), + 'sections' => [ + 'groups' => [ + 'section_title' => esc_html__( 'Groups', 'ccb-core' ), + 'fields' => [ + 'groups_enabled' => [ + 'field_title' => esc_html__( 'Enable Groups', 'ccb-core' ), + 'field_render_function' => 'render_switch', + 'field_validation' => 'switch', + ], + 'groups_name' => [ + 'field_title' => esc_html__( 'Groups Display Name (Plural)', 'ccb-core' ), + 'field_render_function' => 'render_text', + 'field_placeholder' => esc_html__( 'Groups', 'ccb-core' ), + 'field_validation' => 'alphanumeric_extended', + 'field_attributes' => [ 'data-requires' => '{"groups_enabled":1}' ], + 'field_tooltip' => esc_html__( 'This is what you call the groups in your church (i.e. Home Groups, Connections, Life Groups, etc.).', 'ccb-core' ), + ], + 'groups_name_singular' => [ + 'field_title' => esc_html__( 'Groups Display Name (Singular)', 'ccb-core' ), + 'field_render_function' => 'render_text', + 'field_placeholder' => esc_html__( 'Group', 'ccb-core' ), + 'field_validation' => 'alphanumeric_extended', + 'field_attributes' => [ 'data-requires' => '{"groups_enabled":1}' ], + 'field_tooltip' => esc_html__( 'This is the singular name of what you call the groups in your church (i.e. Home Group, Connection, Life Group, etc.).', 'ccb-core' ), + ], + 'groups_slug' => [ + 'field_title' => esc_html__( 'Groups URL Name', 'ccb-core' ), + 'field_render_function' => 'render_text', + 'field_placeholder' => 'groups', + 'field_validation' => 'slug', + 'field_attributes' => [ 'data-requires' => '{"groups_enabled":1}' ], + 'field_tooltip' => esc_html__( 'This is typically where your theme will display all the groups. WordPress calls this a "slug".', 'ccb-core' ), + ], + 'groups_import_images' => [ + 'field_title' => esc_html__( 'Also Import Group Images?', 'ccb-core' ), + 'field_render_function' => 'render_radio', + 'field_options' => [ + 'yes' => esc_html__( 'Yes', 'ccb-core' ), + 'no' => esc_html__( 'No', 'ccb-core' ), + ], + 'field_validation' => '', + 'field_default' => 'no', + 'field_attributes' => [ 'data-requires' => '{"groups_enabled":1}' ], + 'field_tooltip' => sprintf( + 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' ), + '
' + ), + ], + 'groups_advanced' => [ + 'field_title' => esc_html__( 'Enable Advanced Settings (Optional)', 'ccb-core' ), + 'field_render_function' => 'render_switch', + 'field_validation' => 'switch', + 'field_attributes' => [ 'data-requires' => '{"groups_enabled":1}' ], + ], + 'groups_exclude_from_search' => [ + 'field_title' => esc_html__( 'Exclude From Search?', 'ccb-core' ), + 'field_render_function' => 'render_radio', + 'field_options' => [ + 'yes' => esc_html__( 'Yes', 'ccb-core' ), + 'no' => esc_html__( 'No', 'ccb-core' ), + ], + 'field_validation' => '', + 'field_default' => 'no', + 'field_attributes' => [ 'data-requires' => '{"groups_enabled":1,"groups_advanced":1}' ], + ], + 'groups_publicly_queryable' => [ + 'field_title' => esc_html__( 'Publicly Queryable?', 'ccb-core' ), + 'field_render_function' => 'render_radio', + 'field_options' => [ + 'yes' => esc_html__( 'Yes', 'ccb-core' ), + 'no' => esc_html__( 'No', 'ccb-core' ), + ], + 'field_validation' => '', + 'field_default' => 'yes', + 'field_attributes' => [ 'data-requires' => '{"groups_enabled":1,"groups_advanced":1}' ], + ], + 'groups_show_ui' => [ + 'field_title' => esc_html__( 'Show In Admin UI?', 'ccb-core' ), + 'field_render_function' => 'render_radio', + 'field_options' => [ + 'yes' => esc_html__( 'Yes', 'ccb-core' ), + 'no' => esc_html__( 'No', 'ccb-core' ), + ], + 'field_validation' => '', + 'field_default' => 'yes', + 'field_attributes' => [ 'data-requires' => '{"groups_enabled":1,"groups_advanced":1}' ], + ], + 'groups_show_in_nav_menus' => [ + 'field_title' => esc_html__( 'Show In Navigation Menus?', 'ccb-core' ), + 'field_render_function' => 'render_radio', + 'field_options' => [ + 'yes' => esc_html__( 'Yes', 'ccb-core' ), + 'no' => esc_html__( 'No', 'ccb-core' ), + ], + 'field_validation' => '', + 'field_default' => 'no', + 'field_attributes' => [ 'data-requires' => '{"groups_enabled":1,"groups_advanced":1}' ], + ], + ], + ], + ], + ]; + + return $settings; + + } + + /** + * Define the mapping of CCB API fields to the Post fields + * + * @since 1.0.0 + * @param array $maps A collection of mappings from the API to WordPress. + * @return array + */ + public function get_post_api_map( $maps ) { + if ( $this->enabled ) { + $options = CCB_Core_Helpers::instance()->get_options(); + $include_image_link = ! empty( $options['groups_import_images'] ) && 'yes' === $options['groups_import_images'] ? true : false; + + $maps[ $this->name ] = [ + 'service' => 'group_profiles', + 'data' => [ + 'include_participants' => false, + 'include_image_link' => $include_image_link, + ], + 'nodes' => [ 'groups', 'group' ], + 'fields' => [ + 'name' => 'post_title', + 'description' => 'post_content', + 'main_leader' => 'post_meta', + 'calendar_feed' => 'post_meta', + 'current_members' => 'post_meta', + 'group_capacity' => 'post_meta', + 'addresses' => 'post_meta', + ], + ]; + } + return $maps; + } + + /** + * Callback function for `ccb_core_synchronizer_entity_insert_allowed` and + * `ccb_core_synchronizer_entity_update_allowed` so that we can filter OUT + * inactive and non-public groups from an import. + * + * @since 1.0.0 + * + * @param bool $allowed Whether an insert/update is allowed. + * @param SimpleXML $entity The specific entity object. + * @param mixed $entity_id A unique entity id. + * @param string $post_type The current post type. + * @return bool + */ + public function entity_insert_update_allowed( $allowed, $entity, $entity_id, $post_type ) { + if ( $this->name === $post_type ) { + // Only allow active, publicly listed groups to be imported. + if ( 'true' === (string) $entity->inactive || 'false' === (string) $entity->public_search_listed ) { + $allowed = false; + } + } + return $allowed; + } + + /** + * Checks whether downloading group images is enabled + * and an entity has an image attachment, then attaches + * the image as a featured image. + * + * @since 1.0.0 + * + * @param SimpleXML $entity The entity object. + * @param array $settings The settings array for the import. + * @param array $args The `wp_insert_post` args. + * @param string $post_type The current post type. + * @param int $post_id The WordPress post id of this post. + * @return void + */ + public function attach_group_image( $entity, $settings, $args, $post_type, $post_id ) { + if ( $this->name === $post_type && $settings['data']['include_image_link'] ) { + $image_url = (string) $entity->image; + if ( $image_url ) { + CCB_Core_Helpers::instance()->download_image( $image_url, $args['post_title'], $post_id ); + } + } + } +} + +new CCB_Core_Group(); diff --git a/includes/post-types/class-example-post-type.php b/includes/post-types/class-example-post-type.php new file mode 100644 index 0000000..12e784e --- /dev/null +++ b/includes/post-types/class-example-post-type.php @@ -0,0 +1,146 @@ + + */ +class Example_Post_Type extends CCB_Core_CPT { + + /** + * Name of the post type + * + * @var string + */ + public $name = 'ccb_core_example_post'; + + /** + * Use the constructor to add any actions / filters. + */ + public function __construct() { + + // There are several actions and filters that allow you to run additional code during + // the synchronization process. If you need to hook into them, define them here. + add_action( 'ccb_core_after_insert_update_post', [ $this, 'my_callback_after_post_inserted' ], 10, 5 ); + + parent::__construct(); + } + + /** + * Setup the custom post type args + * + * @since 1.0.0 + * @return array $args for register_post_type + */ + public function get_post_args() { + + return [ + 'labels' => [ + 'name' => 'Example Posts', + 'singular_name' => 'Example Post', + 'all_items' => 'All Example Posts', + 'add_new' => 'Add New', + 'add_new_item' => 'Add New Example Post', + 'edit' => 'Edit', + 'edit_item' => 'Edit Example Post', + 'new_item' => 'New Example Post', + 'view_item' => 'View Example Post', + 'search_items' => 'Search Example Posts', + 'not_found' => 'Nothing found in the Database.', + 'not_found_in_trash' => 'Nothing found in Trash', + ], + 'description' => 'These are Example Posts that came from CCB', + 'public' => true, + 'publicly_queryable' => true, + 'exclude_from_search' => false, + 'show_ui' => true, + 'show_in_nav_menus' => false, + 'query_var' => true, + 'menu_position' => 8, + 'rewrite' => [ 'slug' => 'examples' ], + 'has_archive' => 'examples', + 'capability_type' => 'post', + 'hierarchical' => false, + 'supports' => [ 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'custom-fields', 'sticky' ], + ]; + + } + + /** + * Configure the options that users are allowed to set + * + * @since 1.0.0 + * @param array $settings The settings definitions. + * @return array + */ + public function get_post_settings_definitions( $settings ) { + // This method is required, but you do not need to actually create + // a settings page or settings fields if you don't need them. Just + // return the settings in that case. + return $settings; + } + + /** + * Define the mapping of CCB API fields to the Post fields + * + * @since 1.0.0 + * @param array $maps A collection of mappings from the API to WordPress. + * @return array + */ + public function get_post_api_map( $maps ) { + + $maps[ $this->name ] = [ + 'service' => 'ccb_service_name', // This becomes the `srv` URL parameter in the API request. + 'data' => [ + 'another_parameter' => true, // These are any additional URL parameters that need to be sent with the API request. + 'yet_another_parameter' => 'abc123', + ], + 'nodes' => [ 'elements', 'element' ], // The path from all the way to (and including) the CCB Entity. + 'fields' => [ + 'some_property' => 'post_title', // Map to a Post Title from an entity's property. + 'another_property' => 'post_content', // Map to any other WP_Post property by name. + 'a_third_property' => 'post_meta', // Map to `post_meta` to have this saved as post meta. + ], + ]; + + return $maps; + } + + /** + * Run additional logic after a post gets inserted + * + * @since 1.0.0 + * + * @param SimpleXML $entity The entity object. + * @param array $settings The settings array for the import. + * @param array $args The `wp_insert_post` args. + * @param string $post_type The current post type. + * @param int $post_id The WordPress post id of this post. + * @return void + */ + public function my_callback_after_post_inserted( $entity, $settings, $args, $post_type, $post_id ) { + // If this is a callback for this post type... + // phpcs:ignore + if ( $this->name === $post_type ) { + // Perhaps you want to inspect the new post after it gets inserted. You now have access + // to the original Entity (XML object from CCB), the post id, etc. You can now alter the post + // with any custom logic. For example, on the CCB_Core_Group post type we check whether the + // post should also have a featured image (and we download the group image and attach it). + } + } +} + +new Example_Post_Type(); diff --git a/includes/taxonomies/class-ccb-core-calendar-event-type.php b/includes/taxonomies/class-ccb-core-calendar-event-type.php new file mode 100644 index 0000000..b006a86 --- /dev/null +++ b/includes/taxonomies/class-ccb-core-calendar-event-type.php @@ -0,0 +1,65 @@ + + */ +class CCB_Core_Calendar_Event_Type extends CCB_Core_Taxonomy { + + /** + * Name of the taxonomy + * + * @var string + */ + public $name = 'ccb_core_calendar_event_type'; + + /** + * Object types for this taxonomy + * + * @var array + */ + public $object_types = [ 'ccb_core_calendar' ]; + + /** + * Setup the default taxonomy mappings + * + * @since 1.0.0 + * @return array Default options for register_taxonomy + */ + public static function get_taxonomy_args() { + return [ + 'labels' => [ + 'name' => __( 'Types', 'ccb-core' ), + 'singular_name' => __( 'Type', 'ccb-core' ), + 'search_items' => __( 'Search Types', 'ccb-core' ), + 'all_items' => __( 'All Types', 'ccb-core' ), + 'parent_item' => __( 'Parent Type', 'ccb-core' ), + 'parent_item_colon' => __( 'Parent Type:', 'ccb-core' ), + 'edit_item' => __( 'Edit Type', 'ccb-core' ), + 'update_item' => __( 'Update Type', 'ccb-core' ), + 'add_new_item' => __( 'Add New Type', 'ccb-core' ), + 'new_item_name' => __( 'New Type', 'ccb-core' ), + ], + 'hierarchical' => true, + 'show_admin_column' => true, + 'show_ui' => true, + 'query_var' => true, + 'api_mapping' => 'event_type', // The field key from the CCB API. + ]; + } + +} + +new CCB_Core_Calendar_Event_Type(); diff --git a/includes/taxonomies/class-ccb-core-calendar-group-name.php b/includes/taxonomies/class-ccb-core-calendar-group-name.php new file mode 100644 index 0000000..94280e9 --- /dev/null +++ b/includes/taxonomies/class-ccb-core-calendar-group-name.php @@ -0,0 +1,65 @@ + + */ +class CCB_Core_Calendar_Group_Name extends CCB_Core_Taxonomy { + + /** + * Name of the taxonomy + * + * @var string + */ + public $name = 'ccb_core_calendar_group_name'; + + /** + * Object types for this taxonomy + * + * @var array + */ + public $object_types = [ 'ccb_core_calendar' ]; + + /** + * Setup the default taxonomy mappings + * + * @since 1.0.0 + * @return array Default options for register_taxonomy + */ + public static function get_taxonomy_args() { + return [ + 'labels' => [ + 'name' => __( 'Group Names', 'ccb-core' ), + 'singular_name' => __( 'Group Name', 'ccb-core' ), + 'search_items' => __( 'Search Group Names', 'ccb-core' ), + 'all_items' => __( 'All Group Names', 'ccb-core' ), + 'parent_item' => __( 'Parent Group Name', 'ccb-core' ), + 'parent_item_colon' => __( 'Parent Group Name:', 'ccb-core' ), + 'edit_item' => __( 'Edit Group Name', 'ccb-core' ), + 'update_item' => __( 'Update Group Name', 'ccb-core' ), + 'add_new_item' => __( 'Add New Group Name', 'ccb-core' ), + 'new_item_name' => __( 'New Group Name', 'ccb-core' ), + ], + 'hierarchical' => true, + 'show_admin_column' => true, + 'show_ui' => true, + 'query_var' => true, + 'api_mapping' => 'group_name', // The field key from the CCB API. + ]; + } + +} + +new CCB_Core_Calendar_Group_Name(); diff --git a/includes/taxonomies/class-ccb-core-calendar-grouping-name.php b/includes/taxonomies/class-ccb-core-calendar-grouping-name.php new file mode 100644 index 0000000..05dd6ee --- /dev/null +++ b/includes/taxonomies/class-ccb-core-calendar-grouping-name.php @@ -0,0 +1,65 @@ + + */ +class CCB_Core_Calendar_Grouping_Name extends CCB_Core_Taxonomy { + + /** + * Name of the taxonomy + * + * @var string + */ + public $name = 'ccb_core_calendar_grouping_name'; + + /** + * Object types for this taxonomy + * + * @var array + */ + public $object_types = [ 'ccb_core_calendar' ]; + + /** + * Setup the default taxonomy mappings + * + * @since 1.0.0 + * @return array Default options for register_taxonomy + */ + public static function get_taxonomy_args() { + return [ + 'labels' => [ + 'name' => __( 'Grouping Names', 'ccb-core' ), + 'singular_name' => __( 'Grouping Name', 'ccb-core' ), + 'search_items' => __( 'Search Grouping Names', 'ccb-core' ), + 'all_items' => __( 'All Grouping Names', 'ccb-core' ), + 'parent_item' => __( 'Parent Grouping Name', 'ccb-core' ), + 'parent_item_colon' => __( 'Parent Grouping Name:', 'ccb-core' ), + 'edit_item' => __( 'Edit Grouping Name', 'ccb-core' ), + 'update_item' => __( 'Update Grouping Name', 'ccb-core' ), + 'add_new_item' => __( 'Add New Grouping Name', 'ccb-core' ), + 'new_item_name' => __( 'New Grouping Name', 'ccb-core' ), + ], + 'hierarchical' => true, + 'show_admin_column' => true, + 'show_ui' => true, + 'query_var' => true, + 'api_mapping' => 'grouping_name', // The field key from the CCB API. + ]; + } + +} + +new CCB_Core_Calendar_Grouping_Name(); diff --git a/includes/taxonomies/class-ccb-core-group-area.php b/includes/taxonomies/class-ccb-core-group-area.php new file mode 100644 index 0000000..9215a07 --- /dev/null +++ b/includes/taxonomies/class-ccb-core-group-area.php @@ -0,0 +1,65 @@ + + */ +class CCB_Core_Group_Area extends CCB_Core_Taxonomy { + + /** + * Name of the taxonomy + * + * @var string + */ + public $name = 'ccb_core_group_area'; + + /** + * Object types for this taxonomy + * + * @var array + */ + public $object_types = [ 'ccb_core_group' ]; + + /** + * Setup the default taxonomy mappings + * + * @since 1.0.0 + * @return array Default options for register_taxonomy + */ + public static function get_taxonomy_args() { + return [ + 'labels' => [ + 'name' => __( 'Areas', 'ccb-core' ), + 'singular_name' => __( 'Area', 'ccb-core' ), + 'search_items' => __( 'Search Areas', 'ccb-core' ), + 'all_items' => __( 'All Areas', 'ccb-core' ), + 'parent_item' => __( 'Parent Area', 'ccb-core' ), + 'parent_item_colon' => __( 'Parent Area:', 'ccb-core' ), + 'edit_item' => __( 'Edit Area', 'ccb-core' ), + 'update_item' => __( 'Update Area', 'ccb-core' ), + 'add_new_item' => __( 'Add New Area', 'ccb-core' ), + 'new_item_name' => __( 'New Area', 'ccb-core' ), + ], + 'hierarchical' => true, + 'show_admin_column' => true, + 'show_ui' => true, + 'query_var' => true, + 'api_mapping' => 'area', // The field key from the CCB API. + ]; + } + +} + +new CCB_Core_Group_Area(); diff --git a/includes/taxonomies/class-ccb-core-group-day.php b/includes/taxonomies/class-ccb-core-group-day.php new file mode 100644 index 0000000..da7bf7f --- /dev/null +++ b/includes/taxonomies/class-ccb-core-group-day.php @@ -0,0 +1,65 @@ + + */ +class CCB_Core_Group_Day extends CCB_Core_Taxonomy { + + /** + * Name of the taxonomy + * + * @var string + */ + public $name = 'ccb_core_group_day'; + + /** + * Object types for this taxonomy + * + * @var array + */ + public $object_types = [ 'ccb_core_group' ]; + + /** + * Setup the default taxonomy mappings + * + * @since 1.0.0 + * @return array Default options for register_taxonomy + */ + public static function get_taxonomy_args() { + return [ + 'labels' => [ + 'name' => __( 'Days', 'ccb-core' ), + 'singular_name' => __( 'Day', 'ccb-core' ), + 'search_items' => __( 'Search Days', 'ccb-core' ), + 'all_items' => __( 'All Days', 'ccb-core' ), + 'parent_item' => __( 'Parent Day', 'ccb-core' ), + 'parent_item_colon' => __( 'Parent Day:', 'ccb-core' ), + 'edit_item' => __( 'Edit Day', 'ccb-core' ), + 'update_item' => __( 'Update Day', 'ccb-core' ), + 'add_new_item' => __( 'Add New Day', 'ccb-core' ), + 'new_item_name' => __( 'New Day', 'ccb-core' ), + ], + 'hierarchical' => true, + 'show_admin_column' => true, + 'show_ui' => true, + 'query_var' => true, + 'api_mapping' => 'meeting_day', // The field key from the CCB API. + ]; + } + +} + +new CCB_Core_Group_Day(); diff --git a/includes/taxonomies/class-ccb-core-group-department.php b/includes/taxonomies/class-ccb-core-group-department.php new file mode 100644 index 0000000..d7c06fa --- /dev/null +++ b/includes/taxonomies/class-ccb-core-group-department.php @@ -0,0 +1,65 @@ + + */ +class CCB_Core_Group_Department extends CCB_Core_Taxonomy { + + /** + * Name of the taxonomy + * + * @var string + */ + public $name = 'ccb_core_group_department'; + + /** + * Object types for this taxonomy + * + * @var array + */ + public $object_types = [ 'ccb_core_group' ]; + + /** + * Setup the default taxonomy mappings + * + * @since 1.0.0 + * @return array Default options for register_taxonomy + */ + public static function get_taxonomy_args() { + return [ + 'labels' => [ + 'name' => __( 'Departments', 'ccb-core' ), + 'singular_name' => __( 'Department', 'ccb-core' ), + 'search_items' => __( 'Search Departments', 'ccb-core' ), + 'all_items' => __( 'All Departments', 'ccb-core' ), + 'parent_item' => __( 'Parent Department', 'ccb-core' ), + 'parent_item_colon' => __( 'Parent Department:', 'ccb-core' ), + 'edit_item' => __( 'Edit Department', 'ccb-core' ), + 'update_item' => __( 'Update Department', 'ccb-core' ), + 'add_new_item' => __( 'Add New Department', 'ccb-core' ), + 'new_item_name' => __( 'New Department', 'ccb-core' ), + ], + 'hierarchical' => true, + 'show_admin_column' => true, + 'show_ui' => true, + 'query_var' => true, + 'api_mapping' => 'department', // The field key from the CCB API. + ]; + } + +} + +new CCB_Core_Group_Department(); diff --git a/includes/taxonomies/class-ccb-core-group-tag.php b/includes/taxonomies/class-ccb-core-group-tag.php new file mode 100644 index 0000000..5430461 --- /dev/null +++ b/includes/taxonomies/class-ccb-core-group-tag.php @@ -0,0 +1,67 @@ + + */ +class CCB_Core_Group_Tag extends CCB_Core_Taxonomy { + + /** + * Name of the taxonomy + * + * @var string + */ + public $name = 'ccb_core_group_tag'; + + /** + * Object types for this taxonomy + * + * @var array + */ + public $object_types = [ 'ccb_core_group' ]; + + /** + * Setup the default taxonomy mappings + * + * @since 1.0.0 + * @return array Default options for register_taxonomy + */ + public static function get_taxonomy_args() { + return [ + 'labels' => [ + 'name' => __( 'Group Tags', 'ccb-core' ), + 'singular_name' => __( 'Group Tag', 'ccb-core' ), + 'search_items' => __( 'Search Group Tags', 'ccb-core' ), + 'all_items' => __( 'All Group Tags', 'ccb-core' ), + 'parent_item' => __( 'Parent Group Tag', 'ccb-core' ), + 'parent_item_colon' => __( 'Parent Group Tag:', 'ccb-core' ), + 'edit_item' => __( 'Edit Group Tag', 'ccb-core' ), + 'update_item' => __( 'Update Group Tag', 'ccb-core' ), + 'add_new_item' => __( 'Add New Group Tag', 'ccb-core' ), + 'new_item_name' => __( 'New Group Tag', 'ccb-core' ), + ], + 'hierarchical' => false, + 'show_admin_column' => true, + 'show_ui' => true, + 'query_var' => true, + 'api_mapping' => [ + 'childcare_provided' => __( 'Childcare Provided', 'ccb-core' ), // The field key from the CCB API. + ], + ]; + } + +} + +new CCB_Core_Group_Tag(); diff --git a/includes/taxonomies/class-ccb-core-group-time.php b/includes/taxonomies/class-ccb-core-group-time.php new file mode 100644 index 0000000..16cebc7 --- /dev/null +++ b/includes/taxonomies/class-ccb-core-group-time.php @@ -0,0 +1,65 @@ + + */ +class CCB_Core_Group_Time extends CCB_Core_Taxonomy { + + /** + * Name of the taxonomy + * + * @var string + */ + public $name = 'ccb_core_group_time'; + + /** + * Object types for this taxonomy + * + * @var array + */ + public $object_types = [ 'ccb_core_group' ]; + + /** + * Setup the default taxonomy mappings + * + * @since 1.0.0 + * @return array Default options for register_taxonomy + */ + public static function get_taxonomy_args() { + return [ + 'labels' => [ + 'name' => __( 'Times', 'ccb-core' ), + 'singular_name' => __( 'Time', 'ccb-core' ), + 'search_items' => __( 'Search Times', 'ccb-core' ), + 'all_items' => __( 'All Times', 'ccb-core' ), + 'parent_item' => __( 'Parent Time', 'ccb-core' ), + 'parent_item_colon' => __( 'Parent Time:', 'ccb-core' ), + 'edit_item' => __( 'Edit Time', 'ccb-core' ), + 'update_item' => __( 'Update Time', 'ccb-core' ), + 'add_new_item' => __( 'Add New Time', 'ccb-core' ), + 'new_item_name' => __( 'New Time', 'ccb-core' ), + ], + 'hierarchical' => true, + 'show_admin_column' => true, + 'show_ui' => true, + 'query_var' => true, + 'api_mapping' => 'meeting_time', // The field key from the CCB API. + ]; + } + +} + +new CCB_Core_Group_Time(); diff --git a/includes/taxonomies/class-ccb-core-group-type.php b/includes/taxonomies/class-ccb-core-group-type.php new file mode 100644 index 0000000..c3fcafe --- /dev/null +++ b/includes/taxonomies/class-ccb-core-group-type.php @@ -0,0 +1,65 @@ + + */ +class CCB_Core_Group_Type extends CCB_Core_Taxonomy { + + /** + * Name of the taxonomy + * + * @var string + */ + public $name = 'ccb_core_group_type'; + + /** + * Object types for this taxonomy + * + * @var array + */ + public $object_types = [ 'ccb_core_group' ]; + + /** + * Setup the default taxonomy mappings + * + * @since 1.0.0 + * @return array Default options for register_taxonomy + */ + public static function get_taxonomy_args() { + return [ + 'labels' => [ + 'name' => __( 'Types', 'ccb-core' ), + 'singular_name' => __( 'Type', 'ccb-core' ), + 'search_items' => __( 'Search Types', 'ccb-core' ), + 'all_items' => __( 'All Types', 'ccb-core' ), + 'parent_item' => __( 'Parent Type', 'ccb-core' ), + 'parent_item_colon' => __( 'Parent Type:', 'ccb-core' ), + 'edit_item' => __( 'Edit Type', 'ccb-core' ), + 'update_item' => __( 'Update Type', 'ccb-core' ), + 'add_new_item' => __( 'Add New Type', 'ccb-core' ), + 'new_item_name' => __( 'New Type', 'ccb-core' ), + ], + 'hierarchical' => true, + 'show_admin_column' => true, + 'show_ui' => true, + 'query_var' => true, + 'api_mapping' => 'group_type', // The field key from the CCB API. + ]; + } + +} + +new CCB_Core_Group_Type(); diff --git a/includes/taxonomies/class-ccb-core-taxonomy.php b/includes/taxonomies/class-ccb-core-taxonomy.php new file mode 100644 index 0000000..1709f7c --- /dev/null +++ b/includes/taxonomies/class-ccb-core-taxonomy.php @@ -0,0 +1,70 @@ + + */ +abstract class CCB_Core_Taxonomy { + + /** + * Name of the taxonomy + * + * @var string + */ + public $name; + + /** + * Object types for this taxonomy + * + * @var array + */ + public $object_types = []; + + /** + * Initialize the class + */ + public function __construct() { + add_action( 'init', [ $this, 'register_taxonomy' ] ); + add_filter( 'ccb_core_synchronizer_taxonomy_api_map', [ $this, 'get_taxonomy_map' ] ); + } + + /** + * Register the custom taxonomy + * + * @return void + */ + public function register_taxonomy() { + register_taxonomy( $this->name, $this->object_types, static::get_taxonomy_args() ); + } + + /** + * Define the mapping of CCB API fields to this taxonomy + * + * @since 1.0.0 + * @param array $map A collection of mappings from the API to WordPress. + * @return array + */ + public function get_taxonomy_map( $map ) { + if ( ! empty( $this->object_types ) ) { + foreach ( $this->object_types as $object_type ) { + $taxonomy_args = static::get_taxonomy_args(); + $hierarchical = ! empty( $taxonomy_args['hierarchical'] ) ? 'hierarchical' : 'nonhierarchical'; + $map[ $object_type ]['taxonomies'][ $hierarchical ][ $this->name ] = $taxonomy_args['api_mapping']; + } + } + return $map; + } + +} diff --git a/includes/taxonomies/class-example-taxonomy.php b/includes/taxonomies/class-example-taxonomy.php new file mode 100644 index 0000000..37aa367 --- /dev/null +++ b/includes/taxonomies/class-example-taxonomy.php @@ -0,0 +1,69 @@ + + */ +class Example_Taxonomy extends CCB_Core_Taxonomy { + + /** + * Name of the taxonomy + * + * @var string + */ + public $name = 'ccb_core_example_taxonomy'; + + /** + * Object types for this taxonomy + * + * This may be attached to multiple post types, if needed. + * + * @var array + */ + public $object_types = [ 'ccb_core_example_post' ]; + + /** + * Setup the default taxonomy mappings + * + * @since 1.0.0 + * @return array Default options for register_taxonomy + */ + public static function get_taxonomy_args() { + return [ + 'labels' => [ + 'name' => 'Example Categories', + 'singular_name' => 'Example Category', + 'search_items' => 'Search Example Categories', + 'all_items' => 'All Example Categories', + 'parent_item' => 'Parent Example Category', + 'parent_item_colon' => 'Parent Example Category:', + 'edit_item' => 'Edit Example Category', + 'update_item' => 'Update Example Category', + 'add_new_item' => 'Add New Example Category', + 'new_item_name' => 'New Example Category', + ], + 'hierarchical' => true, + 'show_admin_column' => true, + 'show_ui' => true, + 'query_var' => true, + 'api_mapping' => 'entity_property_name', // The field key from the CCB API. + ]; + } + +} + +new Example_Taxonomy(); diff --git a/index.php b/index.php index e71af0e..56a444d 100644 --- a/index.php +++ b/index.php @@ -1 +1 @@ -', initialize : function() { this.syncPollId = setInterval(this.pollForActiveSync, 10000); this.pollForActiveSync(); - $('.test-login-wrapper .button').on('click', event, this.testCredentials); + $('.test-credentials-wrapper .button').on('click', window.event, this.testCredentials); - $('.sync-wrapper .button').on('click', event, this.syncData); + $('.sync-wrapper .button').on('click', window.event, this.syncData); var switches = Array.prototype.slice.call(document.querySelectorAll('.js-switch')); switches.forEach(function(html) { @@ -50,7 +51,6 @@ refreshEnabledFields : function() { $('[data-requires]').each(function() { - //var requiredElements = $(this).data('requires').split(' '); var displayField = true; var requiresObject = $(this).data('requires'); @@ -59,13 +59,13 @@ if (requiresObject.hasOwnProperty(key)) { var requiredElement = $("[name='ccb_core_settings[" + key + "]']"); - if (requiredElement.is(':checkbox')) { + if (requiredElement.is('input:checkbox')) { if (!requiredElement.is(':checked')) { displayField = false; break; } } - else if (requiredElement.is(':radio')) { + else if (requiredElement.is('input:radio')) { requiredElement = $("[name='ccb_core_settings[" + key + "]']:checked"); if (requiredElement.val() !== requiresObject[key]) displayField = false; @@ -90,17 +90,22 @@ var data = { 'action': 'get_latest_sync', - 'nextNonce': CCB_CORE_SETTINGS.nextNonce + 'nonce': CCB_CORE_SETTINGS.nonce }; $.post(ajaxurl, data, function(response) { + if (true === response.success) { + var $resultsWrapper = $('.ccb-core-latest-results'); + var $syncButton = $('.sync-wrapper .button'); + var className = response.data.success ? 'notice-info' : 'notice-error'; + var $content = $('
'); - var $latestSyncMessageWrapper = $('.ccb-core-latest-results'); - var labelContent = 'Latest Sync Results
' + response.description; - - $latestSyncMessageWrapper.removeClass('error notice updated').addClass(response.style); - $latestSyncMessageWrapper.empty().append(labelContent); + $content.append('

' + response.data.message + '

'); + $resultsWrapper.append($content); + $resultsWrapper.find('.spinner').remove(); + $syncButton.removeClass('disabled'); + } }); }, @@ -109,23 +114,44 @@ var data = { 'action': 'poll_sync', - 'nextNonce': CCB_CORE_SETTINGS.nextNonce + 'nonce': CCB_CORE_SETTINGS.nonce }; + CCBCoreAdmin.disableUI(); + $.post(ajaxurl, data, function(response) { - if ( response.syncInProgress == false ) { - clearInterval(ccbCoreAdmin.syncPollId); - ccbCoreAdmin.updateLatestSync(); + if (true === response.success) { + if (false === response.data) { + // A sync is not currently in progress. + clearInterval(CCBCoreAdmin.syncPollId); + CCBCoreAdmin.updateLatestSync(); + } else { + CCBCoreAdmin.adminNotice( CCB_CORE_SETTINGS.translations.syncInProgress, 'warning' ); + } + } + }); - var $spinner = $('.sync-wrapper .spinner'); - var $syncButtons = $('.sync-wrapper .button'); - var $syncMessages = $('div.in-progress-message'); + }, - $spinner.removeClass('is-active'); - $syncButtons.removeClass('disabled'); - $syncMessages.remove(); + syncData : function(event) { - } + event.preventDefault(); + var $syncButton = $('.sync-wrapper .button'); + + if ( $syncButton.hasClass('disabled') ) { + return false; + } + + CCBCoreAdmin.disableUI(); + + var data = { + 'action': 'sync', + 'nonce': CCB_CORE_SETTINGS.nonce + }; + + $.post(ajaxurl, data, function(response) { + CCBCoreAdmin.adminNotice( CCB_CORE_SETTINGS.translations.syncInProgress, 'warning' ); + CCBCoreAdmin.syncPollId = setInterval(CCBCoreAdmin.pollForActiveSync, 10000); }); }, @@ -137,72 +163,68 @@ return false; } - var $spinner = $('.test-login-wrapper .spinner'); - var $testLoginWrapper = $('.test-login-wrapper'); + var $testCredentialsWrapper = $('.test-credentials-wrapper'); $clickedButton.addClass('disabled'); - $spinner.addClass('is-active'); - $testLoginWrapper.find('.ajax-message').remove(); + CCBCoreAdmin.removeNotice(); var data = { 'action': 'test_credentials', - 'nextNonce': CCB_CORE_SETTINGS.nextNonce + 'nonce': CCB_CORE_SETTINGS.nonce }; $.post(ajaxurl, data, function(response) { - $clickedButton.removeClass('disabled'); - $spinner.removeClass('is-active'); - - if (response.success === false) { - $testLoginWrapper.append('
' + response.message + '
'); + if (true === response.success) { + CCBCoreAdmin.adminNotice( CCB_CORE_SETTINGS.translations.credentialsSuccessful ); + } else { + CCBCoreAdmin.adminNotice( CCB_CORE_SETTINGS.translations.credentialsFailed + ': ' + response.data, 'error' ); } - else if (typeof response.services !== 'undefined' && response.services.length > 0) { - $.each(response.services, function( index, value ){ - - var divClass = (value.success === true ? 'updated' : 'error'); - $testLoginWrapper.append('
' + value.label + ': ' + value.message + '
'); - - }); - - } + $clickedButton.removeClass('disabled'); }); }, - syncData : function(event) { + disableUI : function() { + var $resultsWrapper = $('.ccb-core-latest-results'); + var $syncButton = $('.sync-wrapper .button'); + CCBCoreAdmin.removeNotice(); + $syncButton.addClass('disabled'); + $resultsWrapper.empty().append(CCBCoreAdmin.spinner); + }, - event.preventDefault(); - var $clickedButton = $(this); + /** + * Helper method to insert an admin notice on the page + */ + adminNotice : function(message, type = 'info', className = '') { + var $notice = $('
'); + var $content = $('

'); + $notice.addClass('notice-' + type); + $content.text(message); + $notice.append($content); + $('.ccb_core_settings-wrapper > h2 ').after( $notice ); + }, - if ( $clickedButton.hasClass('disabled') ) { - return false; + /** + * Helper method to remove all or some notices + */ + removeNotice : function(className = '') { + var noticeSelector; + if (className.length) { + noticeSelector = '.notice.' + className; + } else { + noticeSelector = '.notice'; } - var $syncWrapper = $clickedButton.parents('.sync-wrapper'); - var $spinner = $syncWrapper.find('.spinner'); - - $clickedButton.addClass('disabled'); - $spinner.addClass('is-active'); - - var data = { - 'action': 'sync', - 'nextNonce': CCB_CORE_SETTINGS.nextNonce - }; - - $.post(ajaxurl, data, function(response) { - $syncWrapper.append('
Syncronization in progress... You can safely navigate away from this page while we work hard in the background. (It should be just a moment).
'); - ccbCoreAdmin.syncPollId = setInterval(ccbCoreAdmin.pollForActiveSync, 10000); - }); - + $(noticeSelector).remove(); } }; $(function() { - ccbCoreAdmin.initialize(); + CCBCoreAdmin.initialize(); }); diff --git a/admin/js/vendor/picker.date.js b/js/vendor/picker.date.js similarity index 100% rename from admin/js/vendor/picker.date.js rename to js/vendor/picker.date.js diff --git a/admin/js/vendor/picker.js b/js/vendor/picker.js similarity index 100% rename from admin/js/vendor/picker.js rename to js/vendor/picker.js diff --git a/admin/js/vendor/powerange.min.js b/js/vendor/powerange.min.js similarity index 100% rename from admin/js/vendor/powerange.min.js rename to js/vendor/powerange.min.js diff --git a/admin/js/vendor/switchery.min.js b/js/vendor/switchery.min.js similarity index 100% rename from admin/js/vendor/switchery.min.js rename to js/vendor/switchery.min.js diff --git a/admin/js/vendor/tipr.min.js b/js/vendor/tipr.min.js similarity index 100% rename from admin/js/vendor/tipr.min.js rename to js/vendor/tipr.min.js diff --git a/lib/Encryption/Encryption.php b/lib/Encryption/Encryption.php deleted file mode 100644 index e63de0e..0000000 --- a/lib/Encryption/Encryption.php +++ /dev/null @@ -1,163 +0,0 @@ -cipher = $cipher; - $this->mode = $mode; - $this->rounds = (int) $rounds; - } - - /** - * Decrypt the data with the provided key - * - * @param string $data The encrypted datat to decrypt - * @param string $key The key to use for decryption - * - * @returns string|false The returned string if decryption is successful - * false if it is not - */ - public function decrypt($data, $key) { - $salt = substr($data, 0, 128); - $enc = substr($data, 128, -64); - $mac = substr($data, -64); - - list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key); - - if ($mac !== hash_hmac('sha512', $enc, $macKey, true)) { - return false; - } - - $dec = mcrypt_decrypt($this->cipher, $cipherKey, $enc, $this->mode, $iv); - - $data = $this->unpad($dec); - - return $data; - } - - /** - * Encrypt the supplied data using the supplied key - * - * @param string $data The data to encrypt - * @param string $key The key to encrypt with - * - * @returns string The encrypted data - */ - public function encrypt($data, $key) { - $salt = mcrypt_create_iv(128, MCRYPT_RAND); - list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key); - - $data = $this->pad($data); - - $enc = mcrypt_encrypt($this->cipher, $cipherKey, $data, $this->mode, $iv); - - $mac = hash_hmac('sha512', $enc, $macKey, true); - return $salt . $enc . $mac; - } - - /** - * Generates a set of keys given a random salt and a master key - * - * @param string $salt A random string to change the keys each encryption - * @param string $key The supplied key to encrypt with - * - * @returns array An array of keys (a cipher key, a mac key, and a IV) - */ - protected function getKeys($salt, $key) { - $ivSize = mcrypt_get_iv_size($this->cipher, $this->mode); - $keySize = mcrypt_get_key_size($this->cipher, $this->mode); - $length = 2 * $keySize + $ivSize; - - $key = $this->pbkdf2('sha512', $key, $salt, $this->rounds, $length); - - $cipherKey = substr($key, 0, $keySize); - $macKey = substr($key, $keySize, $keySize); - $iv = substr($key, 2 * $keySize); - return array($cipherKey, $macKey, $iv); - } - - /** - * Stretch the key using the PBKDF2 algorithm - * - * @see http://en.wikipedia.org/wiki/PBKDF2 - * - * @param string $algo The algorithm to use - * @param string $key The key to stretch - * @param string $salt A random salt - * @param int $rounds The number of rounds to derive - * @param int $length The length of the output key - * - * @returns string The derived key. - */ - protected function pbkdf2($algo, $key, $salt, $rounds, $length) { - $size = strlen(hash($algo, '', true)); - $len = ceil($length / $size); - $result = ''; - for ($i = 1; $i <= $len; $i++) { - $tmp = hash_hmac($algo, $salt . pack('N', $i), $key, true); - $res = $tmp; - for ($j = 1; $j < $rounds; $j++) { - $tmp = hash_hmac($algo, $tmp, $key, true); - $res ^= $tmp; - } - $result .= $res; - } - return substr($result, 0, $length); - } - - protected function pad($data) { - $length = mcrypt_get_block_size($this->cipher, $this->mode); - $padAmount = $length - strlen($data) % $length; - if ($padAmount == 0) { - $padAmount = $length; - } - return $data . str_repeat(chr($padAmount), $padAmount); - } - - protected function unpad($data) { - $length = mcrypt_get_block_size($this->cipher, $this->mode); - $last = ord($data[strlen($data) - 1]); - if ($last > $length) return false; - if (substr($data, -1 * $last) !== str_repeat(chr($last), $last)) { - return false; - } - return substr($data, 0, -1 * $last); - } -} -?> diff --git a/lib/class-ccb-core-vendor-encryption.php b/lib/class-ccb-core-vendor-encryption.php new file mode 100644 index 0000000..c86e6fc --- /dev/null +++ b/lib/class-ccb-core-vendor-encryption.php @@ -0,0 +1,194 @@ +cipher = $cipher; + $this->mode = $mode; + $this->rounds = (int) $rounds; + } + + /** + * Decrypt the data with the provided key + * + * @param string $data The encrypted datat to decrypt. + * @param string $key The key to use for decryption. + * + * @returns string|false The returned string if decryption is successful false if it is not + */ + public function decrypt( $data, $key ) { + $salt = substr( $data, 0, 128 ); + $enc = substr( $data, 128, -64 ); + $mac = substr( $data, -64 ); + + list ( $cipher_key, $mac_key, $iv ) = $this->get_keys( $salt, $key ); + + if ( hash_hmac( 'sha512', $enc, $mac_key, true ) !== $mac ) { + return false; + } + + $dec = mcrypt_decrypt( $this->cipher, $cipher_key, $enc, $this->mode, $iv ); + + $data = $this->unpad( $dec ); + + return $data; + } + + /** + * Encrypt the supplied data using the supplied key + * + * @param string $data The data to encrypt. + * @param string $key The key to encrypt with. + * + * @returns string The encrypted data + */ + public function encrypt( $data, $key ) { + $salt = mcrypt_create_iv( 128, MCRYPT_RAND ); + list ( $cipher_key, $mac_key, $iv ) = $this->get_keys( $salt, $key ); + + $data = $this->pad( $data ); + + $enc = mcrypt_encrypt( $this->cipher, $cipher_key, $data, $this->mode, $iv ); + + $mac = hash_hmac( 'sha512', $enc, $mac_key, true ); + return $salt . $enc . $mac; + } + + /** + * Generates a set of keys given a random salt and a master key + * + * @param string $salt A random string to change the keys each encryption. + * @param string $key The supplied key to encrypt with. + * + * @returns array An array of keys ( a cipher key, a mac key, and a IV ) + */ + protected function get_keys( $salt, $key ) { + if ( function_exists( 'mcrypt_get_iv_size' ) ) { + $iv_size = mcrypt_get_iv_size( $this->cipher, $this->mode ); + $key_size = mcrypt_get_key_size( $this->cipher, $this->mode ); + $length = 2 * $key_size + $iv_size; + + $key = $this->pbkdf2( 'sha512', $key, $salt, $this->rounds, $length ); + + $cipher_key = substr( $key, 0, $key_size ); + $mac_key = substr( $key, $key_size, $key_size ); + $iv = substr( $key, 2 * $key_size ); + return [ $cipher_key, $mac_key, $iv ]; + } else { + return false; + } + } + + /** + * Stretch the key using the PBKDF2 algorithm + * + * @see http://en.wikipedia.org/wiki/PBKDF2 + * + * @param string $algo The algorithm to use. + * @param string $key The key to stretch. + * @param string $salt A random salt. + * @param int $rounds The number of rounds to derive. + * @param int $length The length of the output key. + * + * @returns string The derived key. + */ + protected function pbkdf2( $algo, $key, $salt, $rounds, $length ) { + $size = strlen( hash( $algo, '', true ) ); + $len = ceil( $length / $size ); + $result = ''; + for ( $i = 1; $i <= $len; $i++ ) { + $tmp = hash_hmac( $algo, $salt . pack( 'N', $i ), $key, true ); + $res = $tmp; + for ( $j = 1; $j < $rounds; $j++ ) { + $tmp = hash_hmac( $algo, $tmp, $key, true ); + $res ^= $tmp; + } + $result .= $res; + } + return substr( $result, 0, $length ); + } + + /** + * Add padding based on the block size + * + * @param string $data The data to encrypt. + * @return string + */ + protected function pad( $data ) { + $length = mcrypt_get_block_size( $this->cipher, $this->mode ); + $pad_amount = $length - strlen( $data ) % $length; + if ( 0 === $pad_amount ) { + $pad_amount = $length; + } + return $data . str_repeat( chr( $pad_amount ), $pad_amount ); + } + + /** + * Remove padding based on the block size + * + * @param string $data The data to decrypt. + * @return string + */ + protected function unpad( $data ) { + $length = mcrypt_get_block_size( $this->cipher, $this->mode ); + $last = ord( $data[ strlen( $data ) - 1 ] ); + if ( $last > $length ) { + return false; + } + if ( substr( $data, -1 * $last ) !== str_repeat( chr( $last ), $last ) ) { + return false; + } + return substr( $data, 0, -1 * $last ); + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..6167e45 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,48 @@ + + + Generally-applicable sniffs for WordPress plugins + + + + + + + + + + + + + + + warning + + + + warning + + + + + warning + + + warning + + + warning + + + warning + + + + + . + + + + + */tests/* + */lib/* + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..44f0fdb --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,14 @@ + + + + ./tests/ + + + diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..83eacf7 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,33 @@ +utils = new Test_Utils(); + $this->synchronizer = CCB_Core_Synchronizer::instance(); + $this->synchronizer->map = $this->utils->synchronizer_get_calendar_map(); + } + + /** + * Test the insert of events when the database is empty. + */ + public function test_update_content_insert_events() { + // Testing the $result is a successful insert. + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'public_calendar_listing_sample.xml' ), + $this->synchronizer->map['ccb_core_calendar'], + 'ccb_core_calendar' + ); + + $expected_result = [ + 'success' => true, + 'insert_update' => [ + 'success' => true, + 'processed' => 10, + 'message' => '', + ], + 'delete' => [ + 'success' => true, + 'processed' => 0, + ], + ]; + + $this->assertEqualSetsWithIndex( $expected_result, $result ); + } + + /** + * Test when some events have been updated in CCB. + */ + public function test_update_content_update_events() { + // Initial insert of events + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'public_calendar_listing_sample.xml' ), + $this->synchronizer->map['ccb_core_calendar'], + 'ccb_core_calendar' + ); + + // Testing whether updated events from CCB get updated in WordPress. + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'public_calendar_listing_some_updated_sample.xml' ), + $this->synchronizer->map['ccb_core_calendar'], + 'ccb_core_calendar' + ); + + // Events do not have a unique identifier from CCB, so updates + // are actually 3 inserts and 3 deletes. + $expected_result = [ + 'success' => true, + 'insert_update' => [ + 'success' => true, + 'processed' => 3, + 'message' => '', + ], + 'delete' => [ + 'success' => true, + 'processed' => 3, + 'message' => '', + ], + ]; + + $this->assertEqualSetsWithIndex( $expected_result, $result ); + } + + /** + * Test when some events have been deleted in CCB. + */ + public function test_update_content_deleted_events() { + // Initial insert of events + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'public_calendar_listing_sample.xml' ), + $this->synchronizer->map['ccb_core_calendar'], + 'ccb_core_calendar' + ); + + // Testing whether deleted events from CCB get deleted in WordPress. + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'public_calendar_listing_some_deleted_sample.xml' ), + $this->synchronizer->map['ccb_core_calendar'], + 'ccb_core_calendar' + ); + + $expected_result = [ + 'success' => true, + 'insert_update' => [ + 'success' => true, + 'processed' => 0, + ], + 'delete' => [ + 'success' => true, + 'processed' => 3, + 'message' => '', + ], + ]; + + $this->assertEqualSetsWithIndex( $expected_result, $result ); + } + + /** + * Test when some events are new in CCB. + */ + public function test_update_content_new_events() { + // Initial insert of events + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'public_calendar_listing_sample.xml' ), + $this->synchronizer->map['ccb_core_calendar'], + 'ccb_core_calendar' + ); + + // Testing whether new events from CCB get inserted in WordPress. + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'public_calendar_listing_some_new_sample.xml' ), + $this->synchronizer->map['ccb_core_calendar'], + 'ccb_core_calendar' + ); + + $expected_result = [ + 'success' => true, + 'insert_update' => [ + 'success' => true, + 'processed' => 3, + 'message' => '', + ], + 'delete' => [ + 'success' => true, + 'processed' => 0, + ], + ]; + + $this->assertEqualSetsWithIndex( $expected_result, $result ); + } + + public function tearDown() { + parent::tearDown(); + } + +} diff --git a/tests/test-synchronizer-groups.php b/tests/test-synchronizer-groups.php new file mode 100644 index 0000000..73d8e3c --- /dev/null +++ b/tests/test-synchronizer-groups.php @@ -0,0 +1,158 @@ +utils = new Test_Utils(); + $this->synchronizer = CCB_Core_Synchronizer::instance(); + $this->synchronizer->map = $this->utils->synchronizer_get_groups_map(); + } + + /** + * Test the insert of groups when the database is empty. + */ + public function test_update_content_insert_groups() { + // Testing the $result is a successful insert. + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'group_profiles_sample.xml' ), + $this->synchronizer->map['ccb_core_group'], + 'ccb_core_group' + ); + + $expected_result = [ + 'success' => true, + 'insert_update' => [ + 'success' => true, + 'processed' => 4, + 'message' => '', + ], + 'delete' => [ + 'success' => true, + 'processed' => 0, + ], + ]; + + $this->assertEqualSetsWithIndex( $expected_result, $result ); + } + + /** + * Test when some groups have been updated in CCB. + */ + public function test_update_content_update_groups() { + // Insert some posts. + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'group_profiles_sample.xml' ), + $this->synchronizer->map['ccb_core_group'], + 'ccb_core_group' + ); + + // Testing the $result has some successful updates. + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'group_profiles_some_updated_sample.xml' ), + $this->synchronizer->map['ccb_core_group'], + 'ccb_core_group' + ); + + $expected_result = [ + 'success' => true, + 'insert_update' => [ + 'success' => true, + 'processed' => 2, + 'message' => '', + ], + 'delete' => [ + 'success' => true, + 'processed' => 0, + ], + ]; + + $this->assertEqualSetsWithIndex( $expected_result, $result ); + + } + + /** + * Test when some groups have been unlisted in CCB. + */ + public function test_update_content_unlisted_groups() { + // Insert some posts. + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'group_profiles_sample.xml' ), + $this->synchronizer->map['ccb_core_group'], + 'ccb_core_group' + ); + + // Testing the $result has deleted some unlisted posts. + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'group_profiles_some_unlisted_sample.xml' ), + $this->synchronizer->map['ccb_core_group'], + 'ccb_core_group' + ); + + $expected_result = [ + 'success' => true, + 'insert_update' => [ + 'success' => true, + 'processed' => 0, + ], + 'delete' => [ + 'success' => true, + 'processed' => 2, + 'message' => '', + ], + ]; + + $this->assertEqualSetsWithIndex( $expected_result, $result ); + + } + + /** + * Test when some groups have been inactivated in CCB. + */ + public function test_update_content_inactivated_groups() { + // Insert some posts. + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'group_profiles_sample.xml' ), + $this->synchronizer->map['ccb_core_group'], + 'ccb_core_group' + ); + + // Testing the $result has deleted some unlisted posts. + $result = $this->synchronizer->update_content( + $this->utils->api_mock_response( 'group_profiles_some_inactivated_sample.xml' ), + $this->synchronizer->map['ccb_core_group'], + 'ccb_core_group' + ); + + $expected_result = [ + 'success' => true, + 'insert_update' => [ + 'success' => true, + 'processed' => 0, + ], + 'delete' => [ + 'success' => true, + 'processed' => 2, + 'message' => '', + ], + ]; + + $this->assertEqualSetsWithIndex( $expected_result, $result ); + + } + + public function tearDown() { + parent::tearDown(); + } + +} diff --git a/tests/test-utils.php b/tests/test-utils.php new file mode 100644 index 0000000..fb23e38 --- /dev/null +++ b/tests/test-utils.php @@ -0,0 +1,104 @@ +plugin_path = plugin_dir_path( __FILE__ ); + } + + public function synchronizer_get_groups_map( $include_image_link = false ) { + return [ + 'ccb_core_group' => [ + 'service' => 'group_profiles', + 'data' => [ + 'include_participants' => false, + 'include_image_link' => $include_image_link, + ], + 'nodes' => [ 'groups', 'group' ], + 'fields' => [ + 'name' => 'post_title', + 'description' => 'post_content', + 'main_leader' => 'post_meta', + 'calendar_feed' => 'post_meta', + 'current_members' => 'post_meta', + 'group_capacity' => 'post_meta', + 'addresses' => 'post_meta', + ], + 'taxonomies' => [ + 'hierarchical' => [ + 'ccb_core_group_area' => 'area', + 'ccb_core_group_day' => 'meeting_day', + 'ccb_core_group_department' => 'department', + 'ccb_core_group_time' => 'meeting_time', + 'ccb_core_group_type' => 'group_type', + ], + 'nonhierarchical' => [ + 'ccb_core_group_tag' => [ 'childcare_provided' => 'Childcare Provided' ], + ], + ], + ], + ]; + } + + public function synchronizer_get_calendar_map( $date_start = '', $date_end = '' ) { + $date_start = ! empty( $date_start ) ? $date_start : date( 'Y-m-d', strtotime( '1 weeks ago' ) ); + $date_end = ! empty( $date_end ) ? $date_end : date( 'Y-m-d', strtotime( '+2 weeks' ) ); + return [ + 'ccb_core_calendar' => [ + 'service' => 'public_calendar_listing', + 'data' => [ + 'date_start' => $date_start, + 'date_end' => $date_end, + ], + 'nodes' => [ 'items', 'item' ], + 'fields' => [ + 'event_name' => 'post_title', + 'event_description' => 'post_content', + 'date' => 'post_meta', + 'start_time' => 'post_meta', + 'end_time' => 'post_meta', + 'event_duration' => 'post_meta', + 'location' => 'post_meta', + ], + 'taxonomies' => [ + 'hierarchical' => [ + 'ccb_core_calendar_event_type' => 'event_type', + 'ccb_core_calendar_group_name' => 'group_name', + 'ccb_core_calendar_grouping_name' => 'grouping_name', + ], + ], + ], + ]; + + } + + public function api_mock_response( $xml_file, $http_success = true ) { + $filepath = $this->plugin_path . 'xml/' . $xml_file; + $code = $http_success ? 200 : 500; + $status = ( false === strpos( $xml_file, 'fail' ) && $http_success ) ? 'SUCCESS' : 'ERROR'; + $message = ( false === strpos( $xml_file, 'fail' ) && $http_success ) ? '' : 'Generic unit test error message.'; + + $result = [ + 'code' => $code, + 'status' => $status, + 'message' => $message, + ]; + + if ( file_exists( $filepath ) ) { + $result['xml'] = file_get_contents( $filepath ); + libxml_use_internal_errors( true ); + $result['body'] = simplexml_load_string( $result['xml'] ); + } + return $result; + } +} diff --git a/tests/xml/group_profiles_sample.xml b/tests/xml/group_profiles_sample.xml new file mode 100644 index 0000000..591f860 --- /dev/null +++ b/tests/xml/group_profiles_sample.xml @@ -0,0 +1,405 @@ + + + + + + + + + + + group_profiles + execute + public + + + Test Group One + This is a public_search_listed false example and should not be imported + + Campus One + + Jean Luc + Picard + Jean Luc Picard + jeanluc@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 3300 + Unlimited + +
+ + Meeting Address +Street + City + + + -112.076814 + 33.447549 + Meeting Address +Street + City +
+
+ + + false + Announcement Only + Open to All + true + + true + false + false + Jean Luc Picard + Jean Luc Picard + 2016-06-17 04:02:53 + 2017-04-30 16:36:38 +
+ + Test Group Two + This is an inactive example and should not be imported + + Campus One + + Jean Luc + Picard + Jean Luc Picard + jeanluc@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 3300 + Unlimited + +
+ Ten Forward + Deck 10, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 10, Forward + Enterprise, CO 80129 +
+
+ + + false + Announcement Only + Invitation or Request Required + false + + true + true + true + Jean Luc Picard + Jean Luc Picard + 2016-07-17 04:02:53 + 2017-05-30 16:36:38 +
+ + Test Group Three + This is a group with all properties set + https://i.imgur.com/penLEu9.jpg + Campus Two + + Geordi + La Forge + Geordi La Forge + geordi@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + Department One + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 100 + Unlimited + +
+ Ten Forward + Deck 10, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 10, Forward + Enterprise, CO 80129 +
+
+ Weekdays + Morning + true + Members Interact + Invitation or Request Required + false + + true + true + false + Jean Luc Picard + Jean Luc Picard + 2016-07-17 04:02:53 + 2017-05-30 16:36:38 +
+ + Test Group Four + This is the same as Test Group Three but different name and ID + https://i.imgur.com/penLEu9.jpg + Campus Two + + Geordi + La Forge + Geordi La Forge + geordi@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + Department One + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 100 + Unlimited + +
+ Ten Forward + Deck 10, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 10, Forward + Enterprise, CO 80129 +
+
+ Weekdays + Morning + true + Members Interact + Invitation or Request Required + false + + true + true + false + Jean Luc Picard + Jean Luc Picard + 2016-07-17 04:02:53 + 2017-05-30 16:36:38 +
+ + Test Group Five + This has all properties but different than Test Group Three and Four + https://i.imgur.com/MZ9itik.png + Campus Three + + Worf + Rozhenko + Worf Rozhenko + worf@enterprise.com + + 123-456-7890 + + + + Beverly + Crusher + Beverly Crusher + beverly@enterprise.com + + 123-123-1234 + + + + Deanna + Troi + Deanna Troi + deanna@enterprise.com + + 246-810-1214 + + + Group Type Two + Department Two + Area Two + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 10 + Unlimited + +
+ Main Bridge + Deck 1, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 1, Forward + Enterprise, CO 80129 +
+
+ Sundays + Nights + false + Members Interact + Invitation or Request Required + true + + true + true + false + Jean Luc Picard + Jean Luc Picard + 2016-08-17 04:02:53 + 2017-08-30 16:36:38 +
+ + Test Group Six + Similar to Test Group Five but missing some properties + https://i.imgur.com/MZ9itik.png + Campus Three + + Worf + Rozhenko + Worf Rozhenko + worf@enterprise.com + + 123-456-7890 + + + + + Group Type Two + + + + + 10 + Unlimited + +
+ Main Bridge + Deck 1, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 1, Forward + Enterprise, CO 80129 +
+
+ Mondays + + true + Members Interact + Invitation or Request Required + true + + true + true + false + Jean Luc Picard + Jean Luc Picard + 2016-08-17 04:02:53 + 2017-08-30 16:36:38 +
+
+
+
diff --git a/tests/xml/group_profiles_some_inactivated_sample.xml b/tests/xml/group_profiles_some_inactivated_sample.xml new file mode 100644 index 0000000..750615f --- /dev/null +++ b/tests/xml/group_profiles_some_inactivated_sample.xml @@ -0,0 +1,405 @@ + + + + + + + + + + + group_profiles + execute + public + + + Test Group One + This is a public_search_listed false example and should not be imported + + Campus One + + Jean Luc + Picard + Jean Luc Picard + jeanluc@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 3300 + Unlimited + +
+ + Meeting Address +Street + City + + + -112.076814 + 33.447549 + Meeting Address +Street + City +
+
+ + + false + Announcement Only + Open to All + true + + true + false + false + Jean Luc Picard + Jean Luc Picard + 2016-06-17 04:02:53 + 2017-04-30 16:36:38 +
+ + Test Group Two + This is an inactive example and should not be imported + + Campus One + + Jean Luc + Picard + Jean Luc Picard + jeanluc@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 3300 + Unlimited + +
+ Ten Forward + Deck 10, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 10, Forward + Enterprise, CO 80129 +
+
+ + + false + Announcement Only + Invitation or Request Required + false + + true + true + true + Jean Luc Picard + Jean Luc Picard + 2016-07-17 04:02:53 + 2017-05-30 16:36:38 +
+ + Test Group Three + This is a group with all properties set + https://i.imgur.com/penLEu9.jpg + Campus Two + + Geordi + La Forge + Geordi La Forge + geordi@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + Department One + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 100 + Unlimited + +
+ Ten Forward + Deck 10, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 10, Forward + Enterprise, CO 80129 +
+
+ Weekdays + Morning + true + Members Interact + Invitation or Request Required + false + + true + true + false + Jean Luc Picard + Jean Luc Picard + 2016-07-17 04:02:53 + 2017-05-30 16:36:38 +
+ + Test Group Four + This is the same as Test Group Three but different name and ID + https://i.imgur.com/penLEu9.jpg + Campus Two + + Geordi + La Forge + Geordi La Forge + geordi@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + Department One + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 100 + Unlimited + +
+ Ten Forward + Deck 10, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 10, Forward + Enterprise, CO 80129 +
+
+ Weekdays + Morning + true + Members Interact + Invitation or Request Required + false + + true + true + false + Jean Luc Picard + Jean Luc Picard + 2016-07-17 04:02:53 + 2017-05-30 16:36:38 +
+ + Test Group Five INACTIVATED + This has all properties but different than Test Group Three and Four + https://i.imgur.com/MZ9itik.png + Campus Three + + Worf + Rozhenko + Worf Rozhenko + worf@enterprise.com + + 123-456-7890 + + + + Beverly + Crusher + Beverly Crusher + beverly@enterprise.com + + 123-123-1234 + + + + Deanna + Troi + Deanna Troi + deanna@enterprise.com + + 246-810-1214 + + + Group Type Two + Department Two + Area Two + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 10 + Unlimited + +
+ Main Bridge + Deck 1, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 1, Forward + Enterprise, CO 80129 +
+
+ Sundays + Nights + false + Members Interact + Invitation or Request Required + true + + true + true + true + Jean Luc Picard + Jean Luc Picard + 2016-08-17 04:02:53 + 2017-09-15 16:36:38 +
+ + Test Group Six INACTIVATED + Similar to Test Group Five but missing some properties + https://i.imgur.com/MZ9itik.png + Campus Three + + Worf + Rozhenko + Worf Rozhenko + worf@enterprise.com + + 123-456-7890 + + + + + Group Type Two + + + + + 10 + Unlimited + +
+ Main Bridge + Deck 1, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 1, Forward + Enterprise, CO 80129 +
+
+ Mondays + + true + Members Interact + Invitation or Request Required + true + + true + true + true + Jean Luc Picard + Jean Luc Picard + 2016-08-17 04:02:53 + 2017-09-15 16:36:38 +
+
+
+
diff --git a/tests/xml/group_profiles_some_unlisted_sample.xml b/tests/xml/group_profiles_some_unlisted_sample.xml new file mode 100644 index 0000000..6d33456 --- /dev/null +++ b/tests/xml/group_profiles_some_unlisted_sample.xml @@ -0,0 +1,405 @@ + + + + + + + + + + + group_profiles + execute + public + + + Test Group One + This is a public_search_listed false example and should not be imported + + Campus One + + Jean Luc + Picard + Jean Luc Picard + jeanluc@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 3300 + Unlimited + +
+ + Meeting Address +Street + City + + + -112.076814 + 33.447549 + Meeting Address +Street + City +
+
+ + + false + Announcement Only + Open to All + true + + true + false + false + Jean Luc Picard + Jean Luc Picard + 2016-06-17 04:02:53 + 2017-04-30 16:36:38 +
+ + Test Group Two + This is an inactive example and should not be imported + + Campus One + + Jean Luc + Picard + Jean Luc Picard + jeanluc@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 3300 + Unlimited + +
+ Ten Forward + Deck 10, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 10, Forward + Enterprise, CO 80129 +
+
+ + + false + Announcement Only + Invitation or Request Required + false + + true + true + true + Jean Luc Picard + Jean Luc Picard + 2016-07-17 04:02:53 + 2017-05-30 16:36:38 +
+ + Test Group Three + This is a group with all properties set + https://i.imgur.com/penLEu9.jpg + Campus Two + + Geordi + La Forge + Geordi La Forge + geordi@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + Department One + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 100 + Unlimited + +
+ Ten Forward + Deck 10, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 10, Forward + Enterprise, CO 80129 +
+
+ Weekdays + Morning + true + Members Interact + Invitation or Request Required + false + + true + true + false + Jean Luc Picard + Jean Luc Picard + 2016-07-17 04:02:53 + 2017-05-30 16:36:38 +
+ + Test Group Four + This is the same as Test Group Three but different name and ID + https://i.imgur.com/penLEu9.jpg + Campus Two + + Geordi + La Forge + Geordi La Forge + geordi@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + Department One + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 100 + Unlimited + +
+ Ten Forward + Deck 10, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 10, Forward + Enterprise, CO 80129 +
+
+ Weekdays + Morning + true + Members Interact + Invitation or Request Required + false + + true + true + false + Jean Luc Picard + Jean Luc Picard + 2016-07-17 04:02:53 + 2017-05-30 16:36:38 +
+ + Test Group Five UNLISTED + This has all properties but different than Test Group Three and Four + https://i.imgur.com/MZ9itik.png + Campus Three + + Worf + Rozhenko + Worf Rozhenko + worf@enterprise.com + + 123-456-7890 + + + + Beverly + Crusher + Beverly Crusher + beverly@enterprise.com + + 123-123-1234 + + + + Deanna + Troi + Deanna Troi + deanna@enterprise.com + + 246-810-1214 + + + Group Type Two + Department Two + Area Two + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 10 + Unlimited + +
+ Main Bridge + Deck 1, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 1, Forward + Enterprise, CO 80129 +
+
+ Sundays + Nights + false + Members Interact + Invitation or Request Required + true + + true + false + false + Jean Luc Picard + Jean Luc Picard + 2016-08-17 04:02:53 + 2017-09-15 16:36:38 +
+ + Test Group Six UNLISTED + Similar to Test Group Five but missing some properties + https://i.imgur.com/MZ9itik.png + Campus Three + + Worf + Rozhenko + Worf Rozhenko + worf@enterprise.com + + 123-456-7890 + + + + + Group Type Two + + + + + 10 + Unlimited + +
+ Main Bridge + Deck 1, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 1, Forward + Enterprise, CO 80129 +
+
+ Mondays + + true + Members Interact + Invitation or Request Required + true + + true + false + false + Jean Luc Picard + Jean Luc Picard + 2016-08-17 04:02:53 + 2017-08-30 16:36:38 +
+
+
+
diff --git a/tests/xml/group_profiles_some_updated_sample.xml b/tests/xml/group_profiles_some_updated_sample.xml new file mode 100644 index 0000000..76271a7 --- /dev/null +++ b/tests/xml/group_profiles_some_updated_sample.xml @@ -0,0 +1,405 @@ + + + + + + + + + + + group_profiles + execute + public + + + Test Group One UPDATED + This is a public_search_listed false example and should not be imported + + Campus One + + Jean Luc + Picard + Jean Luc Picard + jeanluc@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 3300 + Unlimited + +
+ + Meeting Address +Street + City + + + -112.076814 + 33.447549 + Meeting Address +Street + City +
+
+ + + false + Announcement Only + Open to All + true + + true + false + false + Jean Luc Picard + Jean Luc Picard + 2016-06-17 04:02:53 + 2017-05-15 16:36:38 +
+ + Test Group Two + This is an inactive example and should not be imported + + Campus One + + Jean Luc + Picard + Jean Luc Picard + jeanluc@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 3300 + Unlimited + +
+ Ten Forward + Deck 10, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 10, Forward + Enterprise, CO 80129 +
+
+ + + false + Announcement Only + Invitation or Request Required + false + + true + true + true + Jean Luc Picard + Jean Luc Picard + 2016-07-17 04:02:53 + 2017-05-30 16:36:38 +
+ + Test Group Three UPDATED + This is a group with all properties set + https://i.imgur.com/penLEu9.jpg + Campus Two + + Geordi + La Forge + Geordi La Forge + geordi@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + Department One + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 100 + Unlimited + +
+ Ten Forward + Deck 10, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 10, Forward + Enterprise, CO 80129 +
+
+ Weekdays + Morning + true + Members Interact + Invitation or Request Required + false + + true + true + false + Jean Luc Picard + Jean Luc Picard + 2016-07-17 04:02:53 + 2017-06-15 16:36:38 +
+ + Test Group Four UPDATED + This is the same as Test Group Three but different name and ID + https://i.imgur.com/penLEu9.jpg + Campus Two + + Geordi + La Forge + Geordi La Forge + geordi@enterprise.com + + + + + + William + Riker + William Riker + + + + + + + Data + + Data + + + + + + Group Type One + Department One + Area One + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 100 + Unlimited + +
+ Ten Forward + Deck 10, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 10, Forward + Enterprise, CO 80129 +
+
+ Weekdays + Morning + true + Members Interact + Invitation or Request Required + false + + true + true + false + Jean Luc Picard + Jean Luc Picard + 2016-07-17 04:02:53 + 2017-06-15 16:36:38 +
+ + Test Group Five + This has all properties but different than Test Group Three and Four + https://i.imgur.com/MZ9itik.png + Campus Three + + Worf + Rozhenko + Worf Rozhenko + worf@enterprise.com + + 123-456-7890 + + + + Beverly + Crusher + Beverly Crusher + beverly@enterprise.com + + 123-123-1234 + + + + Deanna + Troi + Deanna Troi + deanna@enterprise.com + + 246-810-1214 + + + Group Type Two + Department Two + Area Two + webcal://starfleet.ccbchurch.com/group_calendar.ics?id=1&tk=57395EF2EECB102CAABD00B0D0E1CF3B + + 10 + Unlimited + +
+ Main Bridge + Deck 1, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 1, Forward + Enterprise, CO 80129 +
+
+ Sundays + Nights + false + Members Interact + Invitation or Request Required + true + + true + true + false + Jean Luc Picard + Jean Luc Picard + 2016-08-17 04:02:53 + 2017-08-30 16:36:38 +
+ + Test Group Six + Similar to Test Group Five but missing some properties + https://i.imgur.com/MZ9itik.png + Campus Three + + Worf + Rozhenko + Worf Rozhenko + worf@enterprise.com + + 123-456-7890 + + + + + Group Type Two + + + + + 10 + Unlimited + +
+ Main Bridge + Deck 1, Forward + Enterprise + C0 + 80129 + -117.322031 + 33.229150 + Deck 1, Forward + Enterprise, CO 80129 +
+
+ Mondays + + true + Members Interact + Invitation or Request Required + true + + true + true + false + Jean Luc Picard + Jean Luc Picard + 2016-08-17 04:02:53 + 2017-08-30 16:36:38 +
+
+
+
diff --git a/tests/xml/public_calendar_listing_sample.xml b/tests/xml/public_calendar_listing_sample.xml new file mode 100644 index 0000000..88b4123 --- /dev/null +++ b/tests/xml/public_calendar_listing_sample.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + 2018-01-03 + Event One + Event Description One + 08:00:00 + 08:15:00 + 15 + Open To All + Room One + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-03 + Event Two + Event Description Two + 09:00:00 + 10:00:00 + 60 + Open To All + + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-03 + Event Three + + 15:00:00 + 17:45:00 + 160 + Open To All + Room Three + Group Two + Interest + Department Two + William Riker + 123-123-1234 + william@enterprise.com + + + 2018-01-03 + Event Four + Event Description Four + 19:00:00 + 21:00:00 + 120 + Open To All + Room Four + Group Three + Singles + Singles Department + Data + + + + + 2018-01-04 + Event One + Event Description One + 08:00:00 + 08:15:00 + 15 + Open To All + Room One + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-04 + Event Two + Event Description Two + 09:00:00 + 10:00:00 + 60 + Open To All + + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-04 + Event Five + Weekly Event Five + 19:00:00 + 21:00:00 + 120 + Open To All + Main Bridge + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-04 + Event Four + Event Description Four + 19:00:00 + 21:00:00 + 120 + Open To All + Room Four + Group Three + Singles + Singles Department + Data + + + + + 2018-01-05 + Event One + Event Description One + 08:00:00 + 08:15:00 + 15 + Open To All + Room One + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-05 + Event Two + Event Description Two + 09:00:00 + 10:00:00 + 60 + Open To All + + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + + + diff --git a/tests/xml/public_calendar_listing_some_deleted_sample.xml b/tests/xml/public_calendar_listing_some_deleted_sample.xml new file mode 100644 index 0000000..9c8737b --- /dev/null +++ b/tests/xml/public_calendar_listing_some_deleted_sample.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + 2018-01-03 + Event One + Event Description One + 08:00:00 + 08:15:00 + 15 + Open To All + Room One + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-03 + Event Three + + 15:00:00 + 17:45:00 + 160 + Open To All + Room Three + Group Two + Interest + Department Two + William Riker + 123-123-1234 + william@enterprise.com + + + 2018-01-03 + Event Four + Event Description Four + 19:00:00 + 21:00:00 + 120 + Open To All + Room Four + Group Three + Singles + Singles Department + Data + + + + + 2018-01-04 + Event One + Event Description One + 08:00:00 + 08:15:00 + 15 + Open To All + Room One + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-04 + Event Five + Weekly Event Five + 19:00:00 + 21:00:00 + 120 + Open To All + Main Bridge + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-04 + Event Four + Event Description Four + 19:00:00 + 21:00:00 + 120 + Open To All + Room Four + Group Three + Singles + Singles Department + Data + + + + + 2018-01-05 + Event One + Event Description One + 08:00:00 + 08:15:00 + 15 + Open To All + Room One + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + + + diff --git a/tests/xml/public_calendar_listing_some_new_sample.xml b/tests/xml/public_calendar_listing_some_new_sample.xml new file mode 100644 index 0000000..78e3fe8 --- /dev/null +++ b/tests/xml/public_calendar_listing_some_new_sample.xml @@ -0,0 +1,223 @@ + + + + + + + + + + + + + 2018-01-03 + Event One + Event Description One + 08:00:00 + 08:15:00 + 15 + Open To All + Room One + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-03 + Event Two + Event Description Two + 09:00:00 + 10:00:00 + 60 + Open To All + + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-03 + Event NEW + New Event + 10:00:00 + 11:00:00 + 60 + Open To All + + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-03 + Event Three + + 15:00:00 + 17:45:00 + 160 + Open To All + Room Three + Group Two + Interest + Department Two + William Riker + 123-123-1234 + william@enterprise.com + + + 2018-01-03 + Event Four + Event Description Four + 19:00:00 + 21:00:00 + 120 + Open To All + Room Four + Group Three + Singles + Singles Department + Data + + + + + 2018-01-04 + Event One + Event Description One + 08:00:00 + 08:15:00 + 15 + Open To All + Room One + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-04 + Event Two + Event Description Two + 09:00:00 + 10:00:00 + 60 + Open To All + + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-04 + Event NEW + New Event + 10:00:00 + 11:00:00 + 60 + Open To All + + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-04 + Event Five + Weekly Event Five + 19:00:00 + 21:00:00 + 120 + Open To All + Main Bridge + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-04 + Event Four + Event Description Four + 19:00:00 + 21:00:00 + 120 + Open To All + Room Four + Group Three + Singles + Singles Department + Data + + + + + 2018-01-05 + Event One + Event Description One + 08:00:00 + 08:15:00 + 15 + Open To All + Room One + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-05 + Event Two + Event Description Two + 09:00:00 + 10:00:00 + 60 + Open To All + + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-05 + Event NEW + New Event + 10:00:00 + 11:00:00 + 60 + Open To All + + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + + + diff --git a/tests/xml/public_calendar_listing_some_updated_sample.xml b/tests/xml/public_calendar_listing_some_updated_sample.xml new file mode 100644 index 0000000..d9f740e --- /dev/null +++ b/tests/xml/public_calendar_listing_some_updated_sample.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + 2018-01-03 + Event One UPDATED + Event Description One UPDATED + 08:00:00 + 08:15:00 + 15 + Open To All + Room One + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-03 + Event Two + Event Description Two + 09:00:00 + 10:00:00 + 60 + Open To All + + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-03 + Event Three + + 15:00:00 + 17:45:00 + 160 + Open To All + Room Three + Group Two + Interest + Department Two + William Riker + 123-123-1234 + william@enterprise.com + + + 2018-01-03 + Event Four + Event Description Four + 19:00:00 + 21:00:00 + 120 + Open To All + Room Four + Group Three + Singles + Singles Department + Data + + + + + 2018-01-04 + Event One UPDATED + Event Description One UPDATED + 08:00:00 + 08:15:00 + 15 + Open To All + Room One + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-04 + Event Two + Event Description Two + 09:00:00 + 10:00:00 + 60 + Open To All + + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-04 + Event Five + Weekly Event Five + 19:00:00 + 21:00:00 + 120 + Open To All + Main Bridge + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-04 + Event Four + Event Description Four + 19:00:00 + 21:00:00 + 120 + Open To All + Room Four + Group Three + Singles + Singles Department + Data + + + + + 2018-01-05 + Event One UPDATED + Event Description One UPDATED + 08:00:00 + 08:15:00 + 15 + Open To All + Room One + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + 2018-01-05 + Event Two + Event Description Two + 09:00:00 + 10:00:00 + 60 + Open To All + + Group One + Interest + Department One + Jean Luc Picard + 123-456-7890 + jeanluc@enterprise.com + + + + +