Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 267 additions & 0 deletions inc/admin-pages/class-settings-admin-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

namespace WP_Ultimo\Admin_Pages;

use SebastianBergmann\Template\RuntimeException;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused and incorrect import.

Line 12 imports SebastianBergmann\Template\RuntimeException, which is from the PHPUnit test library and should not be used in production code. This import is unused throughout the file—the code correctly uses WP_Ultimo\Exception\Runtime_Exception from line 13.

🔎 Proposed fix
-use SebastianBergmann\Template\RuntimeException;
 use WP_Ultimo\Exception\Runtime_Exception;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
use SebastianBergmann\Template\RuntimeException;
use WP_Ultimo\Exception\Runtime_Exception;
🤖 Prompt for AI Agents
In inc/admin-pages/class-settings-admin-page.php around line 12, remove the
incorrect and unused import "use SebastianBergmann\Template\RuntimeException;" —
delete that line so the file relies on the correct
WP_Ultimo\Exception\Runtime_Exception import on the following line and avoid
pulling PHPUnit test libs into production code.

use WP_Ultimo\Exception\Runtime_Exception;
use WP_Ultimo\Settings;
use WP_Ultimo\UI\Form;
use WP_Ultimo\UI\Field;
Expand Down Expand Up @@ -611,4 +613,269 @@ 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'));
}

$this->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'),
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;
}
Comment on lines +638 to +675
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Eliminate code duplication with export_settings() method.

Line 650 calls $this->export_settings() but ignores the return value, then lines 651–660 duplicate the exact same logic found in the export_settings() method (lines 858–867). This duplication creates a maintenance burden.

🔎 Proposed fix

Refactor to use the return value from export_settings():

-	$this->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'),
-		get_current_site()->cookie_domain,
-	);
+	$result = $this->export_settings();
+	$export_data = $result['data'];
+	$filename = $result['filename'];
+
 	nocache_headers();
 
 	header('Content-Disposition: attachment; filename=' . $filename);

Note: You may also want to update the timestamp format in export_settings() (line 871) from 'Y-m-d-His' to 'Y-m-d' to match the original format, or keep the more precise timestamp.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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'));
}
$this->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'),
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;
}
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 = $this->export_settings();
$export_data = $result['data'];
$filename = $result['filename'];
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;
}
🤖 Prompt for AI Agents
In inc/admin-pages/class-settings-admin-page.php around lines 638 to 675, the
handler calls $this->export_settings() then rebuilds the same export payload
inline, duplicating logic from export_settings(); instead, change
handle_export() to capture and use the return value from
$this->export_settings() (e.g. $export_data = $this->export_settings();) and
remove the duplicated payload construction so the single export_settings()
implementation supplies the data, then keep the existing
headers/filename/response logic (ensure filename generation and timestamp format
remain correct or adjust export_settings() timestamp to 'Y-m-d' if you want
exact parity), and call wp_send_json($export_data, null, JSON_PRETTY_PRINT)
before exit.


/**
* 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' => '<input type="file" name="import_file" id="import_file" accept=".json" required class="wu-w-full" />',
],
'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
* @throws Runtime_Exception When an error is found in the file.
*/
public function handle_import_settings_modal() {

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 structure
if ( ! isset($data['plugin']) || 'ultimate-multisite' !== $data['plugin']) {
throw new Runtime_Exception('invalid_format');
}

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')));
}

WP_Ultimo()->settings->save_settings($data['settings']);

do_action('wu_settings_imported', $data['settings'], $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 () {
?>
<div id="message" class="updated notice wu-admin-notice notice-success is-dismissible">
<p><?php esc_html_e('Settings successfully imported!', 'ultimate-multisite'); ?></p>
</div>
<?php
}
);
}

/**
* 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,
];
}
}
110 changes: 110 additions & 0 deletions inc/class-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -1514,6 +1514,116 @@ public function default_sections(): void {

do_action('wu_settings_integrations');

/*
* Import/Export
* This section holds the Import/Export settings of the Ultimate Multisite Plugin.
*/

$this->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(
'<strong class="wu-text-red-600">%s</strong> %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.
Expand Down
Loading