PluginProbe
WP Debugging / 2.12.6
WP Debugging v2.12.6
2.12.6 2.12.5 trunk 2.10.0 2.10.1 2.10.2 2.11.0 2.11.1 2.11.10 2.11.11 2.11.12 2.11.13 2.11.14 2.11.15 2.11.16 2.11.17 2.11.18 2.11.19 2.11.2 2.11.20 2.11.21 2.11.22 2.11.23 2.11.24 2.11.3 All 59 releases
wp-debugging / vendor / wp-cli / wp-config-transformer / src / WPConfigTransformer.php

WPConfigTransformer.php in WP Debugging 2.12.6, at vendor/wp-cli/wp-config-transformer/src/WPConfigTransformer.php

383 lines 11.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Transforms a wp-config.php file.
5 */
6 class WPConfigTransformer {
7 /**
8 * Append to end of file
9 */
10 const ANCHOR_EOF = 'EOF';
11
12 /**
13 * Path to the wp-config.php file.
14 *
15 * @var string
16 */
17 protected $wp_config_path;
18
19 /**
20 * Original source of the wp-config.php file.
21 *
22 * @var string
23 */
24 protected $wp_config_src;
25
26 /**
27 * Array of parsed configs.
28 *
29 * @var array<string, array<string, array{src: string, value: string, parts: array<string>}>>
30 */
31 protected $wp_configs = array();
32
33 /**
34 * Instantiates the class with a valid wp-config.php.
35 *
36 * @throws Exception If the wp-config.php file is missing.
37 * @throws Exception If the wp-config.php file is not writable.
38 *
39 * @param string $wp_config_path Path to a wp-config.php file.
40 */
41 public function __construct( $wp_config_path, $read_only = false ) {
42 $basename = basename( $wp_config_path );
43
44 if ( ! file_exists( $wp_config_path ) ) {
45 throw new Exception( "{$basename} does not exist." );
46 }
47
48 if ( ! $read_only && ! is_writable( $wp_config_path ) ) {
49 throw new Exception( "{$basename} is not writable." );
50 }
51
52 $this->wp_config_path = $wp_config_path;
53 }
54
55 /**
56 * Checks if a config exists in the wp-config.php file.
57 *
58 * @throws Exception If the wp-config.php file is empty.
59 * @throws Exception If the requested config type is invalid.
60 *
61 * @param string $type Config type (constant or variable).
62 * @param string $name Config name.
63 *
64 * @return bool
65 */
66 public function exists( $type, $name ) {
67 $wp_config_src = (string) file_get_contents( $this->wp_config_path );
68
69 if ( ! trim( $wp_config_src ) ) {
70 throw new Exception( 'Config file is empty.' );
71 }
72 // Normalize the newline to prevent an issue coming from OSX.
73 $this->wp_config_src = str_replace( array( "\r\n", "\n\r", "\r" ), "\n", $wp_config_src );
74 $this->wp_configs = $this->parse_wp_config( $this->wp_config_src );
75
76 if ( ! isset( $this->wp_configs[ $type ] ) ) {
77 throw new Exception( "Config type '{$type}' does not exist." );
78 }
79
80 return isset( $this->wp_configs[ $type ][ $name ] );
81 }
82
83 /**
84 * Get the value of a config in the wp-config.php file.
85 *
86 * @throws Exception If the wp-config.php file is empty.
87 * @throws Exception If the requested config type is invalid.
88 *
89 * @param string $type Config type (constant or variable).
90 * @param string $name Config name.
91 *
92 * @return string|null
93 */
94 public function get_value( $type, $name ) {
95 $wp_config_src = (string) file_get_contents( $this->wp_config_path );
96
97 if ( ! trim( $wp_config_src ) ) {
98 throw new Exception( 'Config file is empty.' );
99 }
100
101 $this->wp_config_src = $wp_config_src;
102 $this->wp_configs = $this->parse_wp_config( $this->wp_config_src );
103
104 if ( ! isset( $this->wp_configs[ $type ] ) ) {
105 throw new Exception( "Config type '{$type}' does not exist." );
106 }
107
108 return $this->wp_configs[ $type ][ $name ]['value'];
109 }
110
111 /**
112 * Adds a config to the wp-config.php file.
113 *
114 * @throws Exception If the config value provided is not a string.
115 * @throws Exception If the config placement anchor could not be located.
116 *
117 * @param string $type Config type (constant or variable).
118 * @param string $name Config name.
119 * @param string $value Config value.
120 * @param array<string, mixed> $options (optional) Array of special behavior options.
121 *
122 * @return bool
123 *
124 * @phpstan-param array{raw?: bool, anchor?: string, separator?: string, placement?: 'before'|'after'} $options
125 */
126 public function add( $type, $name, $value, array $options = array() ) {
127 if ( ! is_string( $value ) ) {
128 throw new Exception( 'Config value must be a string.' );
129 }
130
131 if ( $this->exists( $type, $name ) ) {
132 return false;
133 }
134
135 $defaults = array(
136 'raw' => false, // Display value in raw format without quotes.
137 'anchor' => "/* That's all, stop editing!", // Config placement anchor string.
138 'separator' => PHP_EOL, // Separator between config definition and anchor string.
139 'placement' => 'before', // Config placement direction (insert before or after).
140 );
141
142 list( $raw, $anchor, $separator, $placement ) = array_values( array_merge( $defaults, $options ) );
143
144 $raw = (bool) $raw;
145 $anchor = (string) $anchor;
146 $separator = (string) $separator;
147 $placement = (string) $placement;
148
149 if ( self::ANCHOR_EOF === $anchor ) {
150 $contents = $this->wp_config_src . $this->normalize( $type, $name, $this->format_value( $value, $raw ) );
151 } else {
152 if ( false === strpos( $this->wp_config_src, $anchor ) ) {
153 throw new Exception( 'Unable to locate placement anchor.' );
154 }
155
156 $new_src = $this->normalize( $type, $name, $this->format_value( $value, $raw ) );
157 $new_src = ( 'after' === $placement ) ? $anchor . $separator . $new_src : $new_src . $separator . $anchor;
158 $contents = str_replace( $anchor, $new_src, $this->wp_config_src );
159 }
160
161 return $this->save( $contents );
162 }
163
164 /**
165 * Updates an existing config in the wp-config.php file.
166 *
167 * @throws Exception If the config value provided is not a string.
168 *
169 * @param string $type Config type (constant or variable).
170 * @param string $name Config name.
171 * @param string $value Config value.
172 * @param array<string, mixed> $options (optional) Array of special behavior options.
173 *
174 * @return bool
175 *
176 * @phpstan-param array{raw?: bool, anchor?: string, separator?: string, placement?: 'before'|'after'} $options
177 */
178 public function update( $type, $name, $value, array $options = array() ) {
179 if ( ! is_string( $value ) ) {
180 throw new Exception( 'Config value must be a string.' );
181 }
182
183 $defaults = array(
184 'add' => true, // Add the config if missing.
185 'raw' => false, // Display value in raw format without quotes.
186 'normalize' => false, // Normalize config output using WP Coding Standards.
187 );
188
189 list( $add, $raw, $normalize ) = array_values( array_merge( $defaults, $options ) );
190
191 $add = (bool) $add;
192 $raw = (bool) $raw;
193 $normalize = (bool) $normalize;
194
195 if ( ! $this->exists( $type, $name ) ) {
196 return ( $add ) ? $this->add( $type, $name, $value, $options ) : false;
197 }
198
199 $old_src = $this->wp_configs[ $type ][ $name ]['src'];
200 $old_value = $this->wp_configs[ $type ][ $name ]['value'];
201
202 /**
203 * @var string $new_value
204 */
205 $new_value = $this->format_value( $value, $raw );
206
207 if ( $normalize ) {
208 $new_src = $this->normalize( $type, $name, $new_value );
209 } else {
210 $new_parts = $this->wp_configs[ $type ][ $name ]['parts'];
211 $new_parts[1] = str_replace( $old_value, $new_value, $new_parts[1] ); // Only edit the value part.
212 $new_src = implode( '', $new_parts );
213 }
214
215 $contents = (string) preg_replace(
216 sprintf( '/(?<=^|;|<\?php\s|<\?\s)(\s*?)%s/m', preg_quote( trim( $old_src ), '/' ) ),
217 '$1' . str_replace( '$', '\$', trim( $new_src ) ),
218 $this->wp_config_src
219 );
220
221 return $this->save( $contents );
222 }
223
224 /**
225 * Removes a config from the wp-config.php file.
226 *
227 * @param string $type Config type (constant or variable).
228 * @param string $name Config name.
229 *
230 * @return bool
231 */
232 public function remove( $type, $name ) {
233 if ( ! $this->exists( $type, $name ) ) {
234 return false;
235 }
236
237 if ( 'constant' === $type ) {
238 $pattern = sprintf(
239 "/(?:defined\s*\(\s*['\"][^'\\\"]+['\"]\s*\)\s*(?:or|\|\|)\s*)?\bdefine\s*\(\s*['\"]%s['\"]\s*,\s*(('[^']*'|\"[^\"]*\")|\s*(?:[\s\S]*?))\s*\)\s*;\s*/mi",
240 preg_quote( $name, '/' )
241 );
242 } else {
243 $pattern = sprintf(
244 '/^\s*\$%s\s*=\s*[\s\S]*?;\s*$/mi',
245 preg_quote( $name, '/' )
246 );
247 }
248
249 $contents = (string) preg_replace( $pattern, '', $this->wp_config_src );
250
251 return $this->save( $contents );
252 }
253
254 /**
255 * Applies formatting to a config value.
256 *
257 * @throws Exception When a raw value is requested for an empty string.
258 *
259 * @param string $value Config value.
260 * @param bool $raw Display value in raw format without quotes.
261 *
262 * @return bool|float|int|string|null
263 */
264 protected function format_value( $value, $raw ) {
265 if ( $raw && '' === trim( $value ) ) {
266 throw new Exception( 'Raw value for empty string not supported.' );
267 }
268
269 return ( $raw ) ? $value : var_export( $value, true );
270 }
271
272 /**
273 * Normalizes the source output for a name/value pair.
274 *
275 * @throws Exception If the requested config type does not support normalization.
276 *
277 * @param string $type Config type (constant or variable).
278 * @param string $name Config name.
279 * @param bool|float|int|string|null $value Config value.
280 *
281 * @return string
282 */
283 protected function normalize( $type, $name, $value ) {
284 if ( 'constant' === $type ) {
285 $placeholder = "define( '%s', %s );";
286 } elseif ( 'variable' === $type ) {
287 $placeholder = '$%s = %s;';
288 } else {
289 throw new Exception( "Unable to normalize config type '{$type}'." );
290 }
291
292 return sprintf( $placeholder, $name, $value );
293 }
294
295 /**
296 * Parses the source of a wp-config.php file.
297 *
298 * @param string $src Config file source.
299 *
300 * @return array<string, array<string, array{src: string, value: string, parts: array<string>}>>
301 */
302 protected function parse_wp_config( $src ) {
303 $configs = array();
304 $configs['constant'] = array();
305 $configs['variable'] = array();
306
307 // Strip comments.
308 foreach ( token_get_all( $src ) as $token ) {
309 if ( in_array( $token[0], array( T_COMMENT, T_DOC_COMMENT ), true ) ) {
310 if ( '//' === $token[1] ) {
311 // For empty line comments, actually remove empty line comments instead of all double-slashes.
312 // See: https://github.com/wp-cli/wp-config-transformer/issues/47
313 $src = (string) preg_replace( '/' . preg_quote( '//', '/' ) . '$/m', '', $src );
314 } else {
315 $src = str_replace( $token[1], '', $src );
316 }
317 }
318 }
319
320 preg_match_all( '/(?<=^|;|<\?php\s|<\?\s)(\h*(?:defined\s*\(\s*[\'"][\w]+[\'"]\s*\)\s*(?:or|\|\|)\s*)?define\s*\(\s*[\'"](\w*?)[\'"]\s*)(,\s*(\'\'|""|\'[^\'\\\\]*(?:\\\\[\s\S][^\'\\\\]*)*\'|"[^"\\\\]*(?:\\\\[\s\S][^"\\\\]*)*"|[\s\S]*?)\s*,?\s*)((?:,\s*(?:true|false)[,\s]*)?\)\s*;)/im', $src, $constants );
321 preg_match_all( '/(?<=^|;|<\?php\s|<\?\s)(\h*\$(\w+)\s*=)(\s*(\'\'|""|\'[^\'\\\\]*(?:\\\\[\s\S][^\'\\\\]*)*\'|"[^"\\\\]*(?:\\\\[\s\S][^"\\\\]*)*"|[\s\S]*?)\s*;)/im', $src, $variables );
322
323 if ( ! empty( $constants[0] ) && ! empty( $constants[1] ) && ! empty( $constants[2] ) && ! empty( $constants[3] ) && ! empty( $constants[4] ) && ! empty( $constants[5] ) ) {
324 foreach ( $constants[2] as $index => $name ) {
325 $configs['constant'][ $name ] = array(
326 'src' => $constants[0][ $index ],
327 'value' => $constants[4][ $index ],
328 'parts' => array(
329 $constants[1][ $index ],
330 $constants[3][ $index ],
331 $constants[5][ $index ],
332 ),
333 );
334 }
335 }
336
337 if ( ! empty( $variables[0] ) && ! empty( $variables[1] ) && ! empty( $variables[2] ) && ! empty( $variables[3] ) && ! empty( $variables[4] ) ) {
338 // Remove duplicate(s), last definition wins.
339 $variables[2] = array_reverse( array_unique( array_reverse( $variables[2], true ) ), true );
340 foreach ( $variables[2] as $index => $name ) {
341 $configs['variable'][ $name ] = array(
342 'src' => $variables[0][ $index ],
343 'value' => $variables[4][ $index ],
344 'parts' => array(
345 $variables[1][ $index ],
346 $variables[3][ $index ],
347 ),
348 );
349 }
350 }
351
352 return $configs;
353 }
354
355 /**
356 * Saves new contents to the wp-config.php file.
357 *
358 * @throws Exception If the config file content provided is empty.
359 * @throws Exception If there is a failure when saving the wp-config.php file.
360 *
361 * @param string $contents New config contents.
362 *
363 * @return bool
364 */
365 protected function save( $contents ) {
366 if ( ! trim( $contents ) ) {
367 throw new Exception( 'Cannot save the config file with empty contents.' );
368 }
369
370 if ( $contents === $this->wp_config_src ) {
371 return false;
372 }
373
374 $result = file_put_contents( $this->wp_config_path, $contents, LOCK_EX );
375
376 if ( false === $result ) {
377 throw new Exception( 'Failed to update the config file.' );
378 }
379
380 return true;
381 }
382 }
383