HEX
Server: nginx
System: Linux pool195-106-36.bur.atomicsites.net 6.12.57+deb12-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1~bpo12+1 (2025-11-17) x86_64
User: (0)
PHP: 8.3.32
Disabled: pcntl_fork
Upload Files
File: /wordpress/plugins/wp-cloud-client/1.1.8/src/Debug/WpConfigEditor.php
<?php

declare(strict_types=1);

namespace VPlugins\WPCloudClient\Debug;

/**
 * Reads and writes WordPress constants in wp-config.php.
 *
 * SetConstant() toggles a boolean define(); preserves non-boolean values.
 * ApplyConstants() does a batched read-modify-write for mixed types (bool/string/int);
 * and replaces any existing define() regardless of value type.
 * If a constant is absent it is inserted before the "That's all, stop editing!" marker,
 * or before the require_once wp-settings.php line on minimal wp-config.php files that
 * omit the standard marker (e.g. Automattic atomic platform).
 */
final class WpConfigEditor {

	/**
	 * Absolute path to wp-config.php.
	 *
	 * @var string
	 */
	private string $config_path;

	/**
	 * Class constructor.
	 *
	 * @param string $config_path Explicit path to wp-config.php; auto-detected when empty.
	 *
	 * @throws \RuntimeException If wp-config.php cannot be located.
	 */
	public function __construct( string $config_path = '' ) {
		$this->config_path = '' !== $config_path ? $config_path : $this->findConfigPath();
	}

	/**
	 * Return the absolute path to the wp-config.php file being edited.
	 *
	 * @return string Absolute file path.
	 */
	public function getConfigPath(): string {
		return $this->config_path;
	}

	/**
	 * Return current boolean values for the four debug constants.
	 *
	 * A null value means the constant is not defined in the file.
	 *
	 * @return array{WP_DEBUG: bool|null, WP_DEBUG_LOG: bool|null, WP_DEBUG_DISPLAY: bool|null, SCRIPT_DEBUG: bool|null}
	 *
	 * @throws \RuntimeException If the file cannot be read.
	 */
	public function getConstants(): array {
		$content = $this->readFile();

		return [
			'WP_DEBUG'         => $this->parseConstant( $content, 'WP_DEBUG' ),
			'WP_DEBUG_LOG'     => $this->parseConstant( $content, 'WP_DEBUG_LOG' ),
			'WP_DEBUG_DISPLAY' => $this->parseConstant( $content, 'WP_DEBUG_DISPLAY' ),
			'SCRIPT_DEBUG'     => $this->parseConstant( $content, 'SCRIPT_DEBUG' ),
		];
	}

	/**
	 * Set a single boolean constant in wp-config.php.
	 *
	 * Inserts a new define() if the constant is not already present.
	 * Non-boolean defines (e.g. WP_DEBUG_LOG = '/path/to/log') are preserved untouched.
	 *
	 * @param string $name  Constant name (e.g. 'WP_DEBUG').
	 * @param bool   $value Desired value.
	 *
	 * @throws \RuntimeException If the file cannot be read or written.
	 */
	public function setConstant( string $name, bool $value ): void {
		$content   = $this->readFile();
		$value_str = $value ? 'true' : 'false';

		if ( preg_match( $this->definePattern( $name ), $content ) ) {
			// Existing boolean define — replace it.
			$repl    = "define( '{$name}', {$value_str} )";
			$content = preg_replace_callback( $this->definePattern( $name ), static fn() => $repl, $content ) ?? $content;
		} elseif ( preg_match( $this->existsPattern( $name ), $content ) ) {
			// Non-boolean define exists (e.g. string path) — preserve it, do not insert a duplicate.
			return;
		} else {
			$content = $this->insertDefine( $content, $name, $value_str );
		}

		$this->writeFile( $content );
	}

	/**
	 * Set a single string constant in wp-config.php.
	 *
	 * Replaces the existing define() regardless of its current value type,
	 * or inserts a new one if the constant is absent. The value is single-quoted.
	 *
	 * @param string $name  Constant name (e.g. 'DOMAIN_CURRENT_SITE').
	 * @param string $value Desired string value.
	 *
	 * @throws \RuntimeException If the file cannot be read or written.
	 */
	public function setStringConstant( string $name, string $value ): void {
		$this->setValueConstant( $name, "'" . addslashes( $value ) . "'" );
	}

	/**
	 * Set a single integer constant in wp-config.php.
	 *
	 * Replaces the existing define() regardless of its current value type,
	 * or inserts a new one if the constant is absent.
	 *
	 * @param string $name  Constant name (e.g. 'SITE_ID_CURRENT_SITE').
	 * @param int    $value Desired integer value.
	 *
	 * @throws \RuntimeException If the file cannot be read or written.
	 */
	public function setIntConstant( string $name, int $value ): void {
		$this->setValueConstant( $name, (string) $value );
	}

	/**
	 * Apply multiple constants in a single read–modify–write cycle.
	 *
	 * Any existing define() for a given constant is replaced regardless of its
	 * current value type; absent constants are inserted before the stop-editing
	 * marker. A single file write prevents a partial-state window when several
	 * constants must be updated together.
	 *
	 * @param array<string, bool|string|int> $constants Map of constant name => value.
	 *
	 * @throws \RuntimeException If the file cannot be read or written.
	 */
	public function applyConstants( array $constants ): void {
		$content = $this->readFile();

		foreach ( $constants as $name => $value ) {
			$value_str = $this->toValueStr( $value );
			$content   = $this->applyOne( $content, $name, $value_str );
		}

		$this->writeFile( $content );
	}

	/**
	 * Locate wp-config.php using standard WordPress path conventions.
	 *
	 * @return string Absolute path.
	 *
	 * @throws \RuntimeException If the file cannot be found.
	 */
	private function findConfigPath(): string {
		$candidates = [
			ABSPATH . 'wp-config.php',
			dirname( ABSPATH ) . '/wp-config.php',
		];

		foreach ( $candidates as $path ) {
			if ( file_exists( $path ) ) {
				return $path;
			}
		}

		throw new \RuntimeException( 'wp-config.php not found.' );
	}

	/**
	 * Read the config file contents.
	 *
	 * @return string File contents.
	 *
	 * @throws \RuntimeException If the file is not readable or read fails.
	 */
	private function readFile(): string {
		if ( ! is_readable( $this->config_path ) ) {
			throw new \RuntimeException( sprintf( 'Cannot read wp-config.php at %s.', $this->config_path ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		$content = file_get_contents( $this->config_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents

		if ( false === $content ) {
			throw new \RuntimeException( sprintf( 'Cannot read wp-config.php at %s.', $this->config_path ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		return $content;
	}

	/**
	 * Write content back to the config file.
	 *
	 * @param string $content New file content.
	 *
	 * @throws \RuntimeException If the write fails.
	 */
	private function writeFile( string $content ): void {
		$result = file_put_contents( $this->config_path, $content, LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
		if ( false === $result ) {
			$err = error_get_last()['message'] ?? 'unknown error';
			throw new \RuntimeException( sprintf( 'Cannot write wp-config.php at %s: %s', $this->config_path, $err ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}
	}

	/**
	 * Write a raw define() value for $name, replacing any existing define() of any type.
	 *
	 * @param string $name     Constant name.
	 * @param string $valueStr Ready-to-insert value string (e.g. "'example.com'" or "1").
	 *
	 * @throws \RuntimeException If the file cannot be read or written.
	 */
	private function setValueConstant( string $name, string $valueStr ): void {
		$content = $this->readFile();
		$content = $this->applyOne( $content, $name, $valueStr );
		$this->writeFile( $content );
	}

	/**
	 * Convert a typed value to its PHP define() string representation.
	 *
	 * @param bool|string|int $value Value to convert.
	 * @return string Ready-to-insert value string.
	 */
	private function toValueStr( bool|string|int $value ): string {
		if ( is_bool( $value ) ) {
			return $value ? 'true' : 'false';
		}
		if ( is_int( $value ) ) {
			return (string) $value;
		}
		// A string starting with '$' is a raw PHP expression (e.g. $_SERVER['HTTP_HOST']).
		// Pass it through unquoted so wp-config.php evaluates it at runtime.
		if ( str_starts_with( (string) $value, '$' ) ) {
			return (string) $value;
		}
		return "'" . addslashes( $value ) . "'";
	}

	/**
	 * Apply a single constant to in-memory content without touching disk.
	 *
	 * Uses preg_replace_callback so the replacement is literal — avoids
	 * preg_replace back-reference interpretation of $N / \N in the value string.
	 *
	 * @param string $content  Current file content.
	 * @param string $name     Constant name.
	 * @param string $valueStr Ready-to-insert value string.
	 * @return string Updated content.
	 */
	private function applyOne( string $content, string $name, string $valueStr ): string {
		$replacement = "define( '{$name}', {$valueStr} )";

		if ( preg_match( $this->existsPattern( $name ), $content ) ) {
			// Match the value explicitly by type so a ')' inside a string value cannot
			// truncate the match and leave trailing junk in wp-config.php.
			// The \$\S* branch handles raw PHP expressions such as $_SERVER['HTTP_HOST'].
			return preg_replace_callback(
				'/define\s*\(\s*[\'"]' . preg_quote( $name, '/' ) . '[\'"]\s*,\s*(?:true|false|-?\d+|\'[^\']*\'|"[^"]*"|\$\S*)\s*\)/i',
				static fn() => $replacement,
				$content
			) ?? $content;
		}

		return $this->insertDefine( $content, $name, $valueStr );
	}

	/**
	 * Build a regex that matches a boolean define() call for the given constant name.
	 *
	 * @param string $name Constant name.
	 * @return string Regex pattern.
	 */
	private function definePattern( string $name ): string {
		return '/define\s*\(\s*[\'"]' . preg_quote( $name, '/' ) . '[\'"]\s*,\s*(true|false)\s*\)/i';
	}

	/**
	 * Build a regex that detects any define() for the given constant name, regardless of value type.
	 *
	 * @param string $name Constant name.
	 * @return string Regex pattern.
	 */
	private function existsPattern( string $name ): string {
		return '/define\s*\(\s*[\'"]' . preg_quote( $name, '/' ) . '[\'"]\s*,/';
	}

	/**
	 * Insert a define() before the standard WP stop-editing marker, or append at end.
	 *
	 * @param string $content   Current file content.
	 * @param string $name      Constant name.
	 * @param string $value_str Value string to insert.
	 * @return string Updated file content.
	 */
	private function insertDefine( string $content, string $name, string $value_str ): string {
		$line = "define( '{$name}', {$value_str} );";

		// Primary: standard WordPress stop-editing marker.
		$marker = "/* That's all, stop editing!";
		if ( str_contains( $content, $marker ) ) {
			return str_replace( $marker, "{$line}\n{$marker}", $content );
		}

		// Fallback: insert before require_once wp-settings.php — covers minimal
		// wp-config.php files (e.g. Automattic atomics) that omit the marker but
		// always include this line. Constants must be defined before WordPress boots.
		if ( preg_match( '/^(.*?)(require(?:_once)?\s*[\'"]?.*?wp-settings\.php[\'"]?\s*;)/ms', $content, $m ) ) {
			return $m[1] . $line . "\n" . $m[2] . substr( $content, strlen( $m[1] ) + strlen( $m[2] ) );
		}

		// Last resort: append at end. May land after wp-settings.php require — constants
		// will have no effect at runtime but avoids losing data on write.
		return $content . "\n{$line}\n";
	}

	/**
	 * Parse a single boolean constant from file content.
	 *
	 * @param string $content File content.
	 * @param string $name    Constant name.
	 * @return bool|null True/false if found, null if absent.
	 */
	private function parseConstant( string $content, string $name ): ?bool {
		if ( preg_match( $this->definePattern( $name ), $content, $matches ) ) {
			return 'true' === strtolower( $matches[1] );
		}

		return null;
	}
}