Skip to content

Instantly share code, notes, and snippets.

@bhubbard
Created July 31, 2025 17:54
Show Gist options
  • Select an option

  • Save bhubbard/733482fee8fc02c9c80f2ef6b3250be1 to your computer and use it in GitHub Desktop.

Select an option

Save bhubbard/733482fee8fc02c9c80f2ef6b3250be1 to your computer and use it in GitHub Desktop.
<?php
/**
* Plugin Name: Bloomerang Data Exporter
* Description: Exports data from the Bloomerang API and saves it as JSON files in the uploads directory.
* Version: 1.0.0
* Author: Gemini
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: bloomerang-data-exporter
*/
// Prevent direct file access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Main class for the Bloomerang Data Exporter plugin.
*/
class Bloomerang_Data_Exporter {
/**
* The option name for storing the API key in the database.
* @var string
*/
private $option_name = 'bde_api_key';
/**
* The slug for the admin menu page.
* @var string
*/
private $menu_slug = 'bloomerang-exporter';
/**
* Bloomerang API base URL.
* @var string
*/
private $base_url = 'https://api.bloomerang.co/v2';
/**
* List of API endpoints to export data from.
* @var array
*/
private $endpoints_to_export = [
"constituent",
"transaction",
"interaction",
"fund",
"campaign",
"appeal",
];
/**
* Class constructor.
* Hooks into WordPress actions.
*/
public function __construct() {
add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
add_action( 'admin_init', [ $this, 'register_settings' ] );
add_action( 'admin_post_bde_run_export', [ $this, 'handle_export_trigger' ] );
}
/**
* Adds the plugin page to the WordPress admin menu.
*/
public function add_admin_menu() {
add_menu_page(
__( 'Bloomerang Exporter', 'bloomerang-data-exporter' ),
__( 'Bloomerang Exporter', 'bloomerang-data-exporter' ),
'manage_options',
$this->menu_slug,
[ $this, 'render_admin_page' ],
'dashicons-download'
);
}
/**
* Registers the setting for the API key.
*/
public function register_settings() {
register_setting( 'bde_settings_group', $this->option_name );
}
/**
* Renders the HTML for the admin page.
*/
public function render_admin_page() {
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php settings_errors(); ?>
<div id="poststuff">
<div id="post-body" class="metabox-holder columns-2">
<!-- Main content -->
<div id="post-body-content">
<div class="meta-box-sortables ui-sortable">
<div class="postbox">
<h2 class="hndle"><span><?php _e( 'Export Settings', 'bloomerang-data-exporter' ); ?></span></h2>
<div class="inside">
<form method="post" action="options.php">
<?php
settings_fields( 'bde_settings_group' );
do_settings_sections( 'bde_settings_group' );
?>
<table class="form-table">
<tr valign="top">
<th scope="row">
<label for="<?php echo esc_attr( $this->option_name ); ?>">
<?php _e( 'Bloomerang API Key', 'bloomerang-data-exporter' ); ?>
</label>
</th>
<td>
<input type="password" id="<?php echo esc_attr( $this->option_name ); ?>" name="<?php echo esc_attr( $this->option_name ); ?>" value="<?php echo esc_attr( get_option( $this->option_name ) ); ?>" class="regular-text" />
<p class="description"><?php _e( 'Your v2.0 API key from your Bloomerang user settings.', 'bloomerang-data-exporter' ); ?></p>
</td>
</tr>
</table>
<?php submit_button( 'Save API Key' ); ?>
</form>
</div>
</div>
<div class="postbox">
<h2 class="hndle"><span><?php _e( 'Run Exporter', 'bloomerang-data-exporter' ); ?></span></h2>
<div class="inside">
<p><?php _e( 'Click the button below to start the export process. This may take a long time depending on the amount of data in your Bloomerang account. Please do not navigate away from this page until the process is complete.', 'bloomerang-data-exporter' ); ?></p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="bde_run_export">
<?php wp_nonce_field( 'bde_run_export_nonce' ); ?>
<?php
$api_key = get_option( $this->option_name );
submit_button( 'Export All Data', 'primary', 'submit', true, empty( $api_key ) ? [ 'disabled' => 'disabled' ] : null );
if ( empty( $api_key ) ) {
echo '<p class="description" style="color: red;">' . __( 'You must save an API key before you can run the export.', 'bloomerang-data-exporter' ) . '</p>';
}
?>
</form>
</div>
</div>
</div>
</div>
</div>
<br class="clear">
</div>
</div>
<?php
}
/**
* Handles the export trigger from the admin page button.
*/
public function handle_export_trigger() {
// Verify nonce for security
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'bde_run_export_nonce' ) ) {
wp_die( 'Security check failed.' );
}
// Check user capabilities
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'You do not have permission to perform this action.' );
}
$this->run_export();
// Redirect back to the settings page with a success message
wp_redirect( add_query_arg( [ 'page' => $this->menu_slug, 'message' => 'success' ], admin_url( 'admin.php' ) ) );
exit;
}
/**
* The main export logic.
*/
private function run_export() {
// Increase execution time limit to prevent timeouts on large exports
@set_time_limit( 300 ); // 5 minutes
$api_key = get_option( $this->option_name );
if ( empty( $api_key ) ) {
add_settings_error( 'bde_messages', 'bde_api_key_error', __( 'API Key is not set. Cannot run export.', 'bloomerang-data-exporter' ), 'error' );
return;
}
$total_files_created = 0;
foreach ( $this->endpoints_to_export as $endpoint ) {
$all_data = $this->fetch_all_data_for_endpoint( $endpoint, $api_key );
if ( ! empty( $all_data ) ) {
$this->save_data_to_file( $all_data, $endpoint );
$total_files_created++;
} else {
add_settings_error( 'bde_messages', 'bde_export_error_' . $endpoint, sprintf( __( 'No data was returned or an error occurred for the "%s" endpoint.', 'bloomerang-data-exporter' ), $endpoint ), 'warning' );
}
}
if ($total_files_created > 0) {
add_settings_error( 'bde_messages', 'bde_export_success', sprintf(__( 'Export process completed successfully. %d files were created in your uploads directory.', 'bloomerang-data-exporter' ), $total_files_created), 'success' );
} else {
add_settings_error( 'bde_messages', 'bde_export_failed', __( 'Export process finished, but no data could be exported.', 'bloomerang-data-exporter' ), 'error' );
}
}
/**
* Fetches all records for a single endpoint, handling pagination.
*
* @param string $endpoint The API endpoint to fetch from.
* @param string $api_key The Bloomerang API key.
* @return array A list of all records.
*/
private function fetch_all_data_for_endpoint( $endpoint, $api_key ) {
$all_records = [];
$skip = 0;
$take = 50; // Max records per API request
$headers = [ 'X-API-KEY' => $api_key ];
while ( true ) {
$url = add_query_arg( [
'skip' => $skip,
'take' => $take,
], "{$this->base_url}/{$endpoint}" );
$response = wp_remote_get( $url, [
'headers' => $headers,
'timeout' => 30, // 30-second timeout for the request
] );
if ( is_wp_error( $response ) ) {
add_settings_error( 'bde_messages', 'bde_wp_error_' . $endpoint, sprintf(__( 'Request failed for endpoint "%s": %s', 'bloomerang-data-exporter' ), $endpoint, $response->get_error_message()), 'error' );
return []; // Stop on error
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
add_settings_error( 'bde_messages', 'bde_json_error_' . $endpoint, sprintf(__( 'Failed to decode JSON for endpoint "%s".', 'bloomerang-data-exporter' ), $endpoint), 'error' );
return []; // Stop on error
}
$records = [];
if ( isset( $data['Results'] ) && is_array( $data['Results'] ) ) {
$records = $data['Results'];
} elseif ( isset( $data['results'] ) && is_array( $data['results'] ) ) {
$records = $data['results'];
} elseif ( is_array( $data ) ) {
// Some endpoints might return a simple array
$records = $data;
}
if ( empty( $records ) ) {
break; // No more records to fetch
}
$all_records = array_merge( $all_records, $records );
$skip += $take;
// Small delay to be a good API citizen
usleep( 200000 ); // 0.2 seconds
}
return $all_records;
}
/**
* Saves the fetched data to a JSON file in the uploads directory.
*
* @param array $data The data to save.
* @param string $endpoint The endpoint name, used for the filename.
*/
private function save_data_to_file( $data, $endpoint ) {
$upload_dir = wp_upload_dir();
$safe_endpoint_name = str_replace( '/', '_', $endpoint );
$filename = "bloomerang_export_{$safe_endpoint_name}_" . date( 'Y-m-d_H-i-s' ) . '.json';
$filepath = trailingslashit( $upload_dir['basedir'] ) . $filename;
$json_data = json_encode( $data, JSON_PRETTY_PRINT );
if ( file_put_contents( $filepath, $json_data ) === false ) {
add_settings_error( 'bde_messages', 'bde_file_save_error_' . $endpoint, sprintf(__( 'Failed to save file for endpoint "%s". Check file permissions for the uploads directory.', 'bloomerang-data-exporter' ), $endpoint), 'error' );
}
}
}
// Instantiate the plugin class.
new Bloomerang_Data_Exporter();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment