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

updated Router to properly translate uri dashes that map to controller subdirs #4307

Merged
merged 7 commits into from
Mar 9, 2021

Conversation

sneakyimp
Copy link
Contributor

@sneakyimp sneakyimp commented Feb 19, 2021

Fixes #4294

Description
I have made a minor tweak to CodeIgniter\Router::validateRequest so that dashes in a subdirectory are mapped to underscores if translateURIDashes= true. This appears to correctly auto-route requests when you have a dash in the URI that corresponds to a controller directory.

Checklist:

  • Securely signed commits
  • Component(s) with PHPdocs
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@sneakyimp
Copy link
Contributor Author

@paulbalandan @kenjis @michalsn Could someone review this PR to fix #4294?

Copy link
Member

@paulbalandan paulbalandan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've looked at the changes and it looks good to me. I agree with the changes you've made. I have a minor cosmetic request. Change $segmentConvert to simply $segment. :)

I'll check first if this method is covered by tests or not before approving this for merge.

@paulbalandan
Copy link
Member

Can you add a test case for this one similar to here https://github.com/codeigniter4/CodeIgniter4/blob/develop/tests/system/Router/RouterTest.php#L215-L227 but using the underscored subfolder as in your case. It should assert that it will still get the correct controller and method.

@sneakyimp
Copy link
Contributor Author

Thank you, @paulbalandan!

I've looked at the changes and it looks good to me. I agree with the changes you've made. I have a minor cosmetic request. Change $segmentConvert to simply $segment. :)

Please note that I chose a different variable name to distinguish this value from the raw segments referred to elsewhere. It was my feeling that this more clearly indicated that the segment had been changed with ucfirst and with the dash-to-underscore replacement. I'd like to keep it because I think it makes the intent of the code much clearer. It was pretty difficult to understand the intent of this code because there are so few comments and the variables and functions are poorly named. In particular, the function validateRequest is very poorly named. I'd very much like to give that function a different name but have not done so in order to expedite matters. Please let me know if I may keep this other var name for greater clarity?

I have another question: may I remove the ucfirst call in setDirectory? This function is only called in the validateRequest function, where I have already applied ucfirst to the supplied parameter, and in system/Test/FeatureTestTrait.php where the supplied parameter is null.

I'll check first if this method is covered by tests or not before approving this for merge.

I have run the phpunit test and it passed all the previous tests, but please do run any additional tests:

vendor/phpunit/phpunit/phpunit tests/system/Router

I'll work on adding another test as you directed.

@paulbalandan
Copy link
Member

The tests run pretty smoothly and in fact the coverage increased for Router so that's good. But since we're fixing a bug here we'll want to document it that the fix has been well tested.

@sneakyimp
Copy link
Contributor Author

@paulbalandan I have committed another set of changes. NOTE that it implements more stringent enforcement of PSR-4 compliance in that it will throw PageNotFoundExceptions for controller names which are not acceptable PHP class names. This has the benefit of rejecting any .. or . attempts which may sneak into the Router but it will also reject controller names with dashes or periods or puncutation, etc. This should make the Router a bit more efficient because it eliminates a fair amount of additional logic that was getting executed.

Note that I did change RouterTest::testZeroAsURIPath() to expect an Exception instead of passing. A zero is not a valid Controller name -- you can't declare a class 0 {}; in PHP nor can you use a zero in any namespace declaration.

