PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.22
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.22
2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 2.5.24 All 137 releases
metasync / MetaSyncDebug.php

MetaSyncDebug.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.22, at MetaSyncDebug.php

637 lines 20.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Transforms a wp-config.php file.
4 *
5 * @package MetaSync
6 * @since 1.0.0
7 */
8
9 // Prevent direct access
10 if (!defined('ABSPATH')) {
11 exit;
12 }
13
14 /**
15 * Exception thrown when wp-config.php file is missing.
16 */
17 class WPConfigFileNotFoundException extends \Exception
18 {
19 }
20
21 /**
22 * Exception thrown when wp-config.php file is not writable.
23 */
24 class WPConfigFileNotWritableException extends \Exception
25 {
26 }
27
28 /**
29 * Exception thrown when wp-config.php file is empty.
30 */
31 class WPConfigFileEmptyException extends \Exception
32 {
33 }
34
35 /**
36 * Exception thrown when config type is invalid.
37 */
38 class WPConfigInvalidTypeException extends \Exception
39 {
40 }
41
42 /**
43 * Exception thrown when config value is invalid.
44 */
45 class WPConfigInvalidValueException extends \Exception
46 {
47 }
48
49 /**
50 * Exception thrown when placement anchor cannot be located.
51 */
52 class WPConfigAnchorNotFoundException extends \Exception
53 {
54 }
55
56 /**
57 * Exception thrown when normalization fails.
58 */
59 class WPConfigNormalizationException extends \Exception
60 {
61 }
62
63 /**
64 * Exception thrown when saving wp-config.php fails.
65 */
66 class WPConfigSaveException extends \Exception
67 {
68 }
69
70 /**
71 * Transforms a wp-config.php file.
72 */
73 class WPConfigTransformerMetaSync
74 {
75 /**
76 * Append to end of file
77 */
78 const ANCHOR_EOF = 'EOF';
79
80 /**
81 * Path to the wp-config.php file.
82 *
83 * @var string
84 */
85 protected $wpConfigPath;
86
87 /**
88 * Original source of the wp-config.php file.
89 *
90 * @var string
91 */
92 protected $wpConfigSrc;
93
94 /**
95 * Array of parsed configs.
96 *
97 * @var array
98 */
99 protected $wpConfigs = [];
100
101 /**
102 * Instantiates the class with a valid wp-config.php.
103 *
104 * @throws WPConfigFileNotFoundException If the wp-config.php file is missing.
105 * @throws WPConfigFileNotWritableException If the wp-config.php file is not writable.
106 *
107 * @param string $wpConfigPath Path to a wp-config.php file.
108 */
109 public function __construct($wpConfigPath)
110 {
111 $basename = basename($wpConfigPath);
112
113 if (!file_exists($wpConfigPath)) {
114 throw new WPConfigFileNotFoundException("{$basename} does not exist.");
115 }
116
117 if (!is_writable($wpConfigPath)) {
118 throw new WPConfigFileNotWritableException("{$basename} is not writable.");
119 }
120
121 $this->wpConfigPath = $wpConfigPath;
122 }
123
124 /**
125 * Checks if a config exists in the wp-config.php file.
126 *
127 * @throws WPConfigFileEmptyException If the wp-config.php file is empty.
128 * @throws WPConfigInvalidTypeException If the requested config type is invalid.
129 *
130 * @param string $type Config type (constant or variable).
131 * @param string $name Config name.
132 *
133 * @return bool
134 */
135 public function exists($type, $name)
136 {
137 $wpConfigSrc = file_get_contents($this->wpConfigPath);
138
139 if (!trim($wpConfigSrc)) {
140 throw new WPConfigFileEmptyException('Config file is empty.');
141 }
142
143 // Normalize the newline to prevent an issue coming from OSX.
144 $this->wpConfigSrc = str_replace(["\n\r", "\r"], "\n", $wpConfigSrc);
145 $this->wpConfigs = $this->parseWpConfig($this->wpConfigSrc);
146
147 if (!isset($this->wpConfigs[$type])) {
148 throw new WPConfigInvalidTypeException("Config type '{$type}' does not exist.");
149 }
150
151 return isset($this->wpConfigs[$type][$name]);
152 }
153
154 /**
155 * Get the value of a config in the wp-config.php file.
156 *
157 * @throws WPConfigFileEmptyException If the wp-config.php file is empty.
158 *
159 * @param string $type Config type (constant or variable).
160 * @param string $name Config name.
161 *
162 * @return mixed|null
163 */
164 public function getValue($type, $name)
165 {
166 $wpConfigSrc = file_get_contents($this->wpConfigPath);
167
168 if (!trim($wpConfigSrc)) {
169 throw new WPConfigFileEmptyException('Config file is empty.');
170 }
171
172 $this->wpConfigSrc = $wpConfigSrc;
173 $this->wpConfigs = $this->parseWpConfig($this->wpConfigSrc);
174
175 if (!isset($this->wpConfigs[$type])) {
176 return null;
177 }
178
179 return $this->wpConfigs[$type][$name]['value'];
180 }
181
182 /**
183 * Adds a config to the wp-config.php file.
184 *
185 * @throws WPConfigAnchorNotFoundException If the config placement anchor could not be located.
186 *
187 * @param string $type Config type (constant or variable).
188 * @param string $name Config name.
189 * @param string $value Config value.
190 * @param array $options Optional. Array of special behavior options.
191 *
192 * @return bool
193 */
194 public function add($type, $name, $value, array $options = [])
195 {
196 if (!is_string($value)) {
197 return false;
198 }
199
200 if ($this->exists($type, $name)) {
201 return false;
202 }
203
204 $defaults = [
205 'raw' => false, // Display value in raw format without quotes.
206 'anchor' => "/* That's all, stop editing!", // Config placement anchor string.
207 'separator' => PHP_EOL, // Separator between config definition and anchor string.
208 'placement' => 'before', // Config placement direction (insert before or after).
209 ];
210
211 list($raw, $anchor, $separator, $placement) = array_values(array_merge($defaults, $options));
212
213 $raw = (bool) $raw;
214 $anchor = (string) $anchor;
215 $separator = (string) $separator;
216 $placement = (string) $placement;
217
218 if (self::ANCHOR_EOF === $anchor) {
219 $contents = $this->wpConfigSrc . $this->normalize($type, $name, $this->formatValue($value, $raw));
220 } else {
221 if (false === strpos($this->wpConfigSrc, $anchor)) {
222 throw new WPConfigAnchorNotFoundException('Unable to locate placement anchor.');
223 }
224
225 $newSrc = $this->normalize($type, $name, $this->formatValue($value, $raw));
226 $newSrc = ('after' === $placement) ? $anchor . $separator . $newSrc : $newSrc . $separator . $anchor;
227 $contents = str_replace($anchor, $newSrc, $this->wpConfigSrc);
228 }
229
230 return $this->save($contents);
231 }
232
233 /**
234 * Updates an existing config in the wp-config.php file.
235 *
236 * @throws WPConfigInvalidValueException If the config value provided is not a string.
237 *
238 * @param string $type Config type (constant or variable).
239 * @param string $name Config name.
240 * @param string $value Config value.
241 * @param array $options Optional. Array of special behavior options.
242 *
243 * @return bool
244 */
245 public function update($type, $name, $value, array $options = [])
246 {
247 if (!is_string($value)) {
248 throw new WPConfigInvalidValueException('Config value must be a string.');
249 }
250
251 $defaults = [
252 'add' => true, // Add the config if missing.
253 'raw' => false, // Display value in raw format without quotes.
254 'normalize' => true, // Normalize config output using WP Coding Standards.
255 ];
256
257 list($add, $raw, $normalize) = array_values(array_merge($defaults, $options));
258
259 $add = (bool) $add;
260 $raw = (bool) $raw;
261 $normalize = (bool) $normalize;
262
263 if (!$this->exists($type, $name)) {
264 return ($add) ? $this->add($type, $name, $value, $options) : false;
265 }
266
267 $oldSrc = $this->wpConfigs[$type][$name]['src'];
268 $oldValue = $this->wpConfigs[$type][$name]['value'];
269 $newValue = $this->formatValue($value, $raw);
270
271 if ($normalize) {
272 $newSrc = $this->normalize($type, $name, $newValue);
273 } else {
274 $newParts = $this->wpConfigs[$type][$name]['parts'];
275 $newParts[1] = str_replace($oldValue, $newValue, $newParts[1]); // Only edit the value part.
276 $newSrc = implode('', $newParts);
277 }
278
279 if ($value === "true") {
280 $contents = preg_replace(
281 sprintf('/(?<=^|;|<\?php\s|<\?\s)(\s*?)%s/m', preg_quote(trim($oldSrc), '/')),
282 '$1' . str_replace('$', '\$', trim($newSrc)),
283 $this->wpConfigSrc
284 );
285 } else {
286 if (!$this->exists($type, $name)) {
287 return $this->save('');
288 }
289
290 $pattern = sprintf('/(?<=^|;|<\?php\s|<\?\s)%s\s*(\S|$)/m', preg_quote($this->wpConfigs[$type][$name]['src'], '/'));
291 $contents = preg_replace($pattern, '$1', $this->wpConfigSrc);
292 }
293
294 return $this->save($contents);
295 }
296
297 /**
298 * Removes a config from the wp-config.php file.
299 *
300 * @param string $type Config type (constant or variable).
301 * @param string $name Config name.
302 *
303 * @return bool
304 */
305 public function remove($type, $name)
306 {
307 if (!$this->exists($type, $name)) {
308 return false;
309 }
310
311 $pattern = sprintf('/(?<=^|;|<\?php\s|<\?\s)%s\s*(\S|$)/m', preg_quote($this->wpConfigs[$type][$name]['src'], '/'));
312 $contents = preg_replace($pattern, '$1', $this->wpConfigSrc);
313
314 return $this->save($contents);
315 }
316
317 /**
318 * Applies formatting to a config value.
319 *
320 * @throws WPConfigInvalidValueException When a raw value is requested for an empty string.
321 *
322 * @param string $value Config value.
323 * @param bool $raw Display value in raw format without quotes.
324 *
325 * @return mixed
326 */
327 protected function formatValue($value, $raw)
328 {
329 if ($raw && '' === trim($value)) {
330 throw new WPConfigInvalidValueException('Raw value for empty string not supported.');
331 }
332
333 return ($raw) ? $value : var_export($value, true);
334 }
335
336 /**
337 * Normalizes the source output for a name/value pair.
338 *
339 * @throws WPConfigNormalizationException If the requested config type does not support normalization.
340 *
341 * @param string $type Config type (constant or variable).
342 * @param string $name Config name.
343 * @param mixed $value Config value.
344 *
345 * @return string
346 */
347 protected function normalize($type, $name, $value)
348 {
349 if ('constant' === $type) {
350 $placeholder = "define( '%s', %s );";
351 } elseif ('variable' === $type) {
352 $placeholder = '$%s = %s;';
353 } else {
354 throw new WPConfigNormalizationException("Unable to normalize config type '{$type}'.");
355 }
356
357 return sprintf($placeholder, $name, $value);
358 }
359
360 /**
361 * Parses the source of a wp-config.php file.
362 *
363 * @param string $src Config file source.
364 *
365 * @return array
366 */
367 protected function parseWpConfig($src)
368 {
369 $configs = [];
370 $configs['constant'] = [];
371 $configs['variable'] = [];
372
373 // Strip comments.
374 foreach (token_get_all($src) as $token) {
375 if (in_array($token[0], [T_COMMENT, T_DOC_COMMENT], true)) {
376 $src = str_replace($token[1], '', $src);
377 }
378 }
379
380 preg_match_all('/(?<=^|;|<\?php\s|<\?\s)(\h*define\s*\(\s*[\'"](\w*?)[\'"]\s*)(,\s*(\'\'|""|\'.*?[^\\\\]\'|".*?[^\\\\]"|.*?)\s*)((?:,\s*(?:true|false)\s*)?\)\s*;)/ims', $src, $constants);
381 preg_match_all('/(?<=^|;|<\?php\s|<\?\s)(\h*\$(\w+)\s*=)(\s*(\'\'|""|\'.*?[^\\\\]\'|".*?[^\\\\]"|.*?)\s*;)/ims', $src, $variables);
382
383 if (!empty($constants[0]) && !empty($constants[1]) && !empty($constants[2]) && !empty($constants[3]) && !empty($constants[4]) && !empty($constants[5])) {
384 foreach ($constants[2] as $index => $name) {
385 $configs['constant'][$name] = [
386 'src' => $constants[0][$index],
387 'value' => $constants[4][$index],
388 'parts' => [
389 $constants[1][$index],
390 $constants[3][$index],
391 $constants[5][$index],
392 ],
393 ];
394 }
395 }
396
397 if (!empty($variables[0]) && !empty($variables[1]) && !empty($variables[2]) && !empty($variables[3]) && !empty($variables[4])) {
398 // Remove duplicate(s), last definition wins.
399 $variables[2] = array_reverse(array_unique(array_reverse($variables[2], true)), true);
400 foreach ($variables[2] as $index => $name) {
401 $configs['variable'][$name] = [
402 'src' => $variables[0][$index],
403 'value' => $variables[4][$index],
404 'parts' => [
405 $variables[1][$index],
406 $variables[3][$index],
407 ],
408 ];
409 }
410 }
411
412 return $configs;
413 }
414
415 /**
416 * Saves new contents to the wp-config.php file.
417 *
418 * @throws WPConfigFileEmptyException If the config file content provided is empty.
419 * @throws WPConfigSaveException If there is a failure when saving the wp-config.php file.
420 *
421 * @param string $contents New config contents.
422 *
423 * @return bool
424 */
425 protected function save($contents)
426 {
427 if (!trim($contents)) {
428 throw new WPConfigFileEmptyException('Cannot save the config file with empty contents.');
429 }
430
431 if ($contents === $this->wpConfigSrc) {
432 return false;
433 }
434
435 // Create backup before modifying wp-config.php
436 $backupPath = $this->wpConfigPath . '.metasync-backup-' . time();
437 if (!copy($this->wpConfigPath, $backupPath)) {
438 throw new WPConfigSaveException('Failed to create backup of wp-config.php');
439 }
440
441 $result = file_put_contents($this->wpConfigPath, $contents, LOCK_EX);
442
443 if (false === $result) {
444 // Restore from backup on failure
445 copy($backupPath, $this->wpConfigPath);
446 unlink($backupPath);
447 throw new WPConfigSaveException('Failed to update the config file.');
448 }
449
450 // Clean up old backups (keep last 5)
451 $this->cleanupOldBackups();
452
453 return true;
454 }
455
456 /**
457 * Clean up old wp-config backup files, keeping only the last 5
458 *
459 * @return void
460 */
461 protected function cleanupOldBackups()
462 {
463 $configDir = dirname($this->wpConfigPath);
464 $backupPattern = basename($this->wpConfigPath) . '.metasync-backup-*';
465 $backups = glob($configDir . '/' . $backupPattern);
466
467 if (count($backups) > 5) {
468 // Sort by modification time (oldest first)
469 usort($backups, function ($a, $b) {
470 return filemtime($a) - filemtime($b);
471 });
472
473 // Delete oldest backups, keep last 5
474 $toDelete = array_slice($backups, 0, count($backups) - 5);
475 foreach ($toDelete as $oldBackup) {
476 unlink($oldBackup);
477 }
478 }
479 }
480 }
481
482 class ConfigControllerMetaSync
483 {
484 const WPDD_DEBUGGING_PREDEFINED_CONSTANTS_STATE = 'dlct_data_initial';
485 private static $configfilePath;
486
487 protected $optionKey = 'debuglogconfigtool_updated_constant';
488 public $debugConstants = ['WP_DEBUG', 'WP_DEBUG_LOG', 'SCRIPT_DEBUG'];
489 protected $configFileManager;
490 private static $configArgs = [
491 'normalize' => true,
492 'raw' => true,
493 'add' => true,
494 ];
495
496 public function __construct()
497 {
498 $this->initialize();
499 }
500
501 private function initialize()
502 {
503 self::$configfilePath = $this->getConfigFilePath();
504 // Set anchor for the constants to write
505 $configContents = file_get_contents(self::$configfilePath);
506 if (false === strpos($configContents, "/* That's all, stop editing!")) {
507 preg_match('@\$table_prefix = (.*);@', $configContents, $matches);
508 self::$configArgs['anchor'] = $matches[0] ?? '';
509 self::$configArgs['placement'] = 'after';
510 }
511
512 if (!is_writable(self::$configfilePath)) {
513 add_action('admin_notices', function () {
514 $class = 'notice notice-error is-dismissible';
515 $message = 'Config file not writable';
516 printf('<div class="%1$s"><p>%2$s</p></div>', esc_attr($class), $message);
517 });
518 return;
519 }
520
521 $this->configFileManager = new WPConfigTransformerMetaSync(self::$configfilePath);
522 }
523
524 public function store()
525 {
526 try {
527 // Whitelist of allowed constants to prevent arbitrary constant modification
528 $allowedConstants = ['WP_DEBUG', 'WP_DEBUG_LOG', 'WP_DEBUG_DISPLAY'];
529
530 $updatedConstants = [];
531 $wpDebugEnabled = get_option('wp_debug_enabled', 'false');
532 $wpDebugLogEnabled = get_option('wp_debug_log_enabled', 'false');
533 $wpDebugDisplayEnabled = get_option('wp_debug_display_enabled', 'false');
534 $constants = [
535 'WP_DEBUG' => [
536 'name' => 'WP_DEBUG',
537 'value' => ($wpDebugEnabled === 'true' ? true : false),
538 'info' => 'Enable WP_DEBUG mode',
539 ],
540 'WP_DEBUG_LOG' => [
541 'name' => 'WP_DEBUG_LOG',
542 'value' => ($wpDebugLogEnabled === 'true' ? true : false),
543 'info' => 'Enable Debug logging to the /wp-content/debug.log file',
544 ],
545 'WP_DEBUG_DISPLAY' => [
546 'name' => 'WP_DEBUG_DISPLAY',
547 'value' => ($wpDebugDisplayEnabled === 'true' ? true : false),
548 'info' => 'Disable or hide display of errors and warnings in html pages'
549 ]
550 ];
551 $this->maybeRemoveDeletedConstants($constants);
552
553 foreach ($constants as $constant) {
554 // Use sanitize_key instead of sanitize_title for constant names
555 $key = strtoupper(sanitize_key($constant['name']));
556
557 // Whitelist validation - only allow specific constants
558 if (!in_array($key, $allowedConstants, true)) {
559 error_log('MetaSync: Attempted to modify non-whitelisted constant: ' . $key);
560 continue;
561 }
562
563 if (empty($key)) {
564 continue;
565 }
566
567 // Sanitize value - only allow boolean values
568 $value = is_bool($constant['value']) ? $constant['value'] : ($constant['value'] === 'true' || $constant['value'] === true);
569 $value = $value ? 'true' : 'false';
570
571 $this->configFileManager->update('constant', $key, $value, self::$configArgs);
572 $updatedConstants[] = $constant;
573 }
574 } catch (\Exception $e) {
575 error_log('MetaSync: Error updating wp-config.php - ' . $e->getMessage());
576 wp_send_json_error([
577 'message' => $e->getMessage(),
578 'success' => false
579 ]);
580 }
581 }
582
583 public function exists($constant)
584 {
585 return $this->configFileManager->exists('constant', strtoupper($constant));
586 }
587
588 public function getValue($constant)
589 {
590 if ($this->exists(strtoupper($constant))) {
591 return $this->configFileManager->getValue('constant', strtoupper($constant));
592 }
593 return null;
594 }
595
596 public function update($key, $value)
597 {
598 try {
599 // By default, when attempting to update a config that doesn't exist, one will be added.
600 $option = self::$configArgs;
601 if (is_bool($value)) {
602 $value = $value ? 'true' : 'false';
603 }
604 return $this->configFileManager->update('constant', strtoupper($key), $value, $option);
605 } catch (\Exception $e) {
606 return false;
607 }
608 }
609
610 public function getConfigFilePath()
611 {
612 $file = ABSPATH . 'wp-config.php';
613 if (!file_exists($file)) {
614 if (@file_exists(dirname(ABSPATH) . '/wp-config.php')) {
615 $file = dirname(ABSPATH) . '/wp-config.php';
616 }
617 }
618 return apply_filters('wp_dlct_config_file_manager_path', $file);
619 }
620
621 /**
622 * Remove deleted constant from config
623 *
624 * @param array $constants Array of constants.
625 *
626 * @return void
627 */
628 protected function maybeRemoveDeletedConstants($constants)
629 {
630 $deletedConstant = array_diff(array_column($constants, 'name'), array_column($constants, 'name'));
631
632 foreach ($deletedConstant as $item) {
633 $this->configFileManager->remove('constant', strtoupper($item));
634 }
635 }
636 }
637