PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.9.2
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.9.2
4.9.2 4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Framework / Utils / DBPermissions.php
wp-staging / Framework / Utils Last commit date
Cache 7 months ago DBPermissions.php 9 months ago DataEncoder.php 10 months ago DatabaseOptions.php 2 weeks ago Escape.php 9 months ago Glob.php 2 years ago Hooks.php 2 months ago Math.php 9 months ago PluginInfo.php 5 months ago Sanitize.php 2 months ago ServerVars.php 2 years ago SlashMode.php 5 years ago Strings.php 7 months ago Times.php 5 months ago Urls.php 2 weeks ago Version.php 1 year ago WpDefaultDirectories.php 2 years ago
DBPermissions.php
218 lines
1 <?php
2
3 namespace WPStaging\Framework\Utils;
4
5 use wpdb;
6 use ArrayIterator;
7 use WPStaging\Framework\Security\Auth;
8
9 /**
10 * Class Check user permissions on DB
11 * @package WPStaging\Framework\Utils
12 */
13 class DBPermissions
14 {
15 /** @var wpdb */
16 protected $wpdb;
17
18 /** @var Auth */
19 private $auth;
20
21 public function __construct(wpdb $wpdb, Auth $auth)
22 {
23 $this->wpdb = $wpdb;
24 $this->auth = $auth;
25 }
26
27 /**
28 * Ajax check user permissions on DB before push process start
29 */
30 public function ajaxCheckDBPermissions()
31 {
32 if (!$this->auth->isAuthenticatedRequest()) {
33 return;
34 }
35
36 $type = isset($_POST['type']) ? sanitize_text_field($_POST['type']) : '';
37 $grantsToCheck = ['CREATE', 'UPDATE', 'INSERT', 'DROP'];
38 if ($type === 'push') {
39 $grantsToCheck[] = 'ALTER';
40 }
41
42 if ($this->isAllowed(['ALL PRIVILEGES']) || $this->isAllowed($grantsToCheck)) {
43 wp_send_json_success();
44 }
45
46 $action = !empty($type) ? $type : 'restore';
47 $permissions = $action === 'push' ? 'CREATE, UPDATE, ALTER, INSERT, DROP' : 'CREATE, UPDATE, INSERT, DROP';
48
49 $message = sprintf(
50 __("The database user might not have sufficient permissions to use the %s action. Continue the process anyway by clicking the 'Proceed' button or change the user's DB permissions and resume the process.<br/><br/> Required permissions are: %s.", 'wp-staging'),
51 $action,
52 $permissions
53 );
54
55
56 $message = '<span id="wpstg-permission-info-output">' . $message . '</span>';
57 $message .= '<span id="wpstg-permission-info-data">' . $this->getDebugInfo() . '</span>';
58 $message .= '<br/><button type="button" id="wpstg-db-permission-show-full-message" class="wpstg-link-btn wpstg-blue-primary">' . __("Show Full Message", "wp-staging") . '</button>';
59
60 wp_send_json_error([
61 'message' => wp_kses_post($message),
62 ]);
63 }
64
65 /**
66 * Check if the current user has the grants given in arguments.
67 *
68 * @param array $grantsToCheck
69 * @return bool
70 */
71 public function isAllowed(array $grantsToCheck): bool
72 {
73 $grants = $this->wpdb->get_results("SHOW GRANTS;");
74 if (empty($grants) || $this->wpdb->last_error) {
75 return false;
76 }
77
78 $hasGranted = array_filter($grants, function ($grant) use ($grantsToCheck) {
79 $grantString = (new ArrayIterator($grant))->current();
80
81 // Check if this grant applies to our database or all databases
82 if (!$this->hasGrantForCurrentDatabase($grantString)) {
83 return false;
84 }
85
86 // Check if ALL PRIVILEGES is granted (covers everything)
87 if ($this->hasAllPrivileges($grantString)) {
88 return true;
89 }
90
91 // Check specific permissions
92 return $this->hasRequiredPermissions($grantString, $grantsToCheck);
93 });
94
95 return !empty($hasGranted);
96 }
97
98 /**
99 * @return string
100 */
101 public function getDebugInfo(): string
102 {
103 $dbUser = empty($_POST['databaseUser']) ? DB_USER : sanitize_text_field($_POST['databaseUser']);
104 $dbName = empty($_POST['databaseDatabase']) ? DB_NAME : sanitize_text_field($_POST['databaseDatabase']);
105 $dbHost = empty($_POST['databaseServer']) ? DB_HOST : sanitize_text_field($_POST['databaseServer']);
106
107 $data = '<textarea class="wpstg-permission-info-output wpstg-textbox" readonly="readonly" name="wpstg-permission-info" title="' . __('Please copy and paste this message and report it to us!', 'wp-staging') . '">';
108 $data .= PHP_EOL . __('DB Name: ', 'wp-staging') . $dbName;
109 $data .= PHP_EOL . __('DB User: ', 'wp-staging') . $dbUser;
110 $data .= PHP_EOL . __('DB Host: ', 'wp-staging') . $dbHost;
111
112 $grants = $this->wpdb->get_results("SHOW GRANTS;");
113 if (empty($grants) || $this->wpdb->last_error) {
114 $data .= PHP_EOL . __('wpdb query error: ', 'wp-staging') . $this->wpdb->last_error;
115 return wp_kses_post($data);
116 }
117
118 $grantsHtml = '';
119 foreach ($grants as $grant) {
120 $grantString = (new ArrayIterator($grant))->current();
121 $grantString = preg_replace('/IDENTIFIED BY PASSWORD\s+([\'"])(?:\\\\.|(?!\1).)*\1/', "IDENTIFIED BY PASSWORD '********'", $grantString);
122 $grantString = preg_replace('/IDENTIFIED BY\s+([\'"])(?:\\\\.|(?!\1).)*\1/', "IDENTIFIED BY '********'", $grantString);
123 $grantsHtml .= PHP_EOL . $grantString . ';';
124 }
125
126 $data .= PHP_EOL . __('User Grants: ', 'wp-staging') . $grantsHtml;
127 $data .= '</textarea>';
128
129 return wp_kses_post($data);
130 }
131
132 /**
133 * @param wpdb $wpdb
134 * @return void
135 */
136 public function setDB(wpdb $wpdb)
137 {
138 $this->wpdb = $wpdb;
139 }
140
141 /**
142 * @param string $grantString
143 * @return bool
144 */
145 private function hasGrantForCurrentDatabase(string $grantString): bool
146 {
147 $dbName = $this->wpdb->dbname;
148 // Global privileges (applies to all databases)
149 if (stripos($grantString, '*.*') !== false) {
150 return true;
151 }
152
153 // Database-specific grants - handle various formats:
154 // `dbname`.*, "dbname".*, dbname.*, ON `dbname`.*, etc.
155 // Also handle escaped database names like `web30\_db4`.*
156 $escapedDbName = str_replace('_', '\\_', $dbName);
157
158 $patterns = [
159 '/\bON\s+\*\.\*/i', // Global: ON *.*
160 '/\bON\s+`' . preg_quote($dbName, '/') . '`\.\*/i', // Quoted: ON `dbname`.*
161 '/\bON\s+`' . preg_quote($escapedDbName, '/') . '`\.\*/i', // Quoted with escaped underscores: ON `db\_name`.*
162 '/\bON\s+"' . preg_quote($dbName, '/') . '"\.\*/i', // Double-quoted: ON "dbname".*
163 '/\bON\s+"' . preg_quote($escapedDbName, '/') . '"\.\*/i', // Double-quoted with escaped underscores: ON "db\_name".*
164 '/\bON\s+' . preg_quote($dbName, '/') . '\.\*/i', // Unquoted: ON dbname.*
165 '/\bON\s+' . preg_quote($escapedDbName, '/') . '\.\*/i', // Unquoted with escaped underscores: ON db\_name.*
166 '/`' . preg_quote($dbName, '/') . '`\.\*/i', // Direct reference: `dbname`.*
167 '/`' . preg_quote($escapedDbName, '/') . '`\.\*/i', // Direct reference with escaped underscores: `db\_name`.*
168 '/"' . preg_quote($dbName, '/') . '"\.\*/i', // Direct double quoted: "dbname".*
169 '/"' . preg_quote($escapedDbName, '/') . '"\.\*/i', // Direct double quoted with escaped underscores: "db\_name".*
170 '/\b' . preg_quote($dbName, '/') . '\.\*/i', // Direct unquoted: dbname.*
171 '/\b' . preg_quote($escapedDbName, '/') . '\.\*/i', // Direct unquoted with escaped underscores: db\_name.*
172 ];
173 foreach ($patterns as $pattern) {
174 if (preg_match($pattern, $grantString)) {
175 return true;
176 }
177 }
178
179 return false;
180 }
181
182 /**
183 * @param string $grantString
184 * @return bool
185 */
186 private function hasAllPrivileges(string $grantString): bool
187 {
188 return preg_match('/\bGRANT\s+ALL(\s+PRIVILEGES)?\b/i', $grantString) === 1; // Match both "ALL" and "ALL PRIVILEGES" formats
189 }
190
191 /**
192 * @param string $grantString
193 * @param array $grantsToCheck
194 * @return bool
195 */
196 private function hasRequiredPermissions(string $grantString, array $grantsToCheck): bool
197 {
198 if (!preg_match('/GRANT\s+(.*?)\s+ON\s+/i', $grantString, $matches)) {
199 return false;
200 }
201
202 $permissionsString = strtoupper(trim($matches[1]));
203 $permissionsString = preg_replace('/\s*,\s*/', ',', $permissionsString);
204 $grantedPermissions = array_filter(array_map('trim', explode(',', $permissionsString))); // Split and normalize granted permissions
205 $grantedPermissionsAssoc = array_flip($grantedPermissions); // Convert granted permissions to associative array for O(1) lookups
206
207 // Check each required permission
208 foreach ($grantsToCheck as $requiredPermission) {
209 $requiredPermission = strtoupper(trim($requiredPermission));
210 if (!in_array($requiredPermission, $grantedPermissions, true)) {
211 return false;
212 }
213 }
214
215 return true;
216 }
217 }
218