I’ve apparently run into some pretty cursed interaction between Parser, ScopedCallback, DeprecationHelper, and destructors.
Steps to reproduce:
- Have at least MediaWiki core (as of 7edec968c3) and Scribunto (as of d5bd9a2532) installed. It’s possible that more extensions that I happen to have installed are relevant as well.
- Have ini_set( 'display_errors', 1 ); in LocalSettings.php.
- Start editing a Wikitext page. I suggest using ?action=submit to bypass VisualEditor.
- Enter wikitext like {{#invoke:Abc|def}}. (Module:Abc need not exist.)
- Preview.
Instead of steps 3-5, you can also load api.php?action=parse&text={{%23invoke%3AAbc|def}}&contentmodel=wikitext&prop=text.
Actual result:
The output ends with a warning similar to this:
( ! ) Warning: Undefined property: MediaWiki\Parser\Parser::$dynamicPropertiesAccessDeprecated in /srv/http/wiki1/includes/debug/DeprecationHelper.php on line 214
Call Stack
1 1.0133 65243256 Wikimedia\ScopedCallback->__destruct( ) .../ScopedCallback.php:0
2 1.0133 65243256 call_user_func_array:{/srv/http/wiki1/vendor/wikimedia/scoped-callback/src/ScopedCallback.php:102}( $callback = class Closure { public $this = class MediaWiki\Parser\Parser { /* many other uninitialized variables */ private $dynamicPropertiesAccessDeprecated = *uninitialized* } }, $args = [] ) .../ScopedCallback.php:102
3 1.0133 65243456 MediaWiki\Parser\Parser->MediaWiki\Parser\{closure:/srv/http/wiki1/includes/parser/Parser.php:6402-6404}( ) .../ScopedCallback.php:102
4 1.0133 65243568 MediaWiki\Parser\Parser->__set( $name = 'mInParse', $value = FALSE ) .../Parser.php:6403
5 1.0133 65244016 MediaWiki\Parser\Parser->__get( $name = 'dynamicPropertiesAccessDeprecated' )
If you’re using VisualEditor, the issue is still reproducible but you’ll only see “Invalid response from server.” in the user interface, because the warning message breaks the API’s JSON response.
Expected result:
No such warning.
What happens:
Good question! As far as I’ve been able to figure out:
- The ScopedCallback returned by Parser::lock() is destructed, and tries to set $this->mInParse = false. ($this refers to the Parser.)
- Apparently the Parser as a whole, and possibly other objects as well, are also being destructed at this time. (Note that the issue isn’t reproducible with simple wikitext like Abc; I suspect that Scribunto holds a reference to the Parser and somehow causes it to be destructed later than it otherwise would be. Also, for the record, I originally noticed this with Wikibase’s {{#statements:…}} parser function, which probably does a similar thing; I’m reporting it with Scribunto here because that’s more likely to be installed.)
- DeprecationHelper::__set() is called (on the $parser – DeprecationHelper is a trait) with $name = 'mInParse' and $value = false. As far as I understand, this should already not happen, because mInParse is a regular and accessible property. I’m guessing it’s now inaccessible due to the ongoing destruction.
- DeprecationHelper::__set() wants to check if dynamic property access is deprecated or not, and accesses $this->dynamicPropertiesAccessDeprecated.
- DeprecationHelper::__get() is called with $name = 'dynamicPropertiesAccessDeprecated. Again, as far as I understand this isn’t supposed to happen.
- DeprecationHelper::__get() doesn’t find an $ownerClass, so it falls through to the next check.
- DeprecationHelper::__get() checks property_exists( $this, $name ). This returns true, even though at this point get_object_vars( $this ) is [] (empty).
- DeprecationHelper::__get() tries to return $this->$name, but despite the property_exists() check, this produces the warning seen above.
Environment:
PHP 8.3.6, Apache2 using libphp, Arch Linux.