wiki.techinc.nl/includes/libs/GhostFieldAccessTrait.php
C. Scott Ananian 19ee8c4f91 Serialization test cases: fix filename after ParserOutput namespacing
The serialization test cases look for files based on the name of the
class they are testing.  After the namespacing of ParserOutput, they
were looking for files named like:
  1.42-MediaWiki\Parser\ParserOutput-binaryPageProperties.json

The embedded backslashes in these filenames would raise havoc on Windows
machines.  What's more, none of the existing ParserOutput tests will
actually be checked anymore because the filenames don't match up
with what is expected after namespacing.

Fix this by stripping the namespace from the classname when forming
the test file names.

When this is done, the tests cases for GhostFieldAccess begin running
again, revealing that they were broken when GhostFieldTestClass was
re-namespaced.  Add a class alias for the GhostFieldTestClass to fix
this.

Finally, PHP <= 8.1 does not deserialize private properties correctly
after a class is renamed and aliased, because the internal name of the
private property contains the "old" class name in the serialization.
Add a new ::restoreAliasedGhostField() method to the
GhostFieldAccessTrait to workaround this issue and restore proper
deserialization of ParserOutput.

Bug: T365060
Followup-To: I9c64a631b0b4e8e4fef8a72ee0f749d35f918052
Followup-To: I4c2cbb0a808b3881a4d6ca489eee5d8c8ebf26cf
Change-Id: I7bafe80cd36c2558517f474871148286350a4e76
2024-05-17 17:07:47 -04:00

99 lines
3 KiB
PHP

<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace Wikimedia\Reflection;
/**
* Trait for accessing "ghost fields".
*
* Ghost fields are fields that have been created in an object instance
* by PHP's unserialize() mechanism, though they no longer exist
* in the current version of the corresponding class.
*
* Accessing non-public ghost fields offers some challenges due to
* how they are handled by PHP internally.
*
* @see https://www.php.net/manual/en/language.types.array.php#language.types.array.casting
* @since 1.36
*/
trait GhostFieldAccessTrait {
/**
* Get the value of the ghost field named $name,
* or null if the field does not exist.
*
* @param string $name
* @param class-string ...$aliases
* @return mixed|null
*/
private function getGhostFieldValue( string $name, string ...$aliases ) {
if ( isset( $this->$name ) ) {
return $this->$name;
}
$data = (array)$this;
// Protected variables have a '*' prepended to the variable name.
// These prepended values have null bytes on either side.
$protectedName = "\x00*\x00{$name}";
if ( isset( $data[$protectedName] ) ) {
return $data[$protectedName];
}
// Private variables have the class name prepended to the variable name.
// These prepended values have null bytes on either side.
$thisClass = get_class( $this );
$privateName = "\x00{$thisClass}\x00{$name}";
if ( isset( $data[$privateName] ) ) {
return $data[$privateName];
}
// Check old aliased class names as well.
foreach ( $aliases as $thisClass ) {
$privateName = "\x00{$thisClass}\x00{$name}";
if ( isset( $data[$privateName] ) ) {
return $data[$privateName];
}
}
return null;
}
/**
* In PHP <= 8.1, private fields of a class are not properly restored
* when the class is aliased to a new name, since the private fields are
* prefixed with the *old* name of the class. This method can be used
* to restore them if present after deserialization.
*
* @param string $name
* @param class-string ...$aliases
*/
private function restoreAliasedGhostField( string $name, string ...$aliases ): void {
$data = (array)$this;
foreach ( $aliases as $thisClass ) {
$privateName = "\x00{$thisClass}\x00{$name}";
if ( isset( $data[$privateName] ) ) {
$this->$name = $data[$privateName];
unset( $data[$privateName] );
return;
}
}
}
}