diff --git a/.gitignore b/.gitignore
index d8712170..50650654 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,3 +60,4 @@ client-mu-plugins/goodbids/config.local.json
!/client-mu-plugins/goodbids/vendor/bshaffer/
!/plugins/wp-mail-smtp-pro/
!/plugins/wp-mail-smtp-pro/vendor/
+!/plugins/batch-upload-plugin/
diff --git a/plugins/batch-upload-plugin/batch-upload-plugin.php b/plugins/batch-upload-plugin/batch-upload-plugin.php
new file mode 100644
index 00000000..e9f29e4d
--- /dev/null
+++ b/plugins/batch-upload-plugin/batch-upload-plugin.php
@@ -0,0 +1,384 @@
+';
+ echo '
' . __( 'Batch Upload', 'goodbids' ) . '
';
+
+ if ( isset( $_POST['batch_upload_nonce'] ) && wp_verify_nonce( $_POST['batch_upload_nonce'], 'batch_upload_action' ) ) {
+ if ( isset( $_FILES['batch_upload_csv'] ) ) {
+ $file = $_FILES['batch_upload_csv'];
+ $filename = $file['tmp_name'];
+
+ if ( ( $handle = fopen( $filename, 'r' ) ) !== false ) {
+ fgetcsv( $handle, 1000, ',' ); // Skip the header row
+
+ $errors = [];
+ $success_sites = [];
+ $failed_sites = [];
+ $row_number = 1;
+
+ // Disable the "New Site Registration" email
+ add_filter( 'wpmu_welcome_notification', '__return_false' );
+
+ // Disable the "Admin Email Changed" email
+ add_filter( 'update_option_new_admin_email', '__return_false', 10, 2 );
+
+ while ( ( $data = fgetcsv( $handle, 1000, ',' ) ) !== false ) {
+ $row_number ++;
+
+ if ( count( $data ) !== 11 ) {
+ $errors[] = sprintf( __( 'Error on row %d: incorrect number of columns', 'goodbids' ), $row_number );
+ continue;
+ }
+
+ $fileName = trim( $data[0] );
+ $ein = trim( $data[1] );
+ $website = trim( $data[2] );
+ $contactName = trim( $data[3] );
+ $contactEmail = trim( $data[4] );
+ $contactTitle = trim( $data[5] );
+ $blogName = trim( $data[6] );
+ $blogDescription = trim( $data[7] );
+ $timezone = trim( $data[8] );
+ $site_url = trim( $data[9] );
+ $home = trim( $data[10] );
+
+ // Skip empty rows
+ if ( empty( $fileName ) && empty( $ein ) && empty( $website ) && empty( $contactName ) && empty( $contactEmail ) && empty( $contactTitle ) && empty( $blogName ) && empty( $blogDescription ) && empty( $timezone ) && empty( $site_url ) && empty( $home ) ) {
+ continue;
+ }
+
+ $row_errors = [];
+
+ if ( empty( $fileName ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: Legal Name is missing', 'goodbids' ), $row_number );
+ }
+
+ if ( empty( $ein ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: EIN is missing', 'goodbids' ), $row_number );
+ }
+ elseif ( ! preg_match( '/^\d{2}-\d{7}$/', $ein ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: EIN format is incorrect', 'goodbids' ), $row_number );
+ }
+
+ if ( empty( $website ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: Website is missing', 'goodbids' ), $row_number );
+ }
+ elseif ( ! goodbids_validate_domain( $website ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: Website value is invalid', 'goodbids' ), $row_number );
+ }
+
+ if ( empty( $contactName ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: Primary Contact legal_name is missing', 'goodbids' ), $row_number );
+ }
+
+ if ( empty( $contactEmail ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: Primary Contact Email Address is missing', 'goodbids' ), $row_number );
+ }
+ elseif ( ! filter_var( $contactEmail, FILTER_VALIDATE_EMAIL ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: Primary Contact Email Address is invalid', 'goodbids' ), $row_number );
+ }
+
+ if ( empty( $contactTitle ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: Primary Contact Job Title is missing', 'goodbids' ), $row_number );
+ }
+
+ if ( empty( $blogName ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: Blog Name is missing', 'goodbids' ), $row_number );
+ }
+
+ if ( empty( $blogDescription ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: Blog Description is missing', 'goodbids' ), $row_number );
+ }
+
+ if ( empty( $timezone ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: timezone_string is missing', 'goodbids' ), $row_number );
+ }
+ elseif ( ! in_array( $timezone, timezone_identifiers_list() ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: timezone_string is invalid', 'goodbids' ), $row_number );
+ }
+
+ if ( empty( $site_url ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: site_url is missing', 'goodbids' ), $row_number );
+ }
+ elseif ( ! goodbids_validateUrl( $site_url ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: site_url is invalid', 'goodbids' ), $row_number );
+ }
+
+ if ( empty( $home ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: home value is missing', 'goodbids' ), $row_number );
+ }
+ elseif ( ! goodbids_validateUrl( $home ) ) {
+ $row_errors[] = sprintf( __( 'Error on row %d: home value is invalid', 'goodbids' ), $row_number );
+ }
+
+ // Skip site creation if there are any validation errors
+ if ( ! empty( $row_errors ) ) {
+ $errors = array_merge( $errors, $row_errors );
+ $failed_sites[] = $site_url;
+ continue;
+ }
+
+ $main_domain = get_network()->domain;
+ $user_id = get_current_user_id();
+ $path = parse_url( $site_url, PHP_URL_PATH );
+
+ $result = wpmu_create_blog( $main_domain, $path, $blogName, $user_id, [ 'public' => 1 ], get_current_network_id() );
+
+ if ( $result instanceof WP_Error ) {
+ $errors[] = sprintf( __( 'Error on row %d: error creating site for domain: %s. Error message: %s', 'goodbids' ), $row_number, esc_html( $main_domain . $path ), $result->get_error_message() );
+ $failed_sites[] = $site_url;
+ }
+ else {
+ $success_sites[] = $site_url;
+
+ $processed_data = [
+ 'legal_name' => $fileName,
+ 'EIN' => $ein,
+ 'website' => $website,
+ 'primary_contact_name' => $contactName,
+ 'primary_contact_email' => $contactEmail,
+ 'primary_contact_job_title' => $contactTitle,
+ 'blog_name' => $blogName,
+ 'blog_description' => $blogDescription,
+ 'timezone_string' => $timezone,
+ 'subdirectory_path' => $path,
+ ];
+
+ goodbids_update_site_options( $result, $processed_data );
+ }
+ }
+
+ // Re-enable the "New Site Registration" email
+ remove_filter( 'wpmu_welcome_notification', '__return_false' );
+
+ // Re-enable the "Admin Email Changed" email
+ remove_filter( 'update_option_new_admin_email', '__return_false', 10 );
+
+ fclose( $handle );
+
+ if ( ! empty( $errors ) ) {
+ echo '' . __( 'CSV file contains errors. Please correct the data and try again.', 'goodbids' ) . '
';
+ echo '
';
+ foreach ( $errors as $error ) {
+ echo '- ' . esc_html( $error ) . '
';
+ }
+ echo '
';
+ }
+
+ if ( ! empty( $success_sites ) ) {
+ echo '' . sprintf( __( '%d site(s) created successfully:', 'goodbids' ), count( $success_sites ) ) . '
';
+ echo '
';
+ foreach ( $success_sites as $site ) {
+ echo '- ' . esc_html( $site ) . '
';
+ }
+ echo '
';
+ }
+
+ if ( ! empty( $failed_sites ) ) {
+ echo '' . sprintf( __( '%d site(s) failed to be created:', 'goodbids' ), count( $failed_sites ) ) . '
';
+ echo '
';
+ foreach ( $failed_sites as $site ) {
+ echo '- ' . esc_html( $site ) . '
';
+ }
+ echo '
';
+ }
+
+ $subject = 'Batch Upload Results';
+ $message = "Batch upload completed.\n\n";
+
+ if ( ! empty( $success_sites ) ) {
+ $message .= "The following sites were created successfully:\n";
+ $message .= implode( "\n", $success_sites );
+ $message .= "\n\n";
+ }
+
+ if ( ! empty( $failed_sites ) ) {
+ $message .= "The following sites failed to be created:\n";
+ $message .= implode( "\n", $failed_sites );
+ $message .= "\n\n";
+ }
+
+ if ( ! empty( $errors ) ) {
+ $message .= "The following errors occurred:\n";
+ $message .= implode( "\n", array_map( function ( $error ) {
+ return '- ' . $error;
+ }, $errors ) );
+ $message .= "\n\n";
+ }
+
+ wp_mail( $toEmail, $subject, $message, '', [ 'cc' => $ccEmail ] );
+ }
+ }
+ }
+
+ echo '';
+
+ // Add a link to the CSV file
+ $csv_file_url = plugins_url( 'example.csv', __FILE__ );
+ echo 'Download the example CSV file: example.csv
';
+
+ echo '';
+}
+
+function goodbids_validateUrl( $url ) {
+
+ // Validate the modified URL
+ if ( filter_var( $url, FILTER_VALIDATE_URL ) ) {
+
+ return true;
+
+ }
+ else {
+
+ return false;
+
+ }
+}
+
+// Function to update site options based on dynamic fields
+function goodbids_update_site_options( $blog_id, $data ) {
+
+ $legalName = $data['legal_name'];
+ $EIN = $data['EIN'];
+ $website = $data['website'];
+ $contactName = $data['primary_contact_name'];
+ $contactEmail = $data['primary_contact_email'];
+ $contactTitle = $data['primary_contact_job_title'];
+ $blogName = $data['blog_name'];
+ $blogDescription = $data['blog_description'];
+ $subdirectoryPath = $data['subdirectory_path']; // Get subdirectory path from processed data
+ $timezone_string = $data['timezone_string'];
+
+ $dynamic_fields = [
+ 'EIN' => 'EIN',
+ 'legal_name' => 'legal_name',
+ 'primary_contact_email_address' => 'same as primary contact email address',
+ 'primary_contact_job_title' => 'same as primary primary contact job title',
+ 'home' => 'same as site url',
+ 'siteurl' => 'same as site url',
+ 'finance_contact_legal_name' => 'legal_name',
+ 'finance_contact_email_address' => 'same as primary contact email address',
+ 'finance_contact_job_title' => 'same as primary primary contact job title',
+ 'admin_email' => 'same as primary contact email',
+ 'woocommerce_email_from_name' => 'blogname',
+ 'woocommerce_stock_email_recipient' => 'same as primary contact email',
+ 'goodbids_onboarded' => 'upload time',
+ 'new_admin_email' => 'same as admin email',
+ 'blogdescription' => 'blogdescription',
+ 'timezone_string' => 'timezone string',
+ 'primary_contact_legal_name' => 'primary contact legal name',
+ 'website' => 'website',
+
+ ];
+
+ foreach ( $dynamic_fields as $field => $default_value ) {
+ $value = '';
+
+ switch ( $default_value ) {
+ case 'legal_name':
+ $value = $legalName;
+ break;
+ case 'EIN':
+ $value = $EIN;
+ break;
+ case 'same as primary contact email address':
+ $value = $contactEmail;
+ break;
+ case 'same as primary primary contact job title':
+ $value = $contactTitle;
+ break;
+ case 'same as site url':
+ $value = network_site_url( $subdirectoryPath ); // Use subdirectory path to generate site URL
+ break;
+ case 'blogname':
+ $value = $blogName;
+ break;
+ case 'upload time':
+ $value = current_time( 'mysql' );
+ break;
+ case 'same as primary contact email':
+ case 'same as admin email':
+ $value = $contactEmail;
+ break;
+ case 'blogdescription':
+ $value = $blogDescription;
+ break;
+ case 'timezone string':
+ $value = $timezone_string;
+ break;
+ case 'primary contact legal name':
+ $value = $contactName;
+ break;
+ case 'website':
+ $value = $website;
+ break;
+ default:
+ $value = $default_value;
+ }
+
+ update_blog_option( $blog_id, $field, $value );
+ }
+}
+
+
+function goodbids_validate_domain( $domain ) {
+ // Remove any leading/trailing spaces
+ $domain = trim( $domain );
+
+ // Convert to lowercase for consistent comparison
+ $domain = strtolower( $domain );
+
+ // Check if the domain starts with "www." and contains valid characters
+ if ( strpos( $domain, 'www.' ) === 0 && preg_match( '/^[a-z0-9.-]+$/', $domain ) ) {
+ return true; // Valid domain
+ }
+ else {
+ return false; // Invalid domain
+ }
+}
+
+?>
diff --git a/plugins/batch-upload-plugin/example.csv b/plugins/batch-upload-plugin/example.csv
new file mode 100644
index 00000000..14307288
--- /dev/null
+++ b/plugins/batch-upload-plugin/example.csv
@@ -0,0 +1,2 @@
+Legal Name,EIN,website,primary contact legal name,primary contact email address,primary contact job title,blogname,blogdescription,timezone_string,site_url,home
+Friends of The Columbia Gorge,93-0782467,WWW.GORGEFRIENDS.ORG,Jasper Croome,tech@goodbids.org,Software Developer,Friends of the Columbia Gorge,"For Decades, Friends of the Gorge has been advocating on behalf of the Columbia River Gorge",America/New_York,https://goodbids.org/friends-of-the-gorge,https://goodbids.org/friends-of-the-gorge
\ No newline at end of file