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 / Settings / Settings.php
wp-staging / Framework / Settings Last commit date
DarkMode.php 7 months ago Settings.php 2 months ago SettingsTable.php 2 months ago
Settings.php
382 lines
1 <?php
2
3 namespace WPStaging\Framework\Settings;
4
5 use WPStaging\Core\WPStaging;
6 use WPStaging\Framework\BackgroundProcessing\FeatureDetection;
7 use WPStaging\Framework\BackgroundProcessing\Queue;
8 use WPStaging\Framework\Network\HttpBasicAuth;
9 use WPStaging\Framework\SiteInfo;
10 use WPStaging\Framework\Utils\Sanitize;
11 use WPStaging\Backup\BackupScheduler;
12 use WPStaging\Framework\Security\Auth;
13 use WPStaging\Framework\Security\DataEncryption;
14 use WPStaging\Notifications\Notifications;
15
16 /**
17 * This class provides functionality for managing application settings.
18 */
19 class Settings
20 {
21 use HttpBasicAuth;
22
23 /** @var string */
24 const ACTION_WPSTG_PRO_SETTINGS = 'wpstg.views.pro.settings';
25
26 /**
27 * @var array
28 * Sanitize the options to escape from XSS
29 */
30 private $optionsToSanitize = [
31 'queryLimit' => 'sanitizeInt',
32 'querySRLimit' => 'sanitizeInt',
33 'fileLimit' => 'sanitizeInt',
34 'maxFileSize' => 'sanitizeInt',
35 'batchSize' => 'sanitizeInt',
36 'delayRequest' => 'sanitizeInt',
37 'cpuLoad' => 'sanitizeString',
38 'unInstallOnDelete' => 'sanitizeBool',
39 'optimizer' => 'sanitizeBool',
40 'disableAdminLogin' => 'sanitizeBool',
41 'keepPermalinks' => 'sanitizeBool',
42 'debugMode' => 'sanitizeBool',
43 ];
44
45 /**
46 * @var SiteInfo
47 */
48 private $siteInfo;
49
50 /**
51 * @var Sanitize
52 */
53 private $sanitize;
54
55 /** @var Queue */
56 private $queue;
57
58 /** @var Auth */
59 private $auth;
60
61 /** @var DataEncryption */
62 private $dataEncryption;
63
64 /**
65 * @param SiteInfo $siteInfo
66 * @param Sanitize $sanitize
67 * @param Queue $queue
68 * @param Auth $auth
69 * @param DataEncryption $dataEncryption
70 */
71 public function __construct(SiteInfo $siteInfo, Sanitize $sanitize, Queue $queue, Auth $auth, DataEncryption $dataEncryption)
72 {
73 $this->siteInfo = $siteInfo;
74 $this->sanitize = $sanitize;
75 $this->queue = $queue;
76 $this->auth = $auth;
77 $this->dataEncryption = $dataEncryption;
78 }
79
80 /**
81 * @return void
82 */
83 public function registerSettings()
84 {
85 register_setting("wpstg_settings", "wpstg_settings", [$this, "sanitizeOptions"]);
86 }
87
88 /**
89 * Sanitize options data and delete the cache
90 * @param array $data
91 * @return array
92 */
93 public function sanitizeOptions(array $data = []): array
94 {
95 // is_array() is required otherwise new clone will fail.
96 $showErrorToggleStagingSiteCloning = false;
97 if ($this->siteInfo->isStagingSite() && is_array($data)) {
98 $isStagingCloneable = isset($data['isStagingSiteCloneable']) ? $data['isStagingSiteCloneable'] : 'false';
99 unset($data['isStagingSiteCloneable']);
100 $showErrorToggleStagingSiteCloning = !$this->toggleStagingSiteCloning($isStagingCloneable === 'true');
101 }
102
103 if (is_array($data)) {
104 $optionBackupScheduleErrorReport = isset($data['schedulesErrorReport']) ? 'true' : '';
105 $optionBackupScheduleWarningReport = isset($data['schedulesWarningReport']) ? 'true' : '';
106 $optionBackupScheduleGeneralReport = isset($data['schedulesGeneralReport']) ? 'true' : '';
107 $optionBackupScheduleReportEmail = !empty($data['schedulesReportEmail']) ? $this->sanitize->sanitizeEmail($data['schedulesReportEmail']) : '';
108
109 if (empty($optionBackupScheduleReportEmail)) {
110 $optionBackupScheduleErrorReport = '';
111 }
112
113 unset($data['schedulesErrorReport'], $data['schedulesReportEmail']);
114
115 $optionBackupScheduleSlackErrorReport = isset($data['schedulesSlackErrorReport']) ? 'true' : '';
116 $optionBackupScheduleReportSlackWebhook = !empty($data['schedulesReportSlackWebhook']) ? $this->sanitize->sanitizeUrl($data['schedulesReportSlackWebhook']) : '';
117 $optionSendEmailAsHTML = isset($data['emailAsHTML']) ? 'true' : '';
118
119 if (empty($optionBackupScheduleReportSlackWebhook)) {
120 $optionBackupScheduleSlackErrorReport = '';
121 }
122
123 unset($data['schedulesErrorSlackReport'], $data['schedulesReportSlackWebhook']);
124
125 $this->setErrorReportOptions(
126 $optionBackupScheduleErrorReport,
127 $optionBackupScheduleWarningReport,
128 $optionBackupScheduleGeneralReport,
129 $optionBackupScheduleReportEmail,
130 $optionBackupScheduleSlackErrorReport,
131 $optionBackupScheduleReportSlackWebhook,
132 $optionSendEmailAsHTML
133 );
134
135 $this->saveHttpAuthCredentials($data);
136 unset($data['httpAuthUsername'], $data['httpAuthPassword']);
137 }
138
139 $sanitized = $this->sanitizeData($data);
140
141 if ($showErrorToggleStagingSiteCloning) {
142 add_settings_error("wpstg-notices", '', __("Settings updated. But unable to activate/deactivate the site cloneable status!", "wp-staging"), "warning");
143 } else {
144 add_settings_error("wpstg-notices", '', __("Settings updated.", "wp-staging"), "updated");
145 }
146
147 return $sanitized;
148 }
149
150 /**
151 * @return null
152 */
153 public function ajaxPurgeQueueTable()
154 {
155 if ($this->auth->isAuthenticatedRequest() === false) {
156 wp_send_json([
157 'success' => false,
158 'message' => esc_html__('Error 403: Unauthorized Request', 'wp-staging'),
159 ]);
160 }
161
162 $result = $this->queue->purgeQueueTable();
163
164 if ($result === false) {
165 wp_send_json([
166 'success' => false,
167 'message' => esc_html__('Unable to purge queue table', 'wp-staging'),
168 ]);
169 }
170
171 if ($result === 0) {
172 wp_send_json([
173 'success' => true,
174 'message' => sprintf(esc_html__('Table %s is already empty.', 'wp-staging'), esc_html($this->queue->getTableName())),
175 ]);
176 }
177
178 wp_send_json([
179 'success' => true,
180 'message' => sprintf(esc_html__('Purged queue table! Removed %s action(s)', 'wp-staging'), esc_html((string)$result)),
181 ]);
182
183 return null;
184 }
185
186 /**
187 * Lightweight AJAX action that serves as the loopback target for HTTP Basic Auth testing.
188 * Returns a simple success response so the caller can verify the request went through.
189 *
190 * No auth check: this endpoint is intentionally public (wp_ajax_nopriv_) so the
191 * server-side loopback in ajaxTestHttpAuth() can reach it without a WP session.
192 *
193 * @return void
194 */
195 public function ajaxHttpAuthPing()
196 {
197 wp_send_json_success(['ping' => true]);
198 }
199
200 /**
201 * Tests that the saved HTTP Basic Auth credentials allow loopback requests to admin-ajax.php.
202 * Performs a wp_remote_post() to the ping action using the stored credentials.
203 *
204 * @return void
205 */
206 public function ajaxTestHttpAuth()
207 {
208 if ($this->auth->isAuthenticatedRequest() === false) {
209 wp_send_json_error(['message' => esc_html__('Error 403: Unauthorized Request', 'wp-staging')]);
210 return;
211 }
212
213 $headers = $this->getHttpAuthHeaders();
214 if (empty($headers)) {
215 wp_send_json_error(['message' => esc_html__('No HTTP Basic Auth credentials are saved yet. Save your settings first, then test the connection.', 'wp-staging')]);
216 return;
217 }
218
219 $url = admin_url('admin-ajax.php');
220
221 $response = wp_remote_post($url, [
222 'timeout' => 15,
223 'sslverify' => apply_filters(FeatureDetection::FILTER_HTTPS_LOCAL_SSL_VERIFY, false),
224 'headers' => $headers,
225 'body' => [
226 'action' => 'wpstg_http_auth_ping',
227 ],
228 ]);
229
230 if (is_wp_error($response)) {
231 wp_send_json_error([
232 'message' => sprintf(
233 esc_html__('Connection failed: %s', 'wp-staging'),
234 esc_html($response->get_error_message())
235 ),
236 ]);
237 return;
238 }
239
240 $statusCode = wp_remote_retrieve_response_code($response);
241 $body = json_decode(wp_remote_retrieve_body($response), true);
242
243 if ($statusCode === 401) {
244 wp_send_json_error(['message' => esc_html__('Authentication failed (401). The username or password is incorrect.', 'wp-staging')]);
245 return;
246 }
247
248 if ($statusCode === 403) {
249 wp_send_json_error(['message' => esc_html__('Access denied (403). The request was blocked, possibly by a firewall or security plugin.', 'wp-staging')]);
250 return;
251 }
252
253 if ($statusCode !== 200 || empty($body['success'])) {
254 wp_send_json_error([
255 'message' => sprintf(
256 esc_html__('Unexpected response (HTTP %s). The loopback request did not succeed.', 'wp-staging'),
257 esc_html((string)$statusCode)
258 ),
259 ]);
260 return;
261 }
262
263 wp_send_json_success(['message' => esc_html__('Connection successful! Background tasks will be able to reach wp-admin.', 'wp-staging')]);
264 }
265
266 /**
267 * @param array $data
268 * @return array
269 */
270 protected function sanitizeData(array $data = []): array
271 {
272 $sanitized = [];
273
274 foreach ($data as $key => $value) {
275 if (is_array($value)) {
276 $sanitized[$key] = $this->sanitizeData($value);
277 continue;
278 }
279
280 $val = htmlspecialchars($value);
281 if (array_key_exists($key, $this->optionsToSanitize)) {
282 $sanitizeMethod = $this->optionsToSanitize[$key];
283 $val = $this->sanitize->$sanitizeMethod($val);
284 }
285
286 $sanitized[$key] = wp_filter_nohtml_kses($val);
287 }
288
289 return $sanitized;
290 }
291
292 /**
293 * Toggle staging site cloning
294 *
295 * @param bool $isCloneable
296 *
297 * @return bool
298 */
299 protected function toggleStagingSiteCloning(bool $isCloneable): bool
300 {
301 if ($isCloneable && $this->siteInfo->enableStagingSiteCloning()) {
302 return true;
303 }
304
305 if (!$isCloneable && $this->siteInfo->disableStagingSiteCloning()) {
306 return true;
307 }
308
309 return false;
310 }
311
312 /**
313 * Set backup schedule error reporting options
314 *
315 * @param string $optionBackupScheduleErrorReport 'true' if active
316 * @param string $optionBackupScheduleWarningReport 'true' if active
317 * @param string $optionBackupScheduleGeneralReport 'true' if active
318 * @param string $optionBackupScheduleReportEmail
319 * @param string $optionBackupScheduleSlackErrorReport 'true' if active
320 * @param string $optionBackupScheduleReportSlackWebhook
321 * @param string $optionSendEmailAsHTML 'true' if active
322 * @return void
323 */
324 protected function setErrorReportOptions(
325 string $optionBackupScheduleErrorReport,
326 string $optionBackupScheduleWarningReport,
327 string $optionBackupScheduleGeneralReport,
328 string $optionBackupScheduleReportEmail,
329 string $optionBackupScheduleSlackErrorReport,
330 string $optionBackupScheduleReportSlackWebhook,
331 string $optionSendEmailAsHTML
332 ) {
333 if (!class_exists('WPStaging\Backup\BackupScheduler')) {
334 return;
335 }
336
337 update_option(BackupScheduler::OPTION_BACKUP_SCHEDULE_ERROR_REPORT, $optionBackupScheduleErrorReport, false);
338 update_option(BackupScheduler::OPTION_BACKUP_SCHEDULE_WARNING_REPORT, $optionBackupScheduleWarningReport, false);
339 update_option(BackupScheduler::OPTION_BACKUP_SCHEDULE_GENERAL_REPORT, $optionBackupScheduleGeneralReport, false);
340 update_option(Notifications::OPTION_BACKUP_SCHEDULE_REPORT_EMAIL, $optionBackupScheduleReportEmail);
341 update_option(BackupScheduler::OPTION_BACKUP_SCHEDULE_SLACK_ERROR_REPORT, $optionBackupScheduleSlackErrorReport, false);
342 update_option(BackupScheduler::OPTION_BACKUP_SCHEDULE_REPORT_SLACK_WEBHOOK, $optionBackupScheduleReportSlackWebhook, false);
343 update_option(Notifications::OPTION_SEND_EMAIL_AS_HTML, $optionSendEmailAsHTML);
344 }
345
346 /**
347 * Save HTTP Basic Auth credentials for loopback requests.
348 * If username is empty, both username and password are cleared.
349 * If password is blank and username is provided, existing password is preserved.
350 *
351 * @param array $data
352 * @return void
353 */
354 protected function saveHttpAuthCredentials(array $data)
355 {
356 $username = isset($data['httpAuthUsername'])
357 ? $this->sanitize->sanitizeString($data['httpAuthUsername'])
358 : '';
359
360 if (empty($username)) {
361 update_option(Queue::OPTION_HTTP_AUTH_CREDENTIALS, ['username' => '', 'password' => ''], false);
362 return;
363 }
364
365 $submittedPassword = isset($data['httpAuthPassword'])
366 ? $this->sanitize->sanitizePassword($data['httpAuthPassword'])
367 : '';
368
369 if (!empty($submittedPassword)) {
370 $password = $this->dataEncryption->encrypt($submittedPassword);
371 } else {
372 $existing = get_option(Queue::OPTION_HTTP_AUTH_CREDENTIALS, []);
373 $password = !empty($existing['password']) ? $existing['password'] : '';
374 }
375
376 update_option(Queue::OPTION_HTTP_AUTH_CREDENTIALS, [
377 'username' => $username,
378 'password' => $password,
379 ], false);
380 }
381 }
382