Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hooks: add input validation to all methods #573

Merged
merged 1 commit into from
Oct 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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