Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
<rant> Magic methods are not supposed to _use_ dynamic properties... they are supposed to _handle_ access _to_ inaccessible (`protected` or `private`) or non-existing properties *sigh*. What's the point of having the magic methods in place otherwise ? </rant> The `wpdb` class introduced the magic `__[isset|get|set|unset]()` methods in WP 3.5.0, but those methods were incorrectly implemented. 1. They contain no "allow list" of which `private`/`protected` properties are supposed to still be accessible, which in effect means any newly added `private`/`protected` properties would all still be treated as `public`. For this class, this means that the `protected` `$reconnect_retries` and `$incompatible_modes` (WP 3.9.0), the `private` `$use_mysqli` and `$has_connected` (WP 3.9.0) and the `private` `$checking_collation` (WP 4.2.0) were all world-accessible and changable, even though they were introduced **after** WP 3.5.0 and have `private`/`protected` visibility modifiers.... This also applies to the `private` `$allow_unsafe_unquoted_parameters` property as introduced in WP 6.1.0, though as that one has not been in a tagged release yet, we can still change it without creating a BC-break. For the `protected` `$col_meta`, `$table_charset` and `$check_current_query` properties which were introduced in WP 4.2.0 partial protection was put in place, protecting these against being _set_, but not against being _unset_ or read. 2. They dynamically set and gave access to new (`public`) properties when an undeclared property was encountered. This basically leaves the class wide open and negates the protection the magic methods (and visibility modifiers) were _intended_ to provide. Unfortunately, this code has existed in WordPress for too long. Changing this now would constitute a massive BC-break. However, the magic method implementation as is, is hugely problematic in light of the PHP 8.2 dynamic properties deprecation and the eventual intended removal of dynamic properties. So this needs fixing _without_ breaking BC, but _with_ protection for potential future `protected`/`private` properties being added. The fix I'm proposing does exactly that, by: 1. Storing undeclared properties being set on the class in a `private` `$arbitrary_props` array. 2. Adding an "allow list" with the names of those `protected`/`private` properties which should remain accessible via the magic methods. The use of this "allow list" makes sure that any new `protected`/`private` properties added at a later date will no longer be accessible via the magic methods. This includes the two properties I'm adding in this commit: those will be properly protected against interference from outside now. Also note that the `$allow_unsafe_unquoted_parameters` property which is also being introduced in WP 6.1.0 has _not_ been added to this list. 3. Fixing the methods themselves to no longer use dynamic property access, but use the `$arbitrary_props` array for undeclared properties. 4. Fixing the methods themselves to respect the allow list. 5. For the new _inaccessible_ properties, an `OutOfBoundsException` will be thrown if any attempt to retrieve their value (`__get()`) or overwrite them (`__set()`) is made. This is not a BC-break as this doesn't affect any pre-existing properties. The exception will only be thrown for new, declared, `private`/`protected` properties, i.e. properties which are introduced with this patch or after. Calls to `__isset()` will yield `false` and `__unset()` will silently ignore the property, which is in line with the PHP native and expected behaviour. 6. When the value of a property which does not exist and hasn't been (dynamically) declared is requested, an `Undefined property` warning will be thrown. PHP would previously natively throw this warning (warning since PHP 8.0, notice in PHP < 8.0). This is now emulated to ensure developer mistakes are not hidden away and developers actually are provided notice of these. The only difference is that for PHP < 8.0, the message has been elevated from a "notice" to a "warning". The only time that this message will be emitted is in case of developer error, so this message elevation is IMO not a BC-break. As for the choice between "notice" and "warning", I've chosen to stay in line with the latest PHP versions, especially as it has already been decided that this warning will be elevated in PHP 9.0 to a fatal error, so preventing those developer errors (or getting them fixed) seems prudent. Take note of the use of `property_exists()` and `array_key_exists()` in the magic methods instead of using `isset()`. A property may be set to `null` on purpose and the magic methods should handle that situation correctly. This also means that for _declared_ properties without a value, no error message level elevation is done. The `__get()` method only does a `property_exists()` check on those, no `isset()`, which means the PHP native error handling will kick in and throw a notice in PHP < 8.0 and a warning in PHP 8.0. Includes updating the tests to expect a warning for the "Undefined property" notice for undeclared properties, independently of the PHP version and to expect an "Undefined property" warning for unset `private` properties (this is due to the tests using a mock of `wpdb` and not `wpdb` itself). Refs: * https://www.php.net/manual/en/language.oop5.overloading.php#object.set * https://www.php.net/manual/en/class.outofboundsexception.php * https://wiki.php.net/rfc/undefined_property_error_promotion * php/php-src#7786 These magic methods were originally introduced via changesets [21472](https://core.trac.wordpress.org/changeset/21472), [21521](https://core.trac.wordpress.org/changeset/21521) and limited protection for select properties was added in [30345](https://core.trac.wordpress.org/changeset/30345).
- Loading branch information