From 0fff5089ba5a4e73b55820e3526f52faf99a4b14 Mon Sep 17 00:00:00 2001 From: Amir Sarabadani Date: Tue, 25 Oct 2022 18:58:49 +0200 Subject: Reorg: Move StubObject classes in includes to its own directory Bug: T166010 Change-Id: Idcf0e9dc6e0841e4f132207bce0f96774dad898c --- includes/StubObject/DeprecatedGlobal.php | 65 +++++++++ includes/StubObject/StubGlobalUser.php | 157 ++++++++++++++++++++ includes/StubObject/StubObject.php | 239 +++++++++++++++++++++++++++++++ includes/StubObject/StubUserLang.php | 44 ++++++ 4 files changed, 505 insertions(+) create mode 100644 includes/StubObject/DeprecatedGlobal.php create mode 100644 includes/StubObject/StubGlobalUser.php create mode 100644 includes/StubObject/StubObject.php create mode 100644 includes/StubObject/StubUserLang.php (limited to 'includes/StubObject') diff --git a/includes/StubObject/DeprecatedGlobal.php b/includes/StubObject/DeprecatedGlobal.php new file mode 100644 index 000000000000..1ec79a91ee72 --- /dev/null +++ b/includes/StubObject/DeprecatedGlobal.php @@ -0,0 +1,65 @@ +version = $version; + } + + // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + public function _newObject() { + /* + * Put the caller offset for wfDeprecated as 6, as + * that gives the function that uses this object, since: + * + * 1 = this function ( _newObject ) + * 2 = MediaWiki\StubObject\StubObject::_unstub + * 3 = MediaWiki\StubObject\StubObject::_call + * 4 = MediaWiki\StubObject\StubObject::__call + * 5 = MediaWiki\StubObject\DeprecatedGlobal:: + * 6 = Actual function using the global. + * (the same applies to _get/__get or _set/__set instead of _call/__call) + * + * Of course its theoretically possible to have other call + * sequences for this method, but that seems to be + * rather unlikely. + */ + wfDeprecated( '$' . $this->global, $this->version, false, 6 ); + return parent::_newObject(); + } +} + +class_alias( DeprecatedGlobal::class, 'DeprecatedGlobal' ); diff --git a/includes/StubObject/StubGlobalUser.php b/includes/StubObject/StubGlobalUser.php new file mode 100644 index 000000000000..66b91f983c94 --- /dev/null +++ b/includes/StubObject/StubGlobalUser.php @@ -0,0 +1,157 @@ +realUser = $realUser; + } + + /** + * @return User + */ + // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + public function _newObject() { + // Based on MediaWiki\StubObject\DeprecatedGlobal::_newObject + /* + * Put the caller offset for wfDeprecated as 6, as + * that gives the function that uses this object, since: + * + * 1 = this function ( _newObject ) + * 2 = MediaWiki\StubObject\StubGlobalUser::_unstub + * 3 = MediaWiki\StubObject\StubObject::_call + * 4 = MediaWiki\StubObject\StubObject::__call + * 5 = MediaWiki\StubObject\StubGlobalUser:: + * 6 = Actual function using the global. + * (the same applies to _get/__get or _set/__set instead of _call/__call) + * + * Of course its theoretically possible to have other call + * sequences for this method, but that seems to be + * rather unlikely. + */ + // Officially deprecated since 1.35 + wfDeprecated( '$wgUser', '1.35', false, 6 ); + return $this->realUser; + } + + /** + * Reset the stub global user to a different "real" user object, while ensuring that + * any method calls on that object will still trigger deprecation notices. + * + * @param StubGlobalUser|User $user + */ + public static function setUser( $user ) { + // This is intended to be interacting with the deprecated global + // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser + global $wgUser; + + self::$destructorDeprecationDisarmed = true; + // Supports MediaWiki\StubObject\StubGlobalUser parameter in case something fetched the existing value of + // $wgUser, set it to something else, and now is trying to restore it + $realUser = self::getRealUser( $user ); + $wgUser = new self( $realUser ); + self::$destructorDeprecationDisarmed = false; + } + + /** + * Get the relevant "real" user object based on either a User object or a MediaWiki\StubObject\StubGlobalUser + * wrapper. Bypasses deprecation notices from converting a MediaWiki\StubObject\StubGlobalUser to an actual + * user, and does not change $wgUser. + * + * @param StubGlobalUser|User $globalUser + * @return User + */ + public static function getRealUser( $globalUser ): User { + if ( $globalUser instanceof StubGlobalUser ) { + return $globalUser->realUser; + } elseif ( $globalUser instanceof User ) { + return $globalUser; + } else { + throw new InvalidArgumentException( + '$globalUser must be a User (or MediaWiki\StubObject\StubGlobalUser), got ' . + ( is_object( $globalUser ) ? get_class( $globalUser ) : gettype( $globalUser ) ) + ); + } + } + + /** + * This function creates a new object of the real class and replace it in + * the global variable. + * This is public, for the convenience of external callers wishing to access + * properties, e.g. eval.php + * + * Overriding MediaWiki\StubObject\StubObject::_unstub because for some reason that thinks there is + * an unstub loop when trying to use the magic __set() logic, but there isn't + * any loop because _newObject() returns a specific instance of User rather + * than calling any methods that could then try to use $wgUser. The main difference + * between this and the parent method is that we don't try to check for + * recursion loops. + * + * @param string $name Name of the method called in this object. + * @param int $level Level to go in the stack trace to get the function + * who called this function. + * @return User The unstubbed version + */ + // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + public function _unstub( $name = '_unstub', $level = 2 ) { + if ( !$GLOBALS[$this->global] instanceof self ) { + return $GLOBALS[$this->global]; // already unstubbed. + } + + $caller = wfGetCaller( $level ); + wfDebug( "Unstubbing \${$this->global} on call of " + . "\${$this->global}::$name from $caller" ); + $GLOBALS[$this->global] = $this->_newObject(); + return $GLOBALS[$this->global]; + } + + public function __destruct() { + if ( !self::$destructorDeprecationDisarmed ) { + wfDeprecatedMsg( '$wgUser reassignment detected', '1.37', false, 3 ); + } + } +} + +class_alias( StubGlobalUser::class, 'StubGlobalUser' ); diff --git a/includes/StubObject/StubObject.php b/includes/StubObject/StubObject.php new file mode 100644 index 000000000000..0fd027abec89 --- /dev/null +++ b/includes/StubObject/StubObject.php @@ -0,0 +1,239 @@ +global = $global; + if ( is_callable( $class ) ) { + $this->factory = $class; + } else { + $this->class = $class; + } + $this->params = $params; + } + + /** + * Returns a bool value whenever $obj is a stub object. Can be used to break + * a infinite loop when unstubbing an object. + * + * @param object $obj Object to check. + * @return bool True if $obj is not an instance of MediaWiki\StubObject\StubObject class. + */ + public static function isRealObject( $obj ) { + return is_object( $obj ) && !$obj instanceof self; + } + + /** + * Unstubs an object, if it is a stub object. Can be used to break a + * infinite loop when unstubbing an object or to avoid reference parameter + * breakage. + * + * @param object &$obj Object to check. + * @return void + */ + public static function unstub( &$obj ) { + if ( $obj instanceof self ) { + $obj = $obj->_unstub( 'unstub', 3 ); + } + } + + /** + * Function called if any function exists with that name in this object. + * It is used to unstub the object. Only used internally, PHP will call + * self::__call() function and that function will call this function. + * This function will also call the function with the same name in the real + * object. + * + * @param string $name Name of the function called + * @param array $args Arguments + * @return mixed + */ + // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + public function _call( $name, $args ) { + $this->_unstub( $name, 5 ); + return call_user_func_array( [ $GLOBALS[$this->global], $name ], $args ); + } + + /** + * Create a new object to replace this stub object. + * @return object + */ + // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + public function _newObject() { + $params = $this->factory + ? [ 'factory' => $this->factory ] + : [ 'class' => $this->class ]; + + // ObjectFactory::getObjectFromSpec accepts an array, not just a callable (phan bug) + // @phan-suppress-next-line PhanTypeInvalidCallableArraySize + return ObjectFactory::getObjectFromSpec( $params + [ + 'args' => $this->params, + 'closure_expansion' => false, + ] ); + } + + /** + * Function called by PHP if no function with that name exists in this + * object. + * + * @param string $name Name of the function called + * @param array $args Arguments + * @return mixed + */ + public function __call( $name, $args ) { + return $this->_call( $name, $args ); + } + + /** + * Wrapper for __get(), similar to _call() above + * + * @param string $name Name of the property to get + * @return mixed + */ + // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + public function _get( $name ) { + $this->_unstub( "__get($name)", 5 ); + return $GLOBALS[$this->global]->$name; + } + + /** + * Function called by PHP if no property with that name exists in this + * object. + * + * @param string $name Name of the property to get + * @return mixed + */ + public function __get( $name ) { + return $this->_get( $name ); + } + + /** + * Wrapper for __set(), similar to _call() above + * + * @param string $name Name of the property to set + * @param mixed $value New property value + */ + // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + public function _set( $name, $value ) { + $this->_unstub( "__set($name)", 5 ); + $GLOBALS[$this->global]->$name = $value; + } + + /** + * Function called by PHP if no property with that name exists in this + * object. + * + * @param string $name Name of the property to set + * @param mixed $value New property value + */ + public function __set( $name, $value ) { + $this->_set( $name, $value ); + } + + /** + * This function creates a new object of the real class and replace it in + * the global variable. + * This is public, for the convenience of external callers wishing to access + * properties, e.g. eval.php + * + * @param string $name Name of the method called in this object. + * @param int $level Level to go in the stack trace to get the function + * who called this function. + * @return object The unstubbed version of itself + * @throws MWException + */ + // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + public function _unstub( $name = '_unstub', $level = 2 ) { + static $recursionLevel = 0; + + if ( !$GLOBALS[$this->global] instanceof self ) { + return $GLOBALS[$this->global]; // already unstubbed. + } + + if ( get_class( $GLOBALS[$this->global] ) != $this->class ) { + $caller = wfGetCaller( $level ); + if ( ++$recursionLevel > 2 ) { + throw new MWException( "Unstub loop detected on call of " + . "\${$this->global}->$name from $caller\n" ); + } + wfDebug( "Unstubbing \${$this->global} on call of " + . "\${$this->global}::$name from $caller" ); + $GLOBALS[$this->global] = $this->_newObject(); + --$recursionLevel; + return $GLOBALS[$this->global]; + } + } +} + +class_alias( StubObject::class, 'StubObject' ); diff --git a/includes/StubObject/StubUserLang.php b/includes/StubObject/StubUserLang.php new file mode 100644 index 000000000000..7db0dde0ec63 --- /dev/null +++ b/includes/StubObject/StubUserLang.php @@ -0,0 +1,44 @@ +getLanguage(); + } +} + +class_alias( StubUserLang::class, 'StubUserLang' ); -- cgit v1.2.3