PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.8.0
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.8.0
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 / Backend / Modules / SystemInfo.php
wp-staging / Backend / Modules Last commit date
Jobs 2 months ago Views 3 months ago SystemInfo.php 4 months ago SystemInfoParser.php 4 months ago
SystemInfo.php
1188 lines
1 <?php
2
3 namespace WPStaging\Backend\Modules;
4
5 use WPStaging\Backend\Upgrade\Upgrade;
6 use WPStaging\Backup\Ajax\FileList\ListableBackupsCollection;
7 use WPStaging\Core\Utils\Browser;
8 use WPStaging\Core\WPStaging;
9 use WPStaging\Core\Utils\Multisite;
10 use WPStaging\Framework\Facades\Info;
11 use WPStaging\Framework\Utils\Urls;
12 use WPStaging\Framework\Adapter\Database;
13 use WPStaging\Framework\BackgroundProcessing\Queue;
14 use WPStaging\Framework\Facades\Sanitize;
15 use WPStaging\Notifications\Notifications;
16 use WPStaging\Staging\Sites;
17 use WPStaging\Framework\SiteInfo;
18 use WPStaging\Framework\Database\WpOptionsInfo;
19 use WPStaging\Framework\Security\DataEncryption;
20 use WPStaging\Backup\BackupScheduler;
21
22 // No Direct Access
23 if (!defined("WPINC")) {
24 die;
25 }
26
27 /**
28 * System Info
29 * Generates system information for debugging and support
30 */
31 class SystemInfo
32 {
33 /**
34 * @var string
35 */
36 const REMOVED_LABEL = '[REMOVED]';
37
38 /**
39 * @var string
40 */
41 const NOT_SET_LABEL = '[not set]';
42
43 /**
44 * @var bool
45 */
46 private $isMultiSite;
47
48 /**
49 * @var mixed|Database
50 */
51 private $database;
52
53 /**
54 * @var Urls
55 */
56 private $urlsHelper;
57
58 /**
59 * @var WpOptionsInfo
60 */
61 private $wpOptionsInfo;
62
63 /**
64 * @var bool
65 */
66 private $isEncodeProLicense = false;
67
68 /** @var SiteInfo */
69 private $siteInfo;
70
71 /**
72 * @var bool Enable structured data output
73 */
74 private $enableStructuredOutput = false;
75
76 /**
77 * @var array Structured data storage
78 */
79 private $structuredData = [];
80
81 /**
82 * @var string Current section name
83 */
84 private $currentSection = null;
85
86 public function __construct()
87 {
88 $this->isMultiSite = is_multisite();
89 $this->urlsHelper = WPStaging::make(Urls::class);
90 $this->database = WPStaging::make(Database::class);
91 $this->wpOptionsInfo = WPStaging::make(WpOptionsInfo::class);
92 $this->siteInfo = WPStaging::make(SiteInfo::class);
93 }
94
95 public function __toString(): string
96 {
97 return $this->get();
98 }
99
100 public function setStructuredOutput(bool $enable = true)
101 {
102 $this->enableStructuredOutput = $enable;
103 if ($enable) {
104 $this->structuredData = [];
105 }
106 }
107
108 public function getSections(): array
109 {
110 $this->setStructuredOutput(true);
111 $this->get(); // This will populate structuredData
112 return $this->getStructuredDataWithDisplayNames();
113 }
114
115 public function get(): string
116 {
117 $output = $this->server();
118 $output .= $this->php();
119 $output .= $this->wp();
120 $output .= $this->getMultisiteInfo();
121 $output .= $this->wpstaging();
122 $output .= $this->plugins();
123 $output .= $this->muPlugins();
124 $output .= $this->dropIns();
125 $output .= $this->multiSitePlugins();
126 $output .= $this->phpExtensions();
127 $output .= $this->browser();
128 $output .= PHP_EOL . "### End System Info ###";
129
130 return $output;
131 }
132
133 public function header(string $string): string
134 {
135 $this->currentSection = $string;
136 return PHP_EOL . "### {$string} ###" . PHP_EOL . PHP_EOL;
137 }
138
139 /**
140 * @param string|array $value
141 */
142 public function info(string $title, $value): string
143 {
144 // Store structured data if enabled
145 if ($this->enableStructuredOutput) {
146 if (!isset($this->structuredData[$this->currentSection])) {
147 $this->structuredData[$this->currentSection] = [];
148 }
149
150 $this->structuredData[$this->currentSection][] = [
151 'label' => rtrim($title, ':'),
152 'value' => $value,
153 ];
154 }
155
156 return str_pad($title, 56, ' ', STR_PAD_RIGHT) . print_r($value, true) . PHP_EOL;
157 }
158
159 /**
160 * Get structured data with display names
161 *
162 * @return array Structured data with section display names as keys
163 */
164 public function getStructuredDataWithDisplayNames(): array
165 {
166 $data = [];
167 foreach ($this->structuredData as $sectionId => $items) {
168 $displayName = SystemInfoParser::getDisplayName($sectionId);
169 $data[$displayName] = $items;
170 }
171
172 return $data;
173 }
174
175 /**
176 * WordPress Configuration
177 * @return string
178 */
179 public function wp(): string
180 {
181 // WordPress Environment
182 $this->currentSection = SystemInfoParser::SECTIONS['WORDPRESS_ENVIRONMENT']['id'];
183 $output = $this->info("Site Type:", ($this->isMultiSite) ? 'Multi Site' : 'Single Site');
184 $output .= $this->info("WordPress Version:", get_bloginfo("version"));
185 $output .= $this->info("Installed in Subdirectory:", ($this->isSubDir() ? 'Yes' : 'No'));
186 $output .= $this->info("WP_DEBUG:", (defined("WP_DEBUG")) ? (WP_DEBUG ? "Enabled" : "Disabled") : self::NOT_SET_LABEL);
187 $output .= $this->info("WPLANG:", (defined("WPLANG") && WPLANG) ? WPLANG : "en_US");
188 $output .= $this->wpRemotePost();
189
190 // URLs & Paths
191 $this->currentSection = SystemInfoParser::SECTIONS['URLS_PATHS']['id'];
192 $output .= $this->info("Site URL:", site_url());
193 $output .= $this->info("Home URL:", $this->urlsHelper->getHomeUrl());
194 $output .= $this->info("Home Path:", get_home_path());
195 $output .= $this->info("ABSPATH:", ABSPATH);
196 $permissions = fileperms(ABSPATH);
197 if ($permissions !== false) {
198 $output .= $this->info("ABSPATH Fileperms:", (string)$permissions);
199 $permissions = substr(sprintf('%o', $permissions), -4);
200 $output .= $this->info("ABSPATH Permissions:", $permissions);
201 } else {
202 $output .= $this->info("ABSPATH Permissions:", "N/A");
203 }
204
205 $absPathStat = stat(ABSPATH);
206 if (!$absPathStat) {
207 $absPathStat = "";
208 }
209
210 if ($this->enableStructuredOutput) {
211 $output .= $this->info("ABSPATH Stat:", $absPathStat);
212 } else {
213 $output .= $this->info("ABSPATH Stat:", json_encode($absPathStat));
214 }
215
216 // WordPress Directories
217 $this->currentSection = SystemInfoParser::SECTIONS['WORDPRESS_DIRECTORIES']['id'];
218 $output .= $this->constantInfo('WP_CONTENT_DIR');
219 $output .= $this->constantInfo('WP_PLUGIN_DIR');
220 $output .= $this->info("Is wp-content Symlink:", is_link(WP_CONTENT_DIR) ? 'Yes' : 'No');
221 $output .= $this->info("Symlinks Disabled:", Info::canUse('symlink') === false ? 'Yes' : 'No');
222 if (is_link(WP_CONTENT_DIR)) {
223 $output .= $this->info("wp-content link target:", readlink(WP_CONTENT_DIR));
224 $output .= $this->info("wp-content realpath:", realpath(WP_CONTENT_DIR));
225 }
226
227 $output .= $this->constantInfo('WP_TEMP_DIR');
228
229 // Media & Uploads
230 $this->currentSection = SystemInfoParser::SECTIONS['MEDIA_UPLOADS']['id'];
231 $output .= $this->constantInfo('UPLOADS');
232 $uploads = wp_upload_dir();
233 $output .= $this->info("Uploads Base Dir:", $uploads['basedir']);
234 $output .= $this->info("Uploads URL:", $uploads['url']);
235 $output .= $this->info("Uploads Path:", $uploads['path']);
236 $output .= $this->info("Uploads Subdir:", $uploads['subdir']);
237 $output .= $this->info("Uploads Base URL:", $uploads['baseurl']);
238 $output .= $this->info("UPLOADS Constant:", (defined("UPLOADS")) ? UPLOADS : self::NOT_SET_LABEL);
239 $output .= $this->info("UPLOAD_PATH (wp-config.php):", (defined("UPLOAD_PATH")) ? UPLOAD_PATH : self::NOT_SET_LABEL);
240 $tableName = $this->database->getPrefix() . 'options';
241 $output .= $this->info("upload_path ($tableName):", get_option("upload_path") ?: self::NOT_SET_LABEL);
242
243 // WordPress Memory Settings
244 $this->currentSection = SystemInfoParser::SECTIONS['WORDPRESS_MEMORY_SETTINGS']['id'];
245 $output .= $this->constantInfo('WP_MEMORY_LIMIT');
246 $output .= $this->constantInfo('WP_MAX_MEMORY_LIMIT');
247
248 // Filesystem & Permissions
249 $this->currentSection = SystemInfoParser::SECTIONS['FILESYSTEM_PERMISSIONS']['id'];
250 $output .= $this->constantInfo('FS_CHMOD_DIR');
251 $output .= $this->constantInfo('FS_CHMOD_FILE');
252
253 // Theme & Permalinks
254 $this->currentSection = SystemInfoParser::SECTIONS['THEME_PERMALINKS']['id'];
255 $settings = (object)get_option('wpstg_settings', []);
256 $output .= $this->info("Active Theme:", $this->theme());
257 $output .= $this->info("Permalink Structure:", get_option("permalink_structure") ?: "Default");
258 $output .= $this->info("Keep Permalinks:", isset($settings->keepPermalinks) ? $settings->keepPermalinks : self::NOT_SET_LABEL);
259
260 // WordPress Cron Jobs
261 $this->currentSection = SystemInfoParser::SECTIONS['WORDPRESS_CRON_JOBS']['id'];
262 $cron = get_option('cron', []);
263 $output .= $this->info("WP-Cron Enabled:", (!defined('DISABLE_WP_CRON') || !DISABLE_WP_CRON) ? 'Yes' : 'No');
264 $output .= $this->info("Scheduled Events:", !empty($cron) ? count($cron) . ' events' : 'No events');
265 if ($this->enableStructuredOutput) {
266 $output .= $this->info("Wordpress cron:", $cron);
267 } else {
268 $output .= $this->info("Wordpress cron:", wp_json_encode($cron));
269 }
270
271 $backupSchedules = get_option('wpstg_backup_schedules', []);
272 if (!empty($backupSchedules)) {
273 $output .= $this->info("Backup Schedule:", $backupSchedules);
274 } else {
275 $output .= $this->info('Backup Schedule:', self::NOT_SET_LABEL);
276 }
277
278 return $output;
279 }
280
281 /**
282 * Theme Information
283 * @return string
284 */
285 public function theme(): string
286 {
287 // Versions earlier than 3.4
288 if (get_bloginfo("version") < "3.4") {
289 $themeData = get_theme_data(get_stylesheet_directory() . "/style.css");
290 return "{$themeData["Name"]} (v{$themeData["Version"]})";
291 }
292
293 $themeData = wp_get_theme();
294 return "{$themeData->Name} (v{$themeData->Version})";
295 }
296
297 /**
298 * Multisite information
299 * @return string
300 */
301 private function getMultisiteInfo(): string
302 {
303 if (!$this->isMultiSite) {
304 return '';
305 }
306
307 $this->currentSection = SystemInfoParser::SECTIONS['MULTISITE']['id'];
308 $multisite = new Multisite();
309
310 $output = $this->info("Multisite:", "Yes");
311 $output .= $this->info("Multisite Blog ID:", (string)get_current_blog_id());
312 $output .= $this->info("MultiSite URL:", $multisite->getHomeURL());
313 $output .= $this->info("MultiSite URL without scheme:", $multisite->getHomeUrlWithoutScheme());
314 $output .= $this->info("MultiSite is Main Site:", is_main_site() ? 'Yes' : 'No');
315
316 $output .= $this->constantInfo('SUBDOMAIN_INSTALL');
317 $output .= $this->constantInfo('DOMAIN_CURRENT_SITE');
318 $output .= $this->constantInfo('PATH_CURRENT_SITE');
319 $output .= $this->constantInfo('SITE_ID_CURRENT_SITE');
320 $output .= $this->constantInfo('BLOG_ID_CURRENT_SITE');
321
322 $networkSites = get_sites();
323 if ($this->enableStructuredOutput) {
324 $output .= $this->info("Network Sites", $networkSites);
325 return $output;
326 }
327
328 $output .= PHP_EOL . $this->info("Network Sites:", count($networkSites)) . PHP_EOL;
329 foreach ($networkSites as $site) {
330 $siteDetails = get_blog_details($site->blog_id);
331 if (!$siteDetails) {
332 continue;
333 }
334
335 $output .= $this->info("Blog ID:", $site->blog_id);
336 $output .= $this->info("Home URL:", get_home_url($site->blog_id));
337 $output .= $this->info("Site URL:", get_site_url($site->blog_id));
338 $output .= $this->info("Domain:", $site->domain);
339 $output .= $this->info("Path:", $site->path);
340 $output .= PHP_EOL;
341 }
342
343 return $output;
344 }
345
346 /**
347 * Wp Staging plugin Information
348 * @return string
349 */
350 public function wpstaging(): string
351 {
352 $settings = (object)get_option('wpstg_settings', []);
353 $optionBackupScheduleErrorReport = get_option(BackupScheduler::OPTION_BACKUP_SCHEDULE_ERROR_REPORT);
354 $optionBackupScheduleWarningReport = get_option(BackupScheduler::OPTION_BACKUP_SCHEDULE_WARNING_REPORT);
355 $optionBackupScheduleGeneralReport = get_option(BackupScheduler::OPTION_BACKUP_SCHEDULE_GENERAL_REPORT);
356 $optionBackupScheduleReportEmail = get_option(Notifications::OPTION_BACKUP_SCHEDULE_REPORT_EMAIL);
357 $optionBackupScheduleSlackErrorReport = get_option(BackupScheduler::OPTION_BACKUP_SCHEDULE_SLACK_ERROR_REPORT);
358 $optionBackupScheduleReportSlackWebhook = get_option(BackupScheduler::OPTION_BACKUP_SCHEDULE_REPORT_SLACK_WEBHOOK);
359 $wpStagingFreeVersion = wpstgGetPluginData('wp-staging.php');
360 $output = PHP_EOL . "## WP Staging ##" . PHP_EOL . PHP_EOL;
361
362 // WP Staging – Plugin Information
363 $this->currentSection = SystemInfoParser::SECTIONS['WP_STAGING_PLUGIN_INFO']['id'];
364 $output .= $this->info("Pro License Key:", $this->getLicenseKey() ?: self::NOT_SET_LABEL);
365 $output .= $this->info("Pro Version:", get_option('wpstgpro_version', self::NOT_SET_LABEL));
366 $output .= $this->info("Pro Install Date:", get_option('wpstgpro_install_date', self::NOT_SET_LABEL));
367 $output .= $this->info("Updated from Pro Version:", get_option('wpstgpro_version_upgraded_from') ?: self::NOT_SET_LABEL);
368 $output .= $this->info("Pro Update Date:", get_option('wpstgpro_upgrade_date', self::NOT_SET_LABEL));
369 $output .= $this->info("Free Version:", empty($wpStagingFreeVersion['Version']) ? self::NOT_SET_LABEL : $wpStagingFreeVersion['Version']);
370 $output .= $this->info("Free Install Date:", get_option(Upgrade::OPTION_INSTALL_DATE, self::NOT_SET_LABEL));
371 $output .= $this->info("Free Update Date:", get_option(Upgrade::OPTION_UPGRADE_DATE, self::NOT_SET_LABEL));
372 $output .= $this->info("Updated from Free Version:", get_option('wpstg_version_upgraded_from') ?: self::NOT_SET_LABEL);
373 $output .= $this->info("Free or Pro Install Date (legacy):", get_option('wpstg_installDate', self::NOT_SET_LABEL));
374 $output .= $this->info("Is Staging Site:", $this->siteInfo->isStagingSite() ? 'Yes' : 'No');
375
376 $this->currentSection = SystemInfoParser::SECTIONS['WP_STAGING_BACKUP_STATUS']['id'];
377 $output .= $this->getBackupDetails();
378 $output .= $this->getQueueInfo();
379
380 // WP Staging – Performance & Limits
381 $this->currentSection = SystemInfoParser::SECTIONS['WP_STAGING_PERFORMANCE']['id'];
382 $output .= $this->info("DB Query Limit:", $this->getSettingValue($settings, 'queryLimit', true));
383 $output .= $this->info("Search & Replace Limit:", $this->getSettingValue($settings, 'querySRLimit', true));
384 $output .= $this->info("File Copy Limit:", $this->getSettingValue($settings, 'fileLimit', true));
385 $output .= $this->info("Maximum File Size:", $this->getSettingValue($settings, 'maxFileSize'));
386 $output .= $this->info("File Copy Batch Size:", $this->getSettingValue($settings, 'batchSize'));
387 $cpuLoad = $this->getSettingValue($settings, 'cpuLoad');
388 $output .= $this->info("CPU Load Priority:", $cpuLoad !== self::NOT_SET_LABEL ? ucfirst(strtolower($cpuLoad)) : $cpuLoad);
389 $output .= $this->info("Optimizer Enabled:", isset($settings->optimizer) && $settings->optimizer ? 'Yes' : 'No');
390 $output .= $this->info("Backup Compression:", isset($settings->enableCompression) ? ($settings->enableCompression ? 'On' : 'Off') : self::NOT_SET_LABEL);
391 $output .= $this->info("Debug Mode Enabled:", isset($settings->debugMode) && $settings->debugMode ? 'Yes' : 'No');
392 // WP Staging – Access & Permissions
393 $this->currentSection = SystemInfoParser::SECTIONS['WP_STAGING_ACCESS']['id'];
394 $userRoles = isset($settings->userRoles) ? $settings->userRoles : [];
395 if (is_array($userRoles) && !empty($userRoles)) {
396 $rolesList = implode(', ', array_map('ucfirst', $userRoles));
397 } else {
398 $rolesList = self::NOT_SET_LABEL;
399 }
400
401 $output .= $this->info("Allowed Roles:", $rolesList);
402 $usersWithAccess = isset($settings->usersWithStagingAccess) ? $settings->usersWithStagingAccess : [];
403 if (is_array($usersWithAccess) && !empty($usersWithAccess)) {
404 $usersList = implode(', ', $usersWithAccess);
405 } else {
406 $usersList = 'Not listed';
407 }
408
409 $output .= $this->info("Users with Staging Access:", $usersList);
410 $output .= $this->info("Delete Data on Uninstall:", isset($settings->unInstallOnDelete) ? ($settings->unInstallOnDelete ? 'Yes' : 'No') : self::NOT_SET_LABEL);
411 $output .= $this->info("Send Usage Information:", !empty(get_option('wpstg_analytics_has_consent')) ? 'true' : 'false');
412 $output .= $this->info("Send Backup Errors via Email:", $this->formatBooleanOption($optionBackupScheduleErrorReport));
413 $output .= $this->info("Send Backup Warnings via Email:", $this->formatBooleanOption($optionBackupScheduleWarningReport));
414 $output .= $this->info("Send Backup General Report via Email:", $this->formatBooleanOption($optionBackupScheduleGeneralReport));
415 $output .= $this->info("Email Address:", !empty($optionBackupScheduleReportEmail) && is_email($optionBackupScheduleReportEmail) ? $optionBackupScheduleReportEmail : self::NOT_SET_LABEL);
416 $output .= $this->info("Send Backup Errors via Slack Webhook:", $this->formatBooleanOption($optionBackupScheduleSlackErrorReport) === 'true' ? 'true' : (WPStaging::isPro() ? 'false' : self::NOT_SET_LABEL));
417 $output .= $this->info("Slack Webhook URL:", WPStaging::isPro() && !empty($optionBackupScheduleReportSlackWebhook) ? self::REMOVED_LABEL : self::NOT_SET_LABEL);
418
419 $this->currentSection = SystemInfoParser::SECTIONS['WP_STAGING_STORAGE_PROVIDER']['id'];
420 // Use consolidated storage provider configuration
421 $parser = WPStaging::make(SystemInfoParser::class);
422 $storageProviders = $parser->getStorageProvidersForSystemInfo();
423 foreach ($storageProviders as $provider) {
424 $output .= $this->formatStorageSettings($provider['optionName'], $provider['title']);
425 }
426
427 $this->currentSection = SystemInfoParser::SECTIONS['WP_STAGING_EXISTING_SITES']['id'];
428 if (!$this->enableStructuredOutput) {
429 $output .= PHP_EOL . "-- Existing Staging Sites" . PHP_EOL . PHP_EOL;
430 }
431
432 $stagingSites = get_option(Sites::STAGING_SITES_OPTION, []);
433 if (is_array($stagingSites)) {
434 foreach ($stagingSites as $key => $clone) {
435 $path = !empty($clone['path']) ? $clone['path'] : self::NOT_SET_LABEL;
436 if ($this->enableStructuredOutput) {
437 $clone['wpVersion'] = $this->getStagingWpVersion($path);
438 $stagingSites[$key] = $clone;
439 continue;
440 }
441
442 $output .= $this->info("Number:", isset($clone['number']) ? $clone['number'] : self::NOT_SET_LABEL);
443 $output .= $this->info("directoryName:", isset($clone['directoryName']) ? $clone['directoryName'] : self::NOT_SET_LABEL);
444 $output .= $this->info("Path:", $path);
445 $output .= $this->info("URL:", isset($clone['url']) ? $clone['url'] : self::NOT_SET_LABEL);
446 $output .= $this->info("DB Prefix:", isset($clone['prefix']) ? $clone['prefix'] : self::NOT_SET_LABEL);
447 $output .= $this->info("DB Prefix wp-config.php:", $this->getStagingPrefix($clone));
448 $output .= $this->info("WP STAGING Version:", isset($clone['version']) ? $clone['version'] : self::NOT_SET_LABEL);
449 $output .= $this->info("WP Version:", $this->getStagingWpVersion($path)) . PHP_EOL . PHP_EOL;
450 }
451 }
452
453 $stagingSites = $this->sanitizeSitePasswords(is_array($stagingSites) ? $stagingSites : []);
454 $stagingSitesOptionBackup = $this->sanitizeSitePasswords((array)get_option(Sites::BACKUP_STAGING_SITES_OPTION, []));
455
456 $output .= $this->info(Sites::STAGING_SITES_OPTION . ": ", serialize($stagingSites));
457 $output .= $this->info(Sites::BACKUP_STAGING_SITES_OPTION . ": ", serialize($stagingSitesOptionBackup));
458 return $output;
459 }
460
461 public function getWpStagingVersion(): string
462 {
463 if (defined('WPSTGPRO_VERSION')) {
464 return 'Pro ' . WPSTGPRO_VERSION;
465 }
466
467 if (defined('WPSTG_VERSION')) {
468 return WPSTG_VERSION;
469 }
470
471 return 'unknown';
472 }
473
474 /**
475 * Browser Information
476 * @return string
477 */
478 public function browser(): string
479 {
480 $this->currentSection = SystemInfoParser::SECTIONS['CLIENT_BROWSER_INFO']['id'];
481 $output = $this->header("Client / Browser Information");
482 $browser = new Browser();
483 $browserInfo = (string)$browser;
484
485 // Parse browser info into structured format if enabled
486 // Browser class returns formatted text with str_pad format: "Label: Value"
487 if ($this->enableStructuredOutput) {
488 $lines = explode("\n", trim($browserInfo));
489 foreach ($lines as $line) {
490 $line = trim($line);
491 if (empty($line)) {
492 continue;
493 }
494
495 // Browser info uses str_pad format: "Label: Value"
496 if (preg_match('/^(.{1,56})\s+(.+)$/', $line, $matches)) {
497 $label = trim($matches[1]);
498 $value = trim($matches[2]);
499 $this->info($label, $value);
500 } else {
501 $this->info('', $line);
502 }
503 }
504 }
505
506 $output .= $browserInfo;
507 return $output;
508 }
509
510 /**
511 * Check wp_remote_post() functionality
512 * @return string
513 */
514 public function wpRemotePost(): string
515 {
516 // Make sure wp_remote_post() is working
517 $wpRemotePost = "does not work";
518
519 // Check if has valid IP address
520 // to avoid error on php-wasm
521 $hostName = 'www.paypal.com';
522 $hostIp = gethostbyname($hostName);
523 if (preg_match('@\.0$@', $hostIp)) {
524 return $this->info("wp_remote_post():", $wpRemotePost);
525 }
526
527 // Send request
528 $response = wp_remote_post(
529 "https://" . $hostName . "/cgi-bin/webscr",
530 [
531 "sslverify" => false,
532 "timeout" => 60,
533 "user-agent" => "WPSTG/" . WPStaging::getVersion(),
534 "body" => ["cmd" => "_notify-validate"],
535 ]
536 );
537
538 // Validate it worked
539 if (!is_wp_error($response) && $response["response"]["code"] >= 200 && $response["response"]["code"] < 300) {
540 $wpRemotePost = "works";
541 }
542
543 return $this->info("wp_remote_post():", $wpRemotePost);
544 }
545
546 /**
547 * List of Active Plugins
548 * @param array $allAvailablePlugins
549 * @param array $activePlugins
550 * @return string
551 */
552 public function activePlugins(array $allAvailablePlugins, array $activePlugins): string
553 {
554 $this->currentSection = SystemInfoParser::SECTIONS['PLUGINS_OVERVIEW']['id'];
555 $output = $this->header("Active Plugins");
556
557 foreach ($allAvailablePlugins as $path => $plugin) {
558 if (!in_array($path, $activePlugins)) {
559 continue;
560 }
561
562 $output .= $this->info($plugin["Name"] . ":", $plugin["Version"]);
563 }
564
565 return $output;
566 }
567
568 /**
569 * List of Inactive Plugins
570 * @param array $allAvailablePlugins
571 * @param array $activePlugins
572 * @return string
573 */
574 public function inactivePlugins(array $allAvailablePlugins, array $activePlugins): string
575 {
576 $this->currentSection = SystemInfoParser::SECTIONS['PLUGINS_OVERVIEW']['id'];
577 if ($this->isMultiSite) {
578 $output = $this->header("Inactive Plugins (Includes this and other sites in the same network)");
579 } else {
580 $output = $this->header("Inactive Plugins");
581 }
582
583 foreach ($allAvailablePlugins as $path => $plugin) {
584 if (in_array($path, $activePlugins)) {
585 continue;
586 }
587
588 $output .= $this->info($plugin["Name"] . ":", $plugin["Version"]);
589 }
590
591 return $output;
592 }
593
594 /**
595 * Get list of active and inactive plugins
596 * @return string
597 */
598 public function plugins(): string
599 {
600 // Get plugins and active plugins
601 $allAvailablePlugins = get_plugins();
602 $activePlugins = get_option("active_plugins", []);
603
604 $activePluginsToGetInactive = $activePlugins;
605 if ($this->isMultiSite) {
606 $networkActivePlugins = array_keys(get_site_option("active_sitewide_plugins", []));
607 $activePluginsToGetInactive = array_merge($activePluginsToGetInactive, $networkActivePlugins);
608 }
609
610 // Active plugins
611 $output = $this->activePlugins($allAvailablePlugins, $activePlugins);
612 $output .= $this->inactivePlugins($allAvailablePlugins, $activePluginsToGetInactive);
613
614 return $output;
615 }
616
617 /**
618 * Multisite Plugins
619 * @return string
620 */
621 public function multiSitePlugins(): string
622 {
623 if (!$this->isMultiSite) {
624 return '';
625 }
626
627 $this->currentSection = SystemInfoParser::SECTIONS['PLUGINS_OVERVIEW']['id'];
628 $output = $this->header("Active Network Plugins (Includes this and other sites in the same network)");
629
630 $plugins = wp_get_active_network_plugins();
631 $activePlugins = get_site_option("active_sitewide_plugins", []);
632
633 foreach ($plugins as $pluginPath) {
634 $pluginBase = plugin_basename($pluginPath);
635
636 if (!array_key_exists($pluginBase, $activePlugins)) {
637 continue;
638 }
639
640 $plugin = get_plugin_data($pluginPath);
641
642 $output .= "{$plugin["Name"]}: {$plugin["Version"]}" . PHP_EOL;
643 }
644
645 unset($plugins, $activePlugins);
646
647 return $output;
648 }
649
650 /**
651 * Server Information
652 * @return string
653 */
654 public function server(): string
655 {
656 // Server & Operating System
657 $this->currentSection = SystemInfoParser::SECTIONS['SERVER_AND_OS']['id'];
658 $output = $this->header("Server & Operating System");
659 $output .= $this->info("Web Server:", isset($_SERVER["SERVER_SOFTWARE"]) ? Sanitize::sanitizeString($_SERVER["SERVER_SOFTWARE"]) : '');
660 $output .= $this->info("OS Architecture:", $this->siteInfo->getOSArchitecture());
661 $output .= $this->info("Server User:", $this->getPHPUser());
662
663 // Reference: https://dev.mysql.com/doc/refman/9.1/en/identifier-case-sensitivity.html
664 switch ($this->database->getLowerTablesNameSettings()) {
665 case '0':
666 $lowerTablesNameSettings = 'case-sensitive';
667 break;
668 case '1':
669 case '2':
670 $lowerTablesNameSettings = 'case-insensitive';
671 break;
672 default:
673 $lowerTablesNameSettings = 'N/A';
674 }
675
676 // Database (MySQL / MariaDB)
677 $this->currentSection = SystemInfoParser::SECTIONS['DATABASE_MYSQL_MARIADB']['id'];
678 $output .= $this->info("Database Type:", $this->database->getServerType());
679 $output .= $this->info("Version:", $this->database->getSqlVersion($compact = true));
680 $output .= $this->info("Full Version:", $this->database->getSqlVersion());
681 $output .= $this->info("Database Name:", $this->database->getWpdb()->dbname);
682 $output .= $this->info("Table Prefix:", $this->getTablePrefix());
683 $output .= $this->info("lower_case_table_names:", $lowerTablesNameSettings);
684 $output .= $this->getPrimaryKeyInfo();
685
686 // PHP Environment
687 $this->currentSection = SystemInfoParser::SECTIONS['PHP_ENVIRONMENT']['id'];
688 $output .= $this->info("PHP Version:", PHP_VERSION);
689 $output .= $this->info("PHP Architecture:", $this->siteInfo->getPhpArchitecture());
690 $output .= $this->info("PHP Safe Mode:", ($this->isSafeModeEnabled() ? "Enabled" : "Disabled"));
691 $displayErrors = ini_get("display_errors");
692 $output .= $this->info("display_errors:", ($displayErrors) ? "On ({$displayErrors})" : "N/A");
693
694 return $output;
695 }
696
697 /**
698 * @return string
699 */
700 public function getMySqlServerType(): string
701 {
702 return $this->database->getServerType();
703 }
704
705 /**
706 * @return string
707 */
708 public function getMySqlFullVersion(): string
709 {
710 return $this->database->getSqlVersion();
711 }
712
713 /**
714 * @return string
715 */
716 public function getMySqlVersionCompact(): string
717 {
718 return $this->database->getSqlVersion($compact = true);
719 }
720
721 /**
722 * @return string
723 */
724 public function getPhpVersion(): string
725 {
726 return PHP_VERSION;
727 }
728
729 /**
730 * @return string
731 */
732 public function getWebServerInfo(): string
733 {
734 return isset($_SERVER["SERVER_SOFTWARE"]) ? Sanitize::sanitizeString($_SERVER["SERVER_SOFTWARE"]) : '';
735 }
736
737 /**
738 * PHP Configuration
739 * @return string
740 */
741 public function php(): string
742 {
743 // PHP Limits
744 $this->currentSection = SystemInfoParser::SECTIONS['PHP_LIMITS']['id'];
745 $memoryLimit = ini_get("memory_limit");
746 $output = $this->info("memory_limit:", $memoryLimit . ' (' . number_format(wp_convert_hr_to_bytes($memoryLimit)) . ' bytes)');
747 $output .= $this->info("max_execution_time:", ini_get("max_execution_time"));
748 $output .= $this->info("max_input_vars:", ini_get("max_input_vars"));
749 $output .= $this->info("upload_max_filesize:", ini_get("upload_max_filesize"));
750 $output .= $this->info("post_max_size:", ini_get("post_max_size"));
751
752 return $output;
753 }
754
755 /**
756 * @return string
757 */
758 public function getPHPUser(): string
759 {
760
761 $user = '';
762
763 if (extension_loaded('posix') && function_exists('posix_getpwuid')) {
764 $file = WPSTG_PLUGIN_DIR . 'Core/WPStaging.php';
765 $user = posix_getpwuid(fileowner($file));
766 return isset($user['name']) ? $user['name'] : 'can not detect PHP user name';
767 }
768
769 if (function_exists('exec') && in_array('exec', explode(',', ini_get('disable_functions')))) {
770 $user = exec('whoami');
771 }
772
773 return empty($user) ? 'can not detect PHP user name' : $user;
774 }
775
776 /**
777 * Check if PHP is on Safe Mode
778 * @return bool
779 */
780 public function isSafeModeEnabled(): bool
781 {
782 return (
783 version_compare(PHP_VERSION, "5.4.0", '<') &&
784 // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved
785 @ini_get("safe_mode")
786 );
787 }
788
789 /**
790 * Checks if function exists or not
791 * @param string $functionName
792 * @return string
793 */
794 public function isSupported(string $functionName): string
795 {
796 return (function_exists($functionName)) ? "Supported" : "Not Supported";
797 }
798
799 /**
800 * Checks if class or extension is loaded / exists to determine if it is installed or not
801 * @param string $name
802 * @param bool $isClass
803 * @return string
804 */
805 public function isInstalled(string $name, bool $isClass = true): string
806 {
807 if ($isClass === true) {
808 return (class_exists($name)) ? "Installed" : "Not Installed";
809 } else {
810 return (extension_loaded($name)) ? "Installed" : "Not Installed";
811 }
812 }
813
814 /**
815 * Gets Installed Important PHP Extensions
816 * @return string
817 */
818 public function phpExtensions(): string
819 {
820 // Important PHP Extensions
821 $version = function_exists('curl_version') ? curl_version() : ['version' => 'Error: not available', 'ssl_version' => 'Error: not available', 'host' => 'Error: not available', 'protocols' => [], 'features' => []];
822
823 $bitfields = [
824 'CURL_VERSION_IPV6',
825 'CURL_VERSION_KERBEROS4',
826 'CURL_VERSION_SSL',
827 'CURL_VERSION_LIBZ',
828 ];
829
830 $this->currentSection = SystemInfoParser::SECTIONS['CURL_ENVIRONMENT']['id'];
831 $output = $this->header("cURL Environment");
832
833 $output .= $this->info("cURL Support:", $this->isSupported("curl_init") ? 'Yes' : 'No');
834 $output .= $this->info("cURL Version:", $version['version']);
835 $output .= $this->info("cURL Host:", $version['host']);
836 $output .= $this->info("SSL Library:", $version['ssl_version']);
837 $this->currentSection = SystemInfoParser::SECTIONS['CURL_FEATURES']['id'];
838 foreach ($bitfields as $feature) {
839 $output .= $this->info($feature . ":", (defined($feature) && $version['features'] & constant($feature) ? 'Supported' : 'Not Supported'));
840 }
841
842 $httpProtocols = ['http', 'https', 'ftp', 'ftps', 'file', 'dict', 'gopher'];
843 $emailProtocols = ['imap', 'imaps', 'pop3', 'pop3s', 'smtp', 'smtps'];
844 $allProtocols = $version['protocols'];
845
846 $httpProtocolsFound = array_values(array_intersect($allProtocols, $httpProtocols));
847 $emailProtocolsFound = array_values(array_intersect($allProtocols, $emailProtocols));
848
849 $otherProtocols = array_values(array_diff(
850 $allProtocols,
851 $httpProtocols,
852 $emailProtocols
853 ));
854 $this->currentSection = SystemInfoParser::SECTIONS['SUPPORTED_PROTOCOLS']['id'];
855 if (!empty($httpProtocolsFound)) {
856 $output .= $this->info('HTTP Protocols:', implode(', ', $httpProtocolsFound));
857 }
858
859 if (!empty($emailProtocolsFound)) {
860 $output .= $this->info('Email Protocols:', implode(', ', $emailProtocolsFound));
861 }
862
863 if (!empty($otherProtocols)) {
864 $output .= $this->info('Other Protocols:', implode(', ', $otherProtocols));
865 }
866
867 $this->currentSection = SystemInfoParser::SECTIONS['PHP_NETWORK_EXTENSIONS']['id'];
868 $output .= $this->info("fsockopen:", $this->isSupported("fsockopen") ? 'Supported' : 'Not Supported');
869 $output .= $this->info("SOAP Client:", $this->isInstalled("SoapClient"));
870 $output .= $this->info("Suhosin:", $this->isInstalled("suhosin", false));
871
872 return $output;
873 }
874
875 /**
876 * Check if WP is installed in subdir
877 * @return bool
878 */
879 private function isSubDir(): bool
880 {
881 // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
882 // This is happening much more often than you would expect
883 $siteurl = preg_replace('#^https?://#', '', rtrim(get_option('siteurl'), '/'));
884 $home = preg_replace('#^https?://#', '', rtrim(get_option('home'), '/'));
885
886 if ($home !== $siteurl) {
887 return true;
888 }
889
890 return false;
891 }
892
893 /**
894 * Try to get the staging prefix from wp-config.php of staging site
895 * @param array $clone
896 * @return string
897 */
898 private function getStagingPrefix(array $clone = []): string
899 {
900 // Throw error
901 $path = ABSPATH . $clone['directoryName'] . DIRECTORY_SEPARATOR . "wp-config.php";
902
903 if (!file_exists($path)) {
904 return 'File does not exist in: ' . $path;
905 }
906
907 if (($content = @file_get_contents($path)) === false) {
908 return 'Can\'t find staging wp-config.php';
909 } else {
910 // Get prefix from wp-config.php
911 //preg_match_all("/table_prefix\s*=\s*'(\w*)';/", $content, $matches);
912 preg_match("/table_prefix\s*=\s*'(\w*)';/", $content, $matches);
913 //wp_die(var_dump($matches));
914
915 if (!empty($matches[1])) {
916 return $matches[1];
917 } else {
918 return 'No table_prefix in wp-config.php';
919 }
920 }
921 }
922
923 /**
924 * Get staging site wordpress version number
925 * @param string $path
926 * @return string
927 */
928 private function getStagingWpVersion(string $path): string
929 {
930
931 if ($path === self::NOT_SET_LABEL) {
932 return "Error: Cannot detect WP version";
933 }
934
935 // Get version number of wp staging
936 $file = trailingslashit($path) . 'wp-includes/version.php';
937
938 if (!file_exists($file)) {
939 return "Error: Cannot detect WP version. File does not exist: $file";
940 }
941
942 $version = @file_get_contents($file);
943
944 $versionStaging = empty($version) ? 'unknown' : $version;
945
946 preg_match("/\\\$wp_version.*=.*'(.*)';/", $versionStaging, $matches);
947
948 if (empty($matches[1])) {
949 return "Error: Cannot detect WP version";
950 }
951
952 return $matches[1];
953 }
954
955 /**
956 * @param $key
957 * @param $value
958 * @return mixed|string
959 */
960 private function removeCredentials($key, $value)
961 {
962 $protectedFields = ['accessToken', 'refreshToken', 'accessKey', 'secretKey', 'password', 'passphrase'];
963 if (!empty($value) && in_array($key, $protectedFields)) {
964 return self::REMOVED_LABEL;
965 }
966
967 return empty($value) ? self::NOT_SET_LABEL : $value;
968 }
969
970 /**
971 * @return string
972 */
973 private function getQueueInfo(): string
974 {
975 $output = '';
976
977 /** @var Queue */
978 $queue = WPStaging::make(Queue::class);
979
980 $output .= $this->info("Backup All Actions in DB:", (string)$queue->count());
981 $output .= $this->info("Pending Actions:", (string)$queue->count(Queue::STATUS_READY));
982 $output .= $this->info("Processing Actions:", (string)$queue->count(Queue::STATUS_PROCESSING));
983 $output .= $this->info("Completed Actions:", (string)$queue->count(Queue::STATUS_COMPLETED));
984 $output .= $this->info("Failed Actions:", (string)$queue->count(Queue::STATUS_FAILED));
985
986 return $output;
987 }
988
989 /**
990 * @return string
991 */
992 private function getTablePrefix(): string
993 {
994 $prefix = $this->database->getPrefix();
995 $length = strlen($prefix);
996 $status = ($length > 16) ? "ERROR: Too long" : "Acceptable";
997 return $prefix . ' (Length: ' . $length . '' . $status . ')';
998 }
999
1000 /**
1001 * @return string
1002 */
1003 private function getBackupDetails(): string
1004 {
1005 $backups = WPStaging::make(ListableBackupsCollection::class)->getListableBackups();
1006
1007 $output = $this->info("Number of Backups:", (string)count($backups));
1008
1009 $totalBackupSize = 0;
1010 foreach ($backups as $backup) {
1011 $totalBackupSize += (float)$backup->size;
1012 }
1013
1014 $output .= $this->info("Total Backups Size:", esc_html((string)size_format($totalBackupSize, 2)));
1015
1016 return $output;
1017 }
1018
1019 /**
1020 * @param string $constantName
1021 * @return string
1022 */
1023 protected function constantInfo(string $constantName): string
1024 {
1025 if (!defined($constantName)) {
1026 return $this->info($constantName . ':', self::NOT_SET_LABEL);
1027 }
1028
1029 $constantValue = constant($constantName);
1030 if (is_bool($constantValue)) {
1031 $constantValue = $constantValue ? 'Yes' : 'No';
1032 }
1033
1034 return $this->info($constantName . ':', $constantValue);
1035 }
1036
1037 /**
1038 * Get setting value with NOT_SET_LABEL fallback
1039 *
1040 * @param object $settings Settings object
1041 * @param string $property Property name
1042 * @param bool $format Whether to number_format the value
1043 * @return string|int
1044 */
1045 protected function getSettingValue($settings, string $property, bool $format = false)
1046 {
1047 if (!isset($settings->$property)) {
1048 return self::NOT_SET_LABEL;
1049 }
1050
1051 return $format ? number_format($settings->$property) : $settings->$property;
1052 }
1053
1054 /**
1055 * Format boolean option value for display
1056 *
1057 * @param mixed $option Option value
1058 * @return string 'true' or 'false'
1059 */
1060 protected function formatBooleanOption($option): string
1061 {
1062 return !empty($option) && $option === 'true' ? 'true' : 'false';
1063 }
1064
1065 /**
1066 * Sanitize passwords in staging site data
1067 *
1068 * @param array $sites Array of staging sites
1069 * @return array Sanitized sites with passwords replaced
1070 */
1071 protected function sanitizeSitePasswords(array $sites): array
1072 {
1073 foreach ($sites as $key => $clone) {
1074 if (!empty($clone['databasePassword'])) {
1075 $sites[$key]['databasePassword'] = self::REMOVED_LABEL;
1076 }
1077
1078 if (!empty($clone['adminPassword'])) {
1079 $sites[$key]['adminPassword'] = self::REMOVED_LABEL;
1080 }
1081 }
1082
1083 return $sites;
1084 }
1085
1086 private function getPrimaryKeyInfo(): string
1087 {
1088 $tableName = $this->database->getPrefix() . 'options';
1089 $isPrimaryKeyMissing = $this->wpOptionsInfo->isOptionTablePrimaryKeyMissing($tableName);
1090 if ($isPrimaryKeyMissing) {
1091 return $this->info("Primary Key in {$tableName} :", self::NOT_SET_LABEL);
1092 }
1093
1094 $isPrimaryKeyIsOptionName = $this->wpOptionsInfo->isPrimaryKeyIsOptionName($tableName);
1095 if ($isPrimaryKeyIsOptionName) {
1096 return $this->info("Primary Key in {$tableName}:", 'option_name');
1097 }
1098
1099 return $this->info("Primary Key in {$tableName}:", 'option_id');
1100 }
1101
1102 public function setEncodeProLicense(bool $isEncodeProLicense = false)
1103 {
1104 $this->isEncodeProLicense = $isEncodeProLicense;
1105 }
1106
1107 public function getEncodeProLicense(): bool
1108 {
1109 return $this->isEncodeProLicense;
1110 }
1111
1112 private function getLicenseKey()
1113 {
1114 $licenseKey = get_option('wpstg_license_key');
1115 if (empty($licenseKey) || !$this->getEncodeProLicense()) {
1116 return $licenseKey;
1117 }
1118
1119 /** @var DataEncryption @dataEncryption */
1120 $dataEncryption = WPStaging::make(DataEncryption::class);
1121 // If phpseclib does not exist, return license key as it is
1122 if (!$dataEncryption->isPhpSecLibAvailable()) {
1123 return $licenseKey;
1124 }
1125
1126 $publicKey = $dataEncryption->getPublicKey();
1127 if (empty($publicKey)) {
1128 return $licenseKey;
1129 }
1130
1131 return $dataEncryption->rsaEncrypt($licenseKey, $publicKey);
1132 }
1133
1134 /**
1135 * @param string $optionName The name of the WP option to retrieve.
1136 * @param string $title The title to display before the settings.
1137 * @return string The formatted output for the settings.
1138 */
1139 protected function formatStorageSettings(string $optionName, string $title): string
1140 {
1141 $output = PHP_EOL . "-- " . $title . PHP_EOL;
1142
1143 $settings = (array) get_option($optionName, []);
1144 if (!empty($settings)) {
1145 // Add provider header as info item for structured output
1146 if ($this->enableStructuredOutput) {
1147 $this->info($title, '');
1148 }
1149
1150 foreach ($settings as $key => $value) {
1151 $output .= $this->info($key, empty($value) ? self::NOT_SET_LABEL : $this->removeCredentials($key, $value));
1152 }
1153 }
1154
1155 return $output;
1156 }
1157
1158 /**
1159 * @return string
1160 */
1161 protected function muPlugins(): string
1162 {
1163 $this->currentSection = SystemInfoParser::SECTIONS['PLUGINS_OVERVIEW']['id'];
1164 $output = $this->header("Must-Use Plugins");
1165 $muPlugins = get_mu_plugins();
1166 foreach ($muPlugins as $pluginData) {
1167 $output .= $this->info($pluginData["Name"] . ":", $pluginData["Version"]);
1168 }
1169
1170 return $output;
1171 }
1172
1173 /**
1174 * @return string
1175 */
1176 protected function dropIns(): string
1177 {
1178 $this->currentSection = SystemInfoParser::SECTIONS['PLUGINS_OVERVIEW']['id'];
1179 $output = $this->header("Drop-Ins");
1180 $dropIns = get_dropins();
1181 foreach ($dropIns as $dropIn) {
1182 $output .= $this->info($dropIn["Name"] . ":", $dropIn["Version"]);
1183 }
1184
1185 return $output;
1186 }
1187 }
1188