Krinkle has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/372594 )
Change subject: Hooks: Introduce NO_ABORT mode in Hooks::run() ...................................................................... Hooks: Introduce NO_ABORT mode in Hooks::run() When set, any hook that returns something other than true or null, will result in a run-time error. To make it easier to introduce this opt-in flag, still allow explicit 'true' returns from existing callers. Bug: T173615 Change-Id: I94c7ab656bd1a046410681a810c0a3fd4f72a2e5 --- M includes/Hooks.php M tests/phpunit/includes/HooksTest.php 2 files changed, 83 insertions(+), 10 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core refs/changes/94/372594/1 diff --git a/includes/Hooks.php b/includes/Hooks.php index f4f86be..5e03719 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -32,6 +32,9 @@ * @since 1.18 */ class Hooks { + const ABORTABLE = 1; + const NO_ABORT = 3; + /** * Array of events mapped to an array of callbacks to be run * when that event is triggered. @@ -118,7 +121,9 @@ * * @param string $event Event name * @param array $args Array of parameters passed to hook functions - * @param string|null $deprecatedVersion Optionally, mark hook as deprecated with version number + * @param string|null $deprecatedVersion [optional] Mark hook as deprecated with version number + * @param int $flags [optional] Whether the hook is abortable. Use Hooks::ABORTABLE or Hooks::NO_ABORT. + * Default: Hooks::ABORTABLE. * @return bool True if no handler aborted the hook * * @throws Exception @@ -128,7 +133,11 @@ * processing to continue. Not returning a value (or explicitly * returning null) is equivalent to returning true. */ - public static function run( $event, array $args = [], $deprecatedVersion = null ) { + public static function run( $event, array $args = [], $deprecatedVersion = null, $flags = 0 ) { + if ( $flags === 0 && is_int( $deprecatedVersion ) ) { + $flags = $deprecatedVersion; + $deprecatedVersion = null; + } foreach ( self::getHandlers( $event ) as $hook ) { // Turn non-array values into an array. (Can't use casting because of objects.) if ( !is_array( $hook ) ) { @@ -185,13 +194,17 @@ $hook_args = array_merge( $hook, $args ); $retval = call_user_func_array( $callback, $hook_args ); - // Process the return value. - if ( is_string( $retval ) ) { - // String returned means error. - throw new FatalError( $retval ); - } elseif ( $retval === false ) { - // False was returned. Stop processing, but no error. - return false; + if ( $flags === self::NO_ABORT && $retval !== null && $retval !== true ) { + wfLogWarning( "Invalid return from $func for unabortable hook $event." ); + } else { + // Process the return value. + if ( is_string( $retval ) ) { + // String returned means error. + throw new FatalError( $retval ); + } elseif ( $retval === false ) { + // False was returned. Stop processing, but no error. + return false; + } } } diff --git a/tests/phpunit/includes/HooksTest.php b/tests/phpunit/includes/HooksTest.php index 527e129..0f75cf6 100644 --- a/tests/phpunit/includes/HooksTest.php +++ b/tests/phpunit/includes/HooksTest.php @@ -142,7 +142,67 @@ } ); $foo = 'original'; Hooks::run( 'MediaWikiHooksTest001', [ &$foo ] ); - $this->assertSame( 'original', $foo, 'Hooks continued processing after a false return.' ); + $this->assertSame( 'original', $foo, 'Hooks abort after a false return.' ); + } + + /** + * @covers Hooks::run + */ + public function testNoAbort() { + $list = []; + Hooks::register( 'MediaWikiHooksTest001', function ( &$list ) { + $list[] = 1; + return true; // Explicit true + } ); + Hooks::register( 'MediaWikiHooksTest001', function ( &$list ) { + $list[] = 2; + return; // Implicit null + } ); + Hooks::register( 'MediaWikiHooksTest001', function ( &$list ) { + $list[] = 3; + // No return + } ); + + MWDebug::init(); + MWDebug::clearLog(); + MediaWiki\suppressWarnings(); + Hooks::run( 'MediaWikiHooksTest001', [ &$list ], Hooks::NO_ABORT ); + MediaWiki\restoreWarnings(); + MWDebug::deinit(); + + $log = MWDebug::getLog(); + $this->assertSame( [], $log, 'No warnings' ); + $this->assertSame( [ 1, 2, 3 ], $list, 'All hooks ran.' ); + } + + /** + * @covers Hooks::run + */ + public function testNoAbortWarning() { + Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) { + return false; + } ); + Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) { + $foo = 'test'; + return true; + } ); + $foo = 'original'; + + MWDebug::init(); + MWDebug::clearLog(); + MediaWiki\suppressWarnings(); + Hooks::run( 'MediaWikiHooksTest001', [ &$foo ], Hooks::NO_ABORT ); + MediaWiki\restoreWarnings(); + MWDebug::deinit(); + + $log = MWDebug::getLog(); + $this->assertSame( 'test', $foo, 'Unabortable hooks continue after a false return.' ); + $this->assertRegExp( + '/^Invalid return from .*closure for unabortable hook MediaWikiHooksTest001.$/', + $log[0]['msg'], + 'Log message' + ); + $this->assertSame( 'Hooks::run', $log[0]['caller'], 'Log caller'); } /** -- To view, visit https://gerrit.wikimedia.org/r/372594 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I94c7ab656bd1a046410681a810c0a3fd4f72a2e5 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: Krinkle <krinklem...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits