Skip to content

Commit

Permalink
Merge pull request #573 from WordPress/feature/hooks-add-input-valida…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
schlessera authored Oct 29, 2021
2 parents 2d78a4d + 94678c0 commit 2d600fb
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 2 deletions.
28 changes: 28 additions & 0 deletions src/Hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

namespace WpOrg\Requests;

use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Hooker;
use WpOrg\Requests\Utility\InputValidator;

/**
* Handles adding and dispatching events
Expand All @@ -30,8 +32,23 @@ class Hooks implements Hooker {
* @param string $hook Hook name
* @param callback $callback Function/method to call on event
* @param int $priority Priority number. <0 is executed earlier, >0 is executed later
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $hook argument is not a string.
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $callback argument is not callable.
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $priority argument is not an integer.
*/
public function register($hook, $callback, $priority = 0) {
if (is_string($hook) === false) {
throw InvalidArgument::create(1, '$hook', 'string', gettype($hook));
}

if (is_callable($callback) === false) {
throw InvalidArgument::create(2, '$callback', 'callable', gettype($callback));
}

if (InputValidator::is_numeric_array_key($priority) === false) {
throw InvalidArgument::create(3, '$priority', 'integer', gettype($priority));
}

if (!isset($this->hooks[$hook])) {
$this->hooks[$hook] = array(
$priority => array(),
Expand All @@ -49,8 +66,19 @@ public function register($hook, $callback, $priority = 0) {
* @param string $hook Hook name
* @param array $parameters Parameters to pass to callbacks
* @return boolean Successfulness
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $hook argument is not a string.
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $parameters argument is not an array.
*/
public function dispatch($hook, $parameters = array()) {
if (is_string($hook) === false) {
throw InvalidArgument::create(1, '$hook', 'string', gettype($hook));
}

// Check strictly against array, as Array* objects don't work in combination with `call_user_func_array()`.
if (is_array($parameters) === false) {
throw InvalidArgument::create(2, '$parameters', 'array', gettype($parameters));
}

if (empty($this->hooks[$hook])) {
return false;
}
Expand Down
181 changes: 179 additions & 2 deletions tests/HooksTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

namespace WpOrg\Requests\Tests;

use Closure;
use stdClass;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Hooks;
use WpOrg\Requests\Tests\Fixtures\ArrayAccessibleObject;
use WpOrg\Requests\Tests\Fixtures\StringableObject;
use WpOrg\Requests\Tests\TestCase;

/**
Expand Down Expand Up @@ -70,8 +75,11 @@ public function testRegister() {
'Registering a second callback on the same hook with the same priority failed'
);

// Verify that new subkeys are created when needed.
$this->hooks->register('hookname', 'is_int', 10);
/*
* Verify that new subkeys are created when needed.
* Also verifies that the input validation isn't too strict for the priority.
*/
$this->hooks->register('hookname', 'is_int', '10');
$this->assertSame(
array(
'hookname' => array(
Expand All @@ -89,6 +97,29 @@ public function testRegister() {
);
}

/**
* Technical test to verify and safeguard Hooks::register() accepts closure callbacks.
*
* @covers ::register
*
* @return void
*/
public function testRegisterClosureCallback() {
$this->hooks->register(
'hookname',
function($param) {
return true;
}
);

$hooks_prop = $this->getPropertyValue($this->hooks, 'hooks');

$this->assertArrayHasKey('hookname', $hooks_prop, '$hooks property does not have key ["hookname"]');
$this->assertArrayHasKey(0, $hooks_prop['hookname'], '$hooks property does not have key ["hookname"][0]');
$this->assertArrayHasKey(0, $hooks_prop['hookname'][0], '$hooks property does not have key ["hookname"][0][0]');
$this->assertInstanceof(Closure::class, $hooks_prop['hookname'][0][0], 'Closure callback is not registered correctly');
}


/**
* Verify that the return value of the dispatch method is false when no hooks are registered.
Expand Down Expand Up @@ -171,6 +202,152 @@ public function testDispatchWithMultipleRegisteredHooks() {
$this->assertTrue($this->hooks->dispatch('hook_b', array(10, 'text')));
}

/**
* Tests receiving an exception when an invalid input type is passed to `register()` as `$hook`.
*
* @dataProvider dataInvalidHookname
*
* @covers ::register
*
* @param mixed $input Invalid hook name input.
*
* @return void
*/
public function testRegisterInvalidHookname($input) {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #1 ($hook) must be of type string');

$this->hooks->register($input, 'is_string');
}

/**
* Tests receiving an exception when an invalid input type is passed to `dispatch()` as `$hook`.
*
* @dataProvider dataInvalidHookname
*
* @covers ::dispatch
*
* @param mixed $input Invalid hook name input.
*
* @return void
*/
public function testDispatchInvalidHookname($input) {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #1 ($hook) must be of type string');

$this->hooks->dispatch($input);
}

/**
* Data Provider.
*
* @return array
*/
public function dataInvalidHookname() {
return array(
'null' => array(null),
'float' => array(1.1),
'stringable object' => array(new StringableObject('value')),
);
}

/**
* Tests receiving an exception when an invalid input type is passed to `register()` as `$callback`.
*
* @dataProvider dataRegisterInvalidCallback
*
* @covers ::register
*
* @param mixed $input Invalid callback.
*
* @return void
*/
public function testRegisterInvalidCallback($input) {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #2 ($callback) must be of type callable');

$this->hooks->register('hookname', $input);
}

/**
* Data Provider.
*
* @return array
*/
public function dataRegisterInvalidCallback() {
return array(
'null' => array(null),
'non-existent function' => array('functionname'),
'non-existent method' => array(array($this, 'dummyCallbackDoesNotExist')),
'empty array' => array(array()),
'plain object' => array(new stdClass(), 'method'),
);
}

/**
* Tests receiving an exception when an invalid input type is passed to `register()` as `$priority`.
*
* @dataProvider dataRegisterInvalidPriority
*
* @covers ::register
*
* @param mixed $input Invalid priority.
*
* @return void
*/
public function testRegisterInvalidPriority($input) {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #3 ($priority) must be of type int');

$this->hooks->register('hookname', array($this, 'dummyCallback1'), $input);
}

/**
* Data Provider.
*
* @return array
*/
public function dataRegisterInvalidPriority() {
return array(
'null' => array(null),
'float' => array(1.1),
'string "123 abc"' => array('123 abc'),
);
}

/**
* Tests receiving an exception when an invalid input type is passed to `dispatch()` as `$parameters`.
*
* @dataProvider dataDispatchInvalidParameters
*
* @covers ::dispatch
*
* @param mixed $input Invalid parameters array.
*
* @return void
*/
public function testDispatchInvalidParameters($input) {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #2 ($parameters) must be of type array');

$this->hooks->dispatch('hookname', $input);
}

/**
* Data Provider.
*
* @return array
*/
public function dataDispatchInvalidParameters() {
return array(
'null' => array(null),
'bool false' => array(false),
'float' => array(1.1),
'string' => array('param'),
'object implementing ArrayAccess' => array(new ArrayAccessibleObject()),
);
}

/**
* Dummy callback method.
*
Expand Down

0 comments on commit 2d600fb

Please sign in to comment.