From c43f86b856c6c442b4ffa354ca23239c2301b689 Mon Sep 17 00:00:00 2001 From: David Stone Date: Wed, 17 Dec 2025 19:25:06 -0700 Subject: [PATCH 1/2] Add import export settings feature --- inc/admin-pages/class-settings-admin-page.php | 185 ++++++++++++++++++ inc/class-settings.php | 110 +++++++++++ inc/helpers/class-settings-porter.php | 180 +++++++++++++++++ 3 files changed, 475 insertions(+) create mode 100644 inc/helpers/class-settings-porter.php diff --git a/inc/admin-pages/class-settings-admin-page.php b/inc/admin-pages/class-settings-admin-page.php index e00b55d7..08b0c2bd 100644 --- a/inc/admin-pages/class-settings-admin-page.php +++ b/inc/admin-pages/class-settings-admin-page.php @@ -611,4 +611,189 @@ public function default_view(): void { $form->render(); } + + /** + * Overrides parent page_loaded to handle export/import functionality. + * + * @since 2.0.0 + * @return void + */ + public function page_loaded() { + + $this->handle_export(); + $this->handle_import_redirect(); + $this->register_forms(); + + parent::page_loaded(); + } + + /** + * Handle settings export request. + * + * @since 2.0.0 + * @return void + */ + protected function handle_export() { + + if ( ! isset($_GET['wu_export_settings'])) { + return; + } + check_admin_referer('wu_export_settings'); + + // Check permissions + if ( ! current_user_can('wu_edit_settings')) { + wp_die(esc_html__('You do not have permission to export settings.', 'ultimate-multisite')); + } + + $result = \WP_Ultimo\Helpers\Settings_Porter::export_settings(); + + if ( ! $result['success']) { + wp_die(esc_html($result['message'])); + } + + \WP_Ultimo\Helpers\Settings_Porter::send_download( + $result['data'], + $result['filename'] + ); + } + + /** + * Register import form. + * + * @since 2.0.0 + * @return void + */ + public function register_forms() { + + wu_register_form( + 'import_settings', + [ + 'render' => [$this, 'render_import_settings_modal'], + 'handler' => [$this, 'handle_import_settings_modal'], + 'capability' => 'wu_edit_settings', + ] + ); + } + + /** + * Render the import settings modal. + * + * @since 2.0.0 + * @return void + */ + public function render_import_settings_modal() { + + $fields = [ + 'import_file_header' => [ + 'type' => 'header', + 'title' => __('Upload Settings File', 'ultimate-multisite'), + 'desc' => __('Select a JSON file previously exported from Ultimate Multisite.', 'ultimate-multisite'), + ], + 'import_file' => [ + 'type' => 'html', + 'content' => '', + ], + 'confirm' => [ + 'type' => 'toggle', + 'title' => __('I understand this will replace all current settings', 'ultimate-multisite'), + 'desc' => __('This action cannot be undone. Make sure you have a backup of your current settings.', 'ultimate-multisite'), + 'value' => false, + 'html_attr' => [ + 'v-model' => 'confirm', + ], + ], + 'submit_button' => [ + 'type' => 'submit', + 'title' => __('Import Settings', 'ultimate-multisite'), + 'value' => 'save', + 'classes' => 'button button-primary wu-w-full', + 'wrapper_classes' => 'wu-items-end', + 'html_attr' => [ + 'v-bind:disabled' => '!confirm', + ], + ], + ]; + + $form = new Form( + 'import_settings', + $fields, + [ + 'views' => 'admin-pages/fields', + 'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0', + 'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid', + 'html_attr' => [ + 'data-wu-app' => 'import_settings_modal', + 'data-state' => wp_json_encode(['confirm' => false]), + 'enctype' => 'multipart/form-data', + ], + ] + ); + + $form->render(); + } + + /** + * Handle import settings form submission. + * + * @since 2.0.0 + * @return void + */ + public function handle_import_settings_modal() { + + // Validate file upload + if ( ! isset($_FILES['import_file']) || empty($_FILES['import_file']['tmp_name'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing + $error = new \WP_Error( + 'no_file', + __('Please select a file to import.', 'ultimate-multisite') + ); + wp_send_json_error($error); + } + + // Validate and parse the file + $validated_data = \WP_Ultimo\Helpers\Settings_Porter::validate_import_file($_FILES['import_file']); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + if (is_wp_error($validated_data)) { + wp_send_json_error($validated_data); + } + + // Import settings + \WP_Ultimo\Helpers\Settings_Porter::import_settings($validated_data); + + // Success + wp_send_json_success( + [ + 'redirect_url' => add_query_arg( + [ + 'tab' => 'import-export', + 'updated' => 1, + ], + wu_network_admin_url('wp-ultimo-settings') + ), + ] + ); + } + + /** + * Display a success message after import redirect. + * + * @since 2.0.0 + * @return void + */ + protected function handle_import_redirect() { + + if ( ! isset($_GET['updated']) || 'import-export' !== wu_request('tab')) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; + } + + add_action( + 'wu_page_wizard_after_title', + function () { + ?> +
+

+
+ add_section( + 'import-export', + [ + 'title' => __('Import/Export', 'ultimate-multisite'), + 'desc' => __('Export your settings to a JSON file or import settings from a previously exported file.', 'ultimate-multisite'), + 'icon' => 'dashicons-wu-download', + 'order' => 995, + ] + ); + + // Export Settings Header + $this->add_field( + 'import-export', + 'export_header', + [ + 'title' => __('Export Settings', 'ultimate-multisite'), + 'desc' => __('Download all your Ultimate Multisite settings as a JSON file for backup or migration purposes.', 'ultimate-multisite'), + 'type' => 'header', + ], + 10 + ); + + // Export Description + $this->add_field( + 'import-export', + 'export_description', + [ + 'type' => 'note', + 'desc' => __('The exported file will contain all ultimate multisite settings defined on this page. This includes general settings, payment gateway configurations, email settings, domain mapping settings, and all other plugin configurations. It does not include products, sites, domains, customers and other entities.', 'ultimate-multisite'), + 'classes' => 'wu-text-gray-600 wu-text-sm', + ], + 20 + ); + + // Export Button + $this->add_field( + 'import-export', + 'export_settings_button', + [ + 'type' => 'submit', + 'title' => __('Export Settings', 'ultimate-multisite'), + 'classes' => 'button button-primary', + 'wrapper_classes' => 'wu-items-start', + 'html_attr' => [ + 'onclick' => 'window.location.href="' . wp_nonce_url( + add_query_arg(['wu_export_settings' => '1'], wu_get_current_url()), + 'wu_export_settings' + ) . '"; return false;', + ], + ], + 30 + ); + + // Import Settings Header + $this->add_field( + 'import-export', + 'import_header', + [ + 'title' => __('Import Settings', 'ultimate-multisite'), + 'desc' => __('Upload a previously exported JSON file to restore settings.', 'ultimate-multisite'), + 'type' => 'header', + ], + 40 + ); + + // Import Button + $this->add_field( + 'import-export', + 'import_settings_button', + [ + 'type' => 'link', + 'display_value' => __('Import Settings', 'ultimate-multisite'), + 'title' => __('Import and Replace All Settings', 'ultimate-multisite'), + 'classes' => 'button button-secondary wu-ml-0 wubox', + 'wrapper_classes' => 'wu-items-start', + 'html_attr' => [ + 'href' => wu_get_form_url( + 'import_settings', + [ + 'width' => 600, + ] + ), + ], + ], + 55 + ); + + // Import Warning + $this->add_field( + 'import-export', + 'import_warning', + [ + 'type' => 'note', + 'desc' => sprintf( + '%s %s', + __('Warning:', 'ultimate-multisite'), + __('Importing settings will replace ALL current settings with the values from the uploaded file. This action cannot be undone. We recommend exporting your current settings as a backup before importing.', 'ultimate-multisite') + ), + 'classes' => 'wu-bg-red-50 wu-border-l-4 wu-border-red-500 wu-p-4', + ], + 60 + ); + + do_action('wu_settings_import_export'); + /* * Other Options * This section holds the Other Options settings of the Ultimate Multisite Plugin. diff --git a/inc/helpers/class-settings-porter.php b/inc/helpers/class-settings-porter.php new file mode 100644 index 00000000..feffa9da --- /dev/null +++ b/inc/helpers/class-settings-porter.php @@ -0,0 +1,180 @@ + '1.0.0', + 'plugin' => 'ultimate-multisite', + 'timestamp' => time(), + 'site_url' => get_site_url(), + 'wp_version' => get_bloginfo('version'), + 'settings' => $settings, + ]; + + $filename = sprintf( + 'ultimate-multisite-settings-export-%s-%s.json', + gmdate('Y-m-d-His'), + get_current_site()->cookie_domain, + ); + + return [ + 'success' => true, + 'data' => $export_data, + 'filename' => $filename, + ]; + } + + /** + * Validate an uploaded import file. + * + * @since 2.0.0 + * + * @param array $file The $_FILES array element. + * @return \WP_Error|array WP_Error on failure, validated data array on success. + */ + public static function validate_import_file($file) { + + // Check for upload errors + if (UPLOAD_ERR_OK !== $file['error']) { + return new \WP_Error( + 'upload_error', + __('File upload failed. Please try again.', 'ultimate-multisite') + ); + } + + // Check file extension + $file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + + if ('json' !== $file_ext) { + return new \WP_Error( + 'invalid_file_type', + __('Invalid file type. Please upload a JSON file.', 'ultimate-multisite') + ); + } + + // Check file size (max 5MB) + $max_size = 5 * 1024 * 1024; + + if ($file['size'] > $max_size) { + return new \WP_Error( + 'file_too_large', + __('File is too large. Maximum size is 5MB.', 'ultimate-multisite') + ); + } + + // Read and decode JSON + $json_content = file_get_contents($file['tmp_name']); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + + if (false === $json_content) { + return new \WP_Error( + 'read_error', + __('Failed to read the uploaded file.', 'ultimate-multisite') + ); + } + + $data = json_decode($json_content, true); + + if (null === $data) { + return new \WP_Error( + 'invalid_json', + __('Invalid JSON format. Please upload a valid export file.', 'ultimate-multisite') + ); + } + + // Validate structure + if (! isset($data['plugin']) || 'ultimate-multisite' !== $data['plugin']) { + return new \WP_Error( + 'invalid_format', + __('This file does not appear to be a valid Ultimate Multisite settings export.', 'ultimate-multisite') + ); + } + + if (! isset($data['settings']) || ! is_array($data['settings'])) { + return new \WP_Error( + 'invalid_structure', + __('The settings data in this file is invalid or missing.', 'ultimate-multisite') + ); + } + + return $data; + } + + /** + * Import settings from validated data. + * + * @since 2.0.0 + * + * @param array $data The validated import data. + * @return void + */ + public static function import_settings($data) { + + $settings = $data['settings']; + + // Sanitize settings before import + + $sanitized_settings = array_map( + function ($value) { + return wu_clean($value); + }, + $settings + ); + + // Use the Settings class save_settings method for proper validation + WP_Ultimo()->settings->save_settings($sanitized_settings); + + do_action('wu_settings_imported', $sanitized_settings, $data); + } + + /** + * Send JSON a file as download to the browser. + * + * @since 2.0.0 + * + * @param string $json The JSON string. + * @param string $filename The filename. + * @return void + */ + public static function send_download($json, $filename) { + + nocache_headers(); + + header('Content-Disposition: attachment; filename=' . $filename); + header('Content-Length: ' . strlen($json)); + header('Pragma: no-cache'); + header('Expires: 0'); + wp_send_json($json, null, JSON_PRETTY_PRINT); + + exit; + } +} From fba0846c3f0ee3db5e751c1a6a5cee06352dcbea Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 30 Dec 2025 13:19:53 -0700 Subject: [PATCH 2/2] improve code --- inc/admin-pages/class-settings-admin-page.php | 124 ++++++++++-- inc/helpers/class-settings-porter.php | 180 ------------------ 2 files changed, 103 insertions(+), 201 deletions(-) delete mode 100644 inc/helpers/class-settings-porter.php diff --git a/inc/admin-pages/class-settings-admin-page.php b/inc/admin-pages/class-settings-admin-page.php index 08b0c2bd..e3734123 100644 --- a/inc/admin-pages/class-settings-admin-page.php +++ b/inc/admin-pages/class-settings-admin-page.php @@ -9,6 +9,8 @@ namespace WP_Ultimo\Admin_Pages; +use SebastianBergmann\Template\RuntimeException; +use WP_Ultimo\Exception\Runtime_Exception; use WP_Ultimo\Settings; use WP_Ultimo\UI\Form; use WP_Ultimo\UI\Field; @@ -645,16 +647,31 @@ protected function handle_export() { wp_die(esc_html__('You do not have permission to export settings.', 'ultimate-multisite')); } - $result = \WP_Ultimo\Helpers\Settings_Porter::export_settings(); + $this->export_settings(); + $settings = wu_get_all_settings(); - if ( ! $result['success']) { - wp_die(esc_html($result['message'])); - } + $export_data = [ + 'version' => \WP_Ultimo::VERSION, + 'plugin' => 'ultimate-multisite', + 'timestamp' => time(), + 'site_url' => get_site_url(), + 'wp_version' => get_bloginfo('version'), + 'settings' => $settings, + ]; - \WP_Ultimo\Helpers\Settings_Porter::send_download( - $result['data'], - $result['filename'] + $filename = sprintf( + 'ultimate-multisite-settings-export-%s-%s.json', + gmdate('Y-m-d'), + get_current_site()->cookie_domain, ); + nocache_headers(); + + header('Content-Disposition: attachment; filename=' . $filename); + header('Pragma: no-cache'); + header('Expires: 0'); + wp_send_json($export_data, null, JSON_PRETTY_PRINT); + + exit; } /** @@ -737,27 +754,59 @@ public function render_import_settings_modal() { * * @since 2.0.0 * @return void + * @throws Runtime_Exception When an error is found in the file. */ public function handle_import_settings_modal() { - // Validate file upload - if ( ! isset($_FILES['import_file']) || empty($_FILES['import_file']['tmp_name'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing - $error = new \WP_Error( - 'no_file', - __('Please select a file to import.', 'ultimate-multisite') - ); - wp_send_json_error($error); - } + try { + // Validate file upload + if ( ! isset($_FILES['import_file']) || empty($_FILES['import_file']['tmp_name'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing + throw new Runtime_Exception('no_file'); + } + + // Validate and parse the file + $file = $_FILES['import_file']; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + // Check for upload errors + if (UPLOAD_ERR_OK !== $file['error']) { + throw new Runtime_Exception('upload_error'); + } + + // Check file extension + $file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + + if ('json' !== $file_ext) { + throw new Runtime_Exception('invalid_file_type'); + } + + // Read and decode JSON + $json_content = file_get_contents($file['tmp_name']); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + + if (false === $json_content) { + throw new Runtime_Exception('read_error'); + } + + $data = json_decode($json_content, true); + + if (null === $data) { + throw new Runtime_Exception('invalid_json'); + } - // Validate and parse the file - $validated_data = \WP_Ultimo\Helpers\Settings_Porter::validate_import_file($_FILES['import_file']); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + // Validate structure + if ( ! isset($data['plugin']) || 'ultimate-multisite' !== $data['plugin']) { + throw new Runtime_Exception('invalid_format'); + } - if (is_wp_error($validated_data)) { - wp_send_json_error($validated_data); + if ( ! isset($data['settings']) || ! is_array($data['settings'])) { + throw new Runtime_Exception('invalid_structure'); + } + } catch ( Runtime_Exception $e ) { + wp_send_json_error(new \WP_Error($e->getMessage(), __('Something is wrong with the uploaded file.', 'ultimate-multisite'))); } - // Import settings - \WP_Ultimo\Helpers\Settings_Porter::import_settings($validated_data); + WP_Ultimo()->settings->save_settings($data['settings']); + + do_action('wu_settings_imported', $data['settings'], $data); // Success wp_send_json_success( @@ -796,4 +845,37 @@ function () { } ); } + + /** + * Export settings to JSON format. + * + * @since 2.0.0 + * + * @return array Array containing 'success' bool, 'data' string (JSON), and 'filename' string. + */ + private function export_settings() { + + $settings = wu_get_all_settings(); + + $export_data = [ + 'version' => \WP_Ultimo::VERSION, + 'plugin' => 'ultimate-multisite', + 'timestamp' => time(), + 'site_url' => get_site_url(), + 'wp_version' => get_bloginfo('version'), + 'settings' => $settings, + ]; + + $filename = sprintf( + 'ultimate-multisite-settings-export-%s-%s.json', + gmdate('Y-m-d-His'), + get_current_site()->cookie_domain, + ); + + return [ + 'success' => true, + 'data' => $export_data, + 'filename' => $filename, + ]; + } } diff --git a/inc/helpers/class-settings-porter.php b/inc/helpers/class-settings-porter.php deleted file mode 100644 index feffa9da..00000000 --- a/inc/helpers/class-settings-porter.php +++ /dev/null @@ -1,180 +0,0 @@ - '1.0.0', - 'plugin' => 'ultimate-multisite', - 'timestamp' => time(), - 'site_url' => get_site_url(), - 'wp_version' => get_bloginfo('version'), - 'settings' => $settings, - ]; - - $filename = sprintf( - 'ultimate-multisite-settings-export-%s-%s.json', - gmdate('Y-m-d-His'), - get_current_site()->cookie_domain, - ); - - return [ - 'success' => true, - 'data' => $export_data, - 'filename' => $filename, - ]; - } - - /** - * Validate an uploaded import file. - * - * @since 2.0.0 - * - * @param array $file The $_FILES array element. - * @return \WP_Error|array WP_Error on failure, validated data array on success. - */ - public static function validate_import_file($file) { - - // Check for upload errors - if (UPLOAD_ERR_OK !== $file['error']) { - return new \WP_Error( - 'upload_error', - __('File upload failed. Please try again.', 'ultimate-multisite') - ); - } - - // Check file extension - $file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); - - if ('json' !== $file_ext) { - return new \WP_Error( - 'invalid_file_type', - __('Invalid file type. Please upload a JSON file.', 'ultimate-multisite') - ); - } - - // Check file size (max 5MB) - $max_size = 5 * 1024 * 1024; - - if ($file['size'] > $max_size) { - return new \WP_Error( - 'file_too_large', - __('File is too large. Maximum size is 5MB.', 'ultimate-multisite') - ); - } - - // Read and decode JSON - $json_content = file_get_contents($file['tmp_name']); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents - - if (false === $json_content) { - return new \WP_Error( - 'read_error', - __('Failed to read the uploaded file.', 'ultimate-multisite') - ); - } - - $data = json_decode($json_content, true); - - if (null === $data) { - return new \WP_Error( - 'invalid_json', - __('Invalid JSON format. Please upload a valid export file.', 'ultimate-multisite') - ); - } - - // Validate structure - if (! isset($data['plugin']) || 'ultimate-multisite' !== $data['plugin']) { - return new \WP_Error( - 'invalid_format', - __('This file does not appear to be a valid Ultimate Multisite settings export.', 'ultimate-multisite') - ); - } - - if (! isset($data['settings']) || ! is_array($data['settings'])) { - return new \WP_Error( - 'invalid_structure', - __('The settings data in this file is invalid or missing.', 'ultimate-multisite') - ); - } - - return $data; - } - - /** - * Import settings from validated data. - * - * @since 2.0.0 - * - * @param array $data The validated import data. - * @return void - */ - public static function import_settings($data) { - - $settings = $data['settings']; - - // Sanitize settings before import - - $sanitized_settings = array_map( - function ($value) { - return wu_clean($value); - }, - $settings - ); - - // Use the Settings class save_settings method for proper validation - WP_Ultimo()->settings->save_settings($sanitized_settings); - - do_action('wu_settings_imported', $sanitized_settings, $data); - } - - /** - * Send JSON a file as download to the browser. - * - * @since 2.0.0 - * - * @param string $json The JSON string. - * @param string $filename The filename. - * @return void - */ - public static function send_download($json, $filename) { - - nocache_headers(); - - header('Content-Disposition: attachment; filename=' . $filename); - header('Content-Length: ' . strlen($json)); - header('Pragma: no-cache'); - header('Expires: 0'); - wp_send_json($json, null, JSON_PRETTY_PRINT); - - exit; - } -}