system/Router/Router.php Outdated Show resolved Hide resolved
system/Router/Router.php Outdated Show resolved Hide resolved
system/Router/Router.php Show resolved Hide resolved
system/Router/Router.php Show resolved Hide resolved
Comment on lines +595 to +615
// if a prior directory value has been set, just return segments and get out of here
if (isset($this->directory))
{
return $segments;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary? As this is untested, can you add a test case covering this possibility.

Copy link
Contributor Author

@sneakyimp sneakyimp Feb 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paulbalandan thank you for your detailed review.

This particular bit of code is not something I've written. I don't know why (or even if) it's necessary. There might be a situation where someone wants to set Router::directory and just have all the segments represent controller/method/params?

I have reviewed the code and it looks like the only place that Router::directory ever gets set is in the Router::setDirectory method which is only called in two places. The first is right there in that same scanControllers/validateRequest function. The second is in system/Test/FeatureTestTrait.php. In that latter case, it gets set to null which would cause isset($this->directory) to return false:

$v = null;
var_dump(isset($v)); // outputs bool(false)

I do not know under what circumstances anyone might want to call setDirectory prior to calling scanControllers or autoRoute function. I can imagine that it might be possible to instantiate a Router object somewhere, set its directory, and want to feed it parameters, but I can't seem to figure out any situation where CodeIgniter does this or might do it? It might be possible for a single Router instance to call autoRoute or scanControllers twice, but I honestly don't know -- and I don't know why it would be called with a different $segments parameter the second time.

It was in there when I began working on this function, and it seemed safest to leave it in. On further reflection, being able to set an arbitrary directory location for a controller and then passing it arbitrary parameters seems unsafe from a security perspective.

If we want to create a test to make sure this code runs, this will do it:

	public function testRouterPriorDirectory()
	{
		$router = new Router($this->collection, $this->request);
		
		$router->setDirectory('foo/bar/baz', false, true);
		$router->handle('Some_controller/some_method/param1/param2/param3');
		
		var_dump($router->directory());
		var_dump($router->controllerName());
		var_dump($router->methodName());
	}

Currently, the output of those last lines:

string(12) "foo/bar/baz/"
string(15) "Some_controller"
string(11) "some_method"

I could add this test and create assertions to expect those values, but I'm not certain why we would want to allow that behavior or if those are the desired values. Personally, I think it'd be safer to remove this check and force any autoRoute or scanControllers functionality to constrain code execution to the controllers folder.

Comment on lines +647 to +673
if ($validate)
{
$segments = explode('/', trim($dir, '/'));
foreach ($segments as $segment)
{
if (! $this->isValidSegment($segment))
{
return;
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addition is not tested.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added RouterTest::testSetDirectoryValid and RouterTest::testSetDirectoryInvalid which test the setDirectory function to run this block of code for both valid and invalid directories.

system/Router/Router.php Outdated Show resolved Hide resolved
@sneakyimp
Copy link
Contributor Author

@paulbalandan I've pushed another commit and I think it resolves the issues you mentioned in your review -- for some reason github only offers me a "resolve conversation" button rather than continuing our discussion. I hope you will please take a look and let me know if you have further concerns.

*/
private function isValidSegment(string $segment): bool
{
return (bool)preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CS: Add space after the cast. (bool) preg_match

{
$this->setDirectory(array_shift($segments), true);
$this->setDirectory($segmentConvert, true, false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering. You added a third param to setDirectory optionally set to true but purposely set it to false here. As you have promoted a stricter check on the segments, no validation here seems a bit off. Or am I missing something?

@sneakyimp
Copy link
Contributor Author

@paulbalandan sorry I'm slow to respond. For some reason github isn't letting me respond directly under your review comment so I'm commenting here.

CS: Add space after the cast. (bool) preg_match

I have added the space, committed to my local repo, rebased on the latest develop branch of CI4, and pushed. Please let me know if you have additional requests, questions, or issues.

I'm wondering. You added a third param to setDirectory optionally set to true but purposely set it to false here. As you have promoted a stricter check on the segments, no validation here seems a bit off. Or am I missing something?

By default, setDirectory has $validate=true. The only reason I set it to $validate=false when I call it in scanController is because I have already validated $segmentConvert a few lines before. My thinking was that it would be inefficient to validate the segment twice. There might be some better way to do this -- I considered having setDirectory return a boolean depending on whether the validation succeded or not, but setDirectory is a public method, and this would probably have introduced a Breaking Change.

@sneakyimp
Copy link
Contributor Author

I have no idea why these checks failed. They seem totally unrelated to my PR.

@kenjis
Copy link
Member

kenjis commented Mar 6, 2021

It is probably because ubuntu is now updated to 20.04.
But I don't know what's wrong.

@paulbalandan
Copy link
Member

Ubuntu latest is now 20.04 so only 7.4+ is built in in the image. We need to specify the extensions we need for lower php versions. I'll send a PR.

@paulbalandan
Copy link
Member

Please rebase,

Copy link
Contributor Author

@sneakyimp sneakyimp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rebase,

I have rebased and force-pushed. Please let me know if you have any additional thoughts.

Comment on lines +647 to +673
if ($validate)
{
$segments = explode('/', trim($dir, '/'));
foreach ($segments as $segment)
{
if (! $this->isValidSegment($segment))
{
return;
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added RouterTest::testSetDirectoryValid and RouterTest::testSetDirectoryInvalid which test the setDirectory function to run this block of code for both valid and invalid directories.

@paulbalandan paulbalandan merged commit 3fd3863 into codeigniter4:develop Mar 9, 2021
$router->setDirectory('foo/bar/baz', false, true);
$router->handle('Some_controller/some_method/param1/param2/param3');

$this->assertEquals('foo/bar/baz/', $router->directory());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sneakyimp Why is this test necessary?
Why does setDirectory() take precedence over URI?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bug: Cannot add route to controller in filename with dash/hyphen
3 participants