Skip to content

Commit dda5829

Browse files
committed
feat(backend/wordpress): add PHP fallback for database export
- Bump plugin version to 1.0.2 - Add safe_arg() for secure shell argument escaping - Add can_use_shell() to check shell execution availability - Enhance handle_export() with shell-based mysqldump as primary, PHP native export as fallback - Implement export_php_native() for memory-efficient SQL dump generation using mysqli - Improve error handling and buffer cleaning for reliability on restricted hosts This ensures the WordPress plugin works on hosts disabling shell functions by falling back to PHP-based export, enhancing compatibility and security.
1 parent 697e469 commit dda5829

File tree

1 file changed

+122
-44
lines changed

1 file changed

+122
-44
lines changed

backend/wordpress/generator.go

Lines changed: 122 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const pluginTemplate = `<?php
1414
/**
1515
* Plugin Name: DB Sync Connector
1616
* Description: generated by DB Sync Manager App. Handles database export/import securely.
17-
* Version: 1.0.0
17+
* Version: 1.0.2
1818
* Author: DB Sync Manager
1919
*/
2020
@@ -49,47 +49,76 @@ class DBack_Sync_API {
4949
return $auth_header === DBACK_API_KEY;
5050
}
5151
52+
/**
53+
* Safe replacement for escapeshellarg() if disabled by host
54+
*/
55+
private function safe_arg($string) {
56+
if (function_exists('escapeshellarg')) {
57+
return escapeshellarg($string);
58+
}
59+
// Fallback: wrap in single quotes and escape existing single quotes
60+
return "'" . str_replace("'", "'\\''", $string) . "'";
61+
}
62+
63+
/**
64+
* Check if shell execution is available
65+
*/
66+
private function can_use_shell() {
67+
return function_exists('exec') && !in_array('exec', array_map('trim', explode(',', ini_get('disable_functions'))));
68+
}
69+
5270
public function handle_export($request) {
5371
// Increase limits
54-
set_time_limit(0);
55-
ini_set('memory_limit', '512M');
72+
@set_time_limit(0);
73+
@ini_set('memory_limit', '512M');
5674
5775
global $wpdb;
58-
$db_name = DB_NAME;
59-
$db_user = DB_USER;
60-
$db_pass = DB_PASSWORD;
61-
$db_host = DB_HOST;
62-
63-
// Use mysqldump via exec if available (fastest)
6476
$dump_file = wp_upload_dir()['basedir'] . '/dback_dump_' . time() . '.sql.gz';
6577
66-
// Command: mysqldump ... | gzip > file
67-
// Note: We rely on system mysqldump.
68-
$cmd = sprintf(
69-
'mysqldump -h %s -u %s -p%s %s | gzip > %s',
70-
escapeshellarg($db_host),
71-
escapeshellarg($db_user),
72-
escapeshellarg($db_pass),
73-
escapeshellarg($db_name),
74-
escapeshellarg($dump_file)
75-
);
76-
77-
// Execute
78-
exec($cmd, $output, $return_var);
78+
// 1. Try System Shell (mysqldump) if allowed
79+
$shell_success = false;
80+
81+
if ($this->can_use_shell()) {
82+
$db_name = DB_NAME;
83+
$db_user = DB_USER;
84+
$db_pass = DB_PASSWORD;
85+
$db_host = DB_HOST;
86+
87+
$cmd = sprintf(
88+
'mysqldump -h %s -u %s -p%s %s | gzip > %s',
89+
$this->safe_arg($db_host),
90+
$this->safe_arg($db_user),
91+
$this->safe_arg($db_pass),
92+
$this->safe_arg($db_name),
93+
$this->safe_arg($dump_file)
94+
);
95+
96+
@exec($cmd, $output, $return_var);
97+
98+
if ($return_var === 0 && file_exists($dump_file) && filesize($dump_file) > 0) {
99+
$shell_success = true;
100+
}
101+
}
79102
80-
if ($return_var !== 0) {
81-
return new WP_Error('export_failed', 'mysqldump command failed', array('status' => 500));
103+
// 2. Fallback to PHP Native Export if shell failed or is disabled
104+
if (!$shell_success) {
105+
$res = $this->export_php_native($dump_file);
106+
if (is_wp_error($res)) {
107+
return $res;
108+
}
82109
}
83110
111+
// Stream file download
84112
if (!file_exists($dump_file)) {
85-
return new WP_Error('export_failed', 'Dump file not found', array('status' => 500));
113+
return new WP_Error('export_failed', 'Dump file generation failed', array('status' => 500));
86114
}
87115
88-
// Stream file download
89116
$file_size = filesize($dump_file);
90117
91118
// Clean buffer
92-
if (ob_get_level()) ob_end_clean();
119+
while (ob_get_level()) {
120+
ob_end_clean();
121+
}
93122
94123
header('Content-Description: File Transfer');
95124
header('Content-Type: application/gzip');
@@ -106,45 +135,94 @@ class DBack_Sync_API {
106135
exit;
107136
}
108137
109-
public function handle_import($request) {
110-
// Logic to receive file and pipe to mysql
111-
// PHP REST API usually handles small bodies. For large dumps, we normally stream.
112-
// But WP REST API buffers body?
113-
// Ideally, we should read from php://input.
138+
/**
139+
* PHP-based fallback for generating SQL dump
140+
*/
141+
private function export_php_native($filepath) {
142+
global $wpdb;
143+
144+
$fp = gzopen($filepath, 'w9');
145+
if (!$fp) {
146+
return new WP_Error('export_failed', 'Cannot open file for writing. Check permissions.', array('status' => 500));
147+
}
148+
149+
$tables = $wpdb->get_col("SHOW TABLES");
150+
151+
foreach ($tables as $table) {
152+
// Get Create Table
153+
$create_table = $wpdb->get_row("SHOW CREATE TABLE $table", ARRAY_N);
154+
gzwrite($fp, "DROP TABLE IF EXISTS $table;\n");
155+
gzwrite($fp, $create_table[1] . ";\n\n");
156+
157+
// Get Data
158+
// Using unbuffered query via mysqli to save memory if available
159+
if (isset($wpdb->dbh) && ($wpdb->dbh instanceof mysqli)) {
160+
$result = mysqli_query($wpdb->dbh, "SELECT * FROM $table", MYSQLI_USE_RESULT);
161+
if ($result) {
162+
while ($row = mysqli_fetch_row($result)) {
163+
$values = array_map(function($val) {
164+
if ($val === null) return 'NULL';
165+
return "'" . esc_sql($val) . "'";
166+
}, $row);
167+
gzwrite($fp, "INSERT INTO $table VALUES (" . implode(',', $values) . ");\n");
168+
}
169+
mysqli_free_result($result);
170+
}
171+
} else {
172+
// Fallback for older systems (high memory usage)
173+
$rows = $wpdb->get_results("SELECT * FROM $table", ARRAY_N);
174+
foreach ($rows as $row) {
175+
$values = array_map(function($val) {
176+
if ($val === null) return 'NULL';
177+
return "'" . esc_sql($val) . "'";
178+
}, $row);
179+
gzwrite($fp, "INSERT INTO $table VALUES (" . implode(',', $values) . ");\n");
180+
}
181+
}
182+
gzwrite($fp, "\n");
183+
}
184+
185+
gzclose($fp);
186+
return true;
187+
}
114188
115-
set_time_limit(0);
189+
public function handle_import($request) {
190+
@set_time_limit(0);
116191
117192
global $wpdb;
193+
194+
// Check if shell is possible
195+
if (!$this->can_use_shell()) {
196+
return new WP_Error('import_failed', 'Server shell access is disabled. Cannot run mysql import command.', array('status' => 500));
197+
}
198+
118199
$db_name = DB_NAME;
119200
$db_user = DB_USER;
120201
$db_pass = DB_PASSWORD;
121202
$db_host = DB_HOST;
122203
123-
// We can't easily pipe php://input to mysql directly in some setups if body is parsed.
124-
// But let's try saving to temp file first.
125204
$upload_dir = wp_upload_dir()['basedir'];
126205
$temp_file = $upload_dir . '/dback_import_' . time() . '.sql.gz';
127206
207+
// Write input to temp file
128208
$input = fopen('php://input', 'rb');
129209
$file = fopen($temp_file, 'wb');
130210
stream_copy_to_stream($input, $file);
131211
fclose($input);
132212
fclose($file);
133213
134-
// Now run mysql command
135-
// gunzip -c file | mysql ...
136214
$cmd = sprintf(
137215
'gunzip -c %s | mysql -h %s -u %s -p%s %s',
138-
escapeshellarg($temp_file),
139-
escapeshellarg($db_host),
140-
escapeshellarg($db_user),
141-
escapeshellarg($db_pass),
142-
escapeshellarg($db_name)
216+
$this->safe_arg($temp_file),
217+
$this->safe_arg($db_host),
218+
$this->safe_arg($db_user),
219+
$this->safe_arg($db_pass),
220+
$this->safe_arg($db_name)
143221
);
144222
145-
exec($cmd, $output, $return_var);
223+
@exec($cmd, $output, $return_var);
146224
147-
unlink($temp_file);
225+
if (file_exists($temp_file)) unlink($temp_file);
148226
149227
if ($return_var !== 0) {
150228
return new WP_Error('import_failed', 'mysql command failed', array('status' => 500));

0 commit comments

Comments
 (0)