@@ -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