Skip to content
Open
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
33 changes: 33 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
# Line endings
#
# Checked out with LF everywhere, whatever core.autocrlf says locally.
#
# Without this, a Windows clone with core.autocrlf=true gets CRLF source, and
# fifteen tests fail that pass everywhere else. They compare helper output
# against a literal multi-line string written in the test file: the helpers
# build their output from "\n" escapes, so they always emit LF, while the
# expected value picks up whatever the file on disk uses. CRLF checkout, CRLF
# expectation, LF actual, failure - in Form_helper_test, Html_helper_test and
# Calendar_test.
#
# This only affects what lands in the working tree. Repository content is
# unchanged, and PHP does not care either way, so nothing downstream breaks.
* text=auto eol=lf

# Windows-only files keep their native endings
*.bat text eol=crlf

# Binaries git should not touch at all
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.pdf binary
*.zip binary
*.gz binary

# This file tells which files and directories should be ignored and
# NOT downloaded when using composer to pull down a project with
# the --prefer-dist option selected. Used to remove development
Expand Down
63 changes: 61 additions & 2 deletions system/libraries/Form_validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ class CI_Form_validation {
*/
protected $CI;

/**
* Object that callback_ rules are resolved against.
*
* NULL means the CodeIgniter instance, which is the historic behaviour and
* is correct when callbacks live on the controller.
*
* This exists because $CI was doing two unrelated jobs: it is the handle
* for core services (lang, uri, router, input) AND it was the only place a
* callback could be looked up. Code that wanted callbacks somewhere other
* than the controller had to overwrite $CI outright, which quietly broke
* the services and made the callback target depend on whoever assigned
* last. Setting this instead leaves $CI alone.
*
* @var object|null
*/
protected $_callback_object = NULL;

/**
* Validation data for the current form submission
*
Expand Down Expand Up @@ -152,6 +169,46 @@ public function __construct($rules = array())

// --------------------------------------------------------------------

/**
* Set Callback Object
*
* Tells the validator where to look for callback_ rules. Without this the
* only way to run callbacks on a model, a service or any other collaborator
* is to overwrite $CI, which also hands it the job of supplying lang, uri,
* router and input.
*
* The library is shared for the whole request, so set this immediately
* before run() rather than once in a constructor - otherwise whoever
* constructed last owns every callback lookup that follows.
*
* Pass NULL to go back to the CodeIgniter instance.
*
* @param object|null $object
* @return CI_Form_validation
*/
public function set_callback_object($object = NULL)
{
$this->_callback_object = is_object($object) ? $object : NULL;
return $this;
}

// --------------------------------------------------------------------

/**
* Callback Object
*
* Where callback_ rules are resolved, defaulting to the CodeIgniter
* instance so existing controllers are unaffected.
*
* @return object
*/
public function callback_object()
{
return isset($this->_callback_object) ? $this->_callback_object : $this->CI;
}

// --------------------------------------------------------------------

/**
* Set Rules
*
Expand Down Expand Up @@ -713,15 +770,17 @@ protected function _execute($row, $rules, $postdata = NULL, $cycles = 0)
{
if ($callback)
{
if ( ! method_exists($this->CI, $rule))
$object = $this->callback_object();

if ( ! method_exists($object, $rule))
{
log_message('debug', 'Unable to find callback validation rule: '.$rule);
$result = FALSE;
}
else
{
// Run the function and grab the result
$result = $this->CI->$rule($postdata, $param);
$result = $object->$rule($postdata, $param);
}
}
else
Expand Down
85 changes: 85 additions & 0 deletions tests/codeigniter/libraries/Form_validation_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,79 @@ public function test_encode_php_tags()
$this->assertEquals('?>', $this->form_validation->encode_php_tags('?>'));
}

// --------------------------------------------------------------------
// set_callback_object()
// --------------------------------------------------------------------

public function test_callback_object_defaults_to_the_ci_instance()
{
// The historic behaviour: callbacks live on the controller
$this->assertSame(
$this->ci_instance(),
$this->form_validation->callback_object()
);
}

public function test_set_callback_object_redirects_callback_lookup()
{
$this->form_validation->set_callback_object(new Form_validation_test_callbacks());

$this->assertTrue($this->run_rules(
array(array('field' => 'foo', 'label' => 'Foo', 'rules' => 'callback_is_the_word_ok')),
array('foo' => 'ok')
));

$this->assertFalse($this->run_rules(
array(array('field' => 'foo', 'label' => 'Foo', 'rules' => 'callback_is_the_word_ok')),
array('foo' => 'nope')
));
}

public function test_set_callback_object_leaves_the_ci_reference_alone()
{
// The point of the whole change. Overwriting $CI to move callbacks
// also took lang, uri, router and input with it.
$before = $this->ci_instance();

$this->form_validation->set_callback_object(new Form_validation_test_callbacks());

$this->assertSame($before, $this->ci_instance());
$this->assertNotSame($this->ci_instance(), $this->form_validation->callback_object());
}

public function test_set_callback_object_null_restores_the_ci_instance()
{
$this->form_validation->set_callback_object(new Form_validation_test_callbacks());
$this->form_validation->set_callback_object(NULL);

$this->assertSame($this->ci_instance(), $this->form_validation->callback_object());
}

public function test_set_callback_object_ignores_a_non_object()
{
$this->form_validation->set_callback_object('not an object');

$this->assertSame($this->ci_instance(), $this->form_validation->callback_object());
}

public function test_set_callback_object_is_chainable()
{
$this->assertSame(
$this->form_validation,
$this->form_validation->set_callback_object(new Form_validation_test_callbacks())
);
}

public function test_a_missing_callback_still_fails_rather_than_erroring()
{
$this->form_validation->set_callback_object(new Form_validation_test_callbacks());

$this->assertFalse($this->run_rules(
array(array('field' => 'foo', 'label' => 'Foo', 'rules' => 'callback_no_such_method')),
array('foo' => 'ok')
));
}

/**
* Run rules
*
Expand All @@ -627,3 +700,15 @@ public function run_rules($rules, $values)
return $valid;
}
}

/**
* A collaborator that is not the controller, which is the whole point of
* set_callback_object().
*/
class Form_validation_test_callbacks {

public function is_the_word_ok($str)
{
return $str === 'ok';
}
}
Loading