-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Bug: Cannot add route to controller in filename with dash/hyphen #4294
Comments
I've been digging into the System\Router\Router class and it looks like there are problems with the validateRequest method. Firstly, I don't think this filtering does anything. Can someone explain to me what it's supposed to accomplish?: $segments = array_filter($segments, function ($segment) {
// @phpstan-ignore-next-line
return ! empty($segment) || ($segment !== '0' || $segment !== 0);
});
$segments = array_values($segments); I think that return condition contains a tautology. I tried this code and the array_filter function has no impact whatsoever on the array's contents: $segments1 = ["foo", "bar", 0, "0", NULL, FALSE, ""];
$segments2 = array_filter($segments1, function ($segment) {
return ! empty($segment) || ($segment !== '0' || $segment !== 0);
});
if ($segments1 == $segments2) {
echo "they are equal\n";
} else {
echo "they are NOT equal\n";
} Output is Also problematic is line 599: if (! is_file(APPPATH . 'Controllers/' . $test . '.php') && $directoryOverride === false && is_dir(APPPATH . 'Controllers/' . $this->directory . ucfirst($segments[0])))
{
$this->setDirectory(array_shift($segments), true);
continue;
} The is_file check is properly translating dashes to underscores, but the is_dir check is checking the original dashed url segment. For the url
I believe this function should be modified so that the is_dir check is checking the dash-translated paths if the user options have set translateURIDashes= @kenjis this is the post I referred to on the Codeigniter forum post. |
…r subdirectories. fix issue codeigniter4#4294
Considering that the errors reported by phpstan were silenced means that there is something fishy with the logic, and yes, it seems both sides of This should have been: $segments = array_filter($segments, function ($segment) {
return ! empty($segment) || ($segment === '0' || $segment === 0);
}); |
Thank you, @paulbalandan
I guess we have to allow zeros as segments because they might be parameters, but I'm having trouble imagining any circumstances where a segment would actually be a zero. The value $segments is passed in as a parameter from an explode statement in the autoRoute function. Also, the return value of the logic is ambiguous, and would benefit from some parenthetical grouping. May I use this? $segments = array_filter($segments, function ($segment) {
return ! (empty($segment) || ($segment === '0'));
}); |
The new logic would leave out |
And while you're here, can you make the function
|
Ah yes the logic I proposed is incorrect -- the space between $segments = array_filter($segments, function ($segment) {
return ($segment || ($segment === '0'));
}); that seems much more readable and clear to me. |
OK. The easiest to understand, I think. $segments = array_filter($segments, static function ($segment): bool {
return (string) $segment !== '';
}); |
Oooh nice that looks pretty tidy. I'll run a performance comparison and see which is faster. |
Booleans and integers will never appear in the URL. so simply just remove the type cast. 😂 |
EDIT: your latest variation wins. I'll use it. $segments = ["foo", "bar", 0, "0", NULL, FALSE, ""];
$iterations = 1000000;
$start = microtime(TRUE);
for($i=0; $i<$iterations; $i++) {
$segments1 = array_filter($segments, static function ($segment): bool {
return $segment !== '';
});
}
echo "#1 elapsed: " . (microtime(TRUE) - $start) . "\n";
$start = microtime(TRUE);
for($i=0; $i<$iterations; $i++) {
$segments1 = array_filter($segments, static function ($segment): bool {
return ($segment || $segment === '0');
});
}
echo "#2 elapsed: " . (microtime(TRUE) - $start) . "\n"; results:
May I keep the $segmentConverted variable name? I think it makes the code clearer. Also, may I remove the ucfirst from setDirectory? It should have no effect on existing code and should improve performance a little bit by removing the redundant ucfirst operation. |
OK. Sure and sure. Those changes are not materially affecting the code so I'm fine either way. |
Hmmmm. I got to thinking about this. We should think hard about security in these validateRequest and setDirectory functions. Keep in mind the vital role of this function: it parses the user-supplied request URI to determine what code actually runs. It might be worth a slightly slower function to make sure that we eliminate any possible mischief. For starters, we might make sure $segment contains only strings consisting of characters permitted in a file path -- just to be extra safe. I don't think the explode statement in autoRoute can possibly return a non-string value, but is there any way a user or errant developer might supply an array of non-strings to Router::validateRequest -- e.g. by a poorly defined route? Should we enforce a string type for our filter function via type hinting? // note the string type hint for the filter function
$segments = array_filter($segments, static function (string $segment): bool {
return $segment !== '';
}); this should throw an exception if there were any non-string type in $segments. Interestingly, it does not throw exceptions for If you remove the NULL from $segments, then the string-type-hinted function does permit a zero to pass into the filtered array. Note that simply adding type hinting in our filter function does introduce a 10-15% performance penalty: $segments = ["foo", "bar", 0, "0", FALSE, ""];
//$segments = ["foo", "bar", 0, "0", ""];
$iterations = 1000000;
$start = microtime(TRUE);
for($i=0; $i<$iterations; $i++) {
$segments1 = array_filter($segments, static function (string $segment): bool {
return $segment !== '';
});
}
echo "#1 elapsed: " . (microtime(TRUE) - $start) . "\n";
$start = microtime(TRUE);
for($i=0; $i<$iterations; $i++) {
$segments1 = array_filter($segments, static function ($segment): bool {
return $segment !== '';
});
}
echo "#2 elapsed: " . (microtime(TRUE) - $start) . "\n";
$start = microtime(TRUE);
for($i=0; $i<$iterations; $i++) {
$segments1 = array_filter($segments, static function (string $segment): bool {
return ($segment || $segment === '0');
});
}
echo "#3 elapsed: " . (microtime(TRUE) - $start) . "\n";
$start = microtime(TRUE);
for($i=0; $i<$iterations; $i++) {
$segments1 = array_filter($segments, static function ($segment): bool {
return ($segment || $segment === '0');
});
}
echo "#4 elapsed: " . (microtime(TRUE) - $start) . "\n"; results
As for eliminating mischief, I think we should do more. I do see some issues in our setDirectory function. In particular:
I'm not entirely sure what criteria we should use to reject a filename or directory name, but we definitely need to prevent this autorouting behavior from being used to probe the file system or run arbitrary files (e.g., in the /tmp folder or a file upload location). I also think some more specific unit tests are needed. For my reference later, Windows provides a list of forbidden file system chars and filenames. |
I've been taking a good hard look at the routing code, and would point out a few things:
I do not understand the purpose of the if (isset($this->directory)) {
return $segments;
} There are a couple of places in the Router class checking namespace Sub\Φ\Myspace;
class Φoo {
public function foo() {
echo "woop";
}
}
$v = new Φoo;
$v->foo(); Periods and dashes (and punctuation generally) are not permitted in namespace declarations, nor are any segments starting with or consisting entirely of numbers. These are invalid: Both I don't think the setDirectory function should be applying ucfirst to the supplied dir value or stripping periods. It could perhaps check the supplied |
…r subdirectories. fix issue codeigniter4#4294
…r subdirectories. fix issue codeigniter4#4294
…r subdirectories. fix issue codeigniter4#4294
…r subdirectories. fix issue codeigniter4#4294
…r subdirs * updated Router to properly translate uri dashes that map to controller subdirectories. fix issue #4294 * changed Router::validateRequest to Router::scanControllers, added isValidSegment, tweaks to enforce PSR4 compliance in directory/namespace segments * PHPCBF cleanup * added numerous dash tests, etc. to RouterTest * Router fixes: fix errant return true in setDirectory, fix (bool) cast in isValidSegment, add back deprecated validateRequest function, add RouterTest methods for better code coverage * fixed broken assertions in RouterTest::testRouterPriorDirectory * added space after boolean cast as requested by paulbalandan
Describe the bug
CodeIgniter 4's "translate uri dashes" functionality does not work under certain circumstances. If you want dashes in your uri that map into dashes in some controller subdirectory name, then you cannot add a route targeting controllers in that subdirectory.
I have an earlier post on the Codeigniter forum relating to this where I am trying to support URLs with dashes in them. Long story short, I must use dashes in my controller subdirectory names if I want to have dashes in my url. For whatever reason, the CI4 auto-routing is unable to recognize when a url with a dash in the path is referring to a controller subdirectory with an underscore.
E.g., if I want this url to connect to the default Home controller in my Nested-directory subdirectory:
https://www.example.com/subdir/nested-directory
Then I have to use a dash in the file system directory name. That url works if I put my controller in this location:
app/Controllers/Subdir/Nested-directory/Home.php
Note that the properly working Home controller uses an underscore in its namespace:
That same url does not work if I put it in a directory with an underscore:
app/Controllers/Subdir/Nested_directory/Home.php
Curiously, it the dash in the url I request
does
get properly routed to an underscore while CI4 is calculating the controller path. However, CI4 doesn't bother to check if the url is requesting the default Home controller in a subdirectory. The complaint it gives is:So it almost works, is just doesn't bother checking for the default Home controller in a subdir. Additionally, if I change my url to have an underscore in it, then CI4 can locate the Home controller in the subdirectory named with an underscore. This url:
https://www.example.com/subdir/nested_directory
Connects correctly to the controller here:
app/Controllers/Subdir/Nested_directory/Home.php
Previously, it was enough to simply make sure I name my subdirectories with dashes so they match the urls, however this does not work when i must add a $routes->add directive mapping urls to a controller that happens to live in a subdirectory with a dash. This route directive works just fine in my Config/Routes file if Nested_directory contains an underscore:
This serves a request for https://www.example.com/foobar/123 with the correct Home controller.
But if if the Nested-directory subdirectory containing my Home controller contains a dash, meaning my Home controller file is
app/Controllers/Subdir/Nested-directory/Home.php
then this will not work:
This will also not work:
Please note that I need to have these dashes instead of underscores because that is recommended by Google. I also very much do not want to create a big, complicated Routes file where I must route all of my many controllers. The auto-routing of CI3 was working just fine. I think this problem could be remedied with a small code change.
Can anyone point me to a solution?
For reference, here is my Config/Routes.php file
CodeIgniter 4 version
4.1.1
Affected module(s)
system/Router ? not entirely sure.
Expected behavior, and steps to reproduce if appropriate
Controllers/Subdir/Nested_directory::index
Controllers/Subdir/Nested-directory/Home::index
Controllers/Subdir/Nested_directory/Home::index
to locate the controller at
Controllers/Subdir/Nested-directory/Home::index
. Note this dash translation for added routes may not be necessary if the autorouting feature can properly translate uri dashes.Context
The text was updated successfully, but these errors were encountered: