$constants Constant name => value. * @return string|null New contents, or null when no safe insertion point exists. */ public function insert( string $contents, array $constants ): ?string { $lines = array(); foreach ( $constants as $name => $value ) { if ( $this->defines( $contents, $name ) ) { continue; } $lines[] = sprintf( 'define( %s, %s );', var_export( $name, true ), var_export( $value, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- generates PHP source, not debug output. } if ( array() === $lines ) { return $contents; } $offset = $this->insertionOffset( $contents ); if ( null === $offset ) { return null; } $block = self::BLOCK_COMMENT . "\n" . implode( "\n", $lines ) . "\n\n"; return substr( $contents, 0, $offset ) . $block . substr( $contents, $offset ); } /** * Removes the `define()` lines of the given constants, wherever they are, * together with the comment line this plugin (or an earlier version of it) adds above its block. * * Only plain one-line `define( 'NAME', value );` statements are removed. A * constant defined any other way (inside an `if`, across several lines, via * a variable) is left in place, so the result is checked: if any of the * constants is still defined, null is returned and nothing should be written. * * @param string $contents Current wp-config.php contents. * @param list $names Constant names. * @return string|null New contents, or null when a constant could not be removed safely. */ public function remove( string $contents, array $names ): ?string { foreach ( $names as $name ) { $pattern = '/^[ \t]*define\s*\(\s*([\'"])' . preg_quote( $name, '/' ) . '\1\s*,[^;\n]*\)\s*;[ \t]*(?:(?:\/\/|#)[^\n]*)?\R?/mi'; $contents = (string) preg_replace( $pattern, '', $contents ); } foreach ( array_merge( array( self::BLOCK_COMMENT ), self::LEGACY_BLOCK_COMMENTS ) as $comment ) { $contents = (string) preg_replace( '/^[ \t]*' . preg_quote( $comment, '/' ) . '[ \t]*\R(?:[ \t]*\R)?/m', '', $contents ); } foreach ( $names as $name ) { if ( $this->defines( $contents, $name ) ) { return null; } } return $contents; } /** * Returns whether the contents already define the given constant. * * @param string $contents wp-config.php contents. * @param string $name Constant name. * @return bool */ public function defines( string $contents, string $name ): bool { $pattern = '/\bdefine\s*\(\s*([\'"])' . preg_quote( $name, '/' ) . '\1\s*,/i'; if ( ! preg_match_all( $pattern, $contents, $matches, PREG_OFFSET_CAPTURE ) ) { return false; } foreach ( $matches[0] as $match ) { $offset = (int) $match[1]; $lineStart = (int) strrpos( substr( $contents, 0, $offset ), "\n" ); $prefix = ltrim( substr( $contents, $lineStart, $offset - $lineStart ) ); // Skip commented-out lines: `// define(…)`, `# define(…)`, ` * define(…)` in a doc block. $commented = str_contains( $prefix, '//' ) || str_contains( $prefix, '#' ) || str_starts_with( $prefix, '*' ) || str_starts_with( $prefix, '/*' ); if ( ! $commented ) { return true; } } return false; } /** * Finds the byte offset where the constants block should be inserted. * * @param string $contents wp-config.php contents. * @return int|null */ private function insertionOffset( string $contents ): ?int { $marker = strpos( $contents, self::STOP_EDITING_MARKER ); if ( false !== $marker ) { return $marker; } if ( 1 === preg_match( self::WP_SETTINGS_PATTERN, $contents, $matches, PREG_OFFSET_CAPTURE ) ) { return (int) $matches[0][1]; } return null; } }