I have an api test that asserts for redirect to a specific route. The issue is the redirect url has query parameters that includes a timestamp which changes. Wondering if Laravel's assertions has an an alternative to assertRedirect method that ignores query parameters.
/** #test */
public function test_can_redirect()
{
$this->call('GET', "users/auth")
->assertRedirect('http://localhost:8000/dashboard?timestamp=1550848436');
}
I want to assert a redirect to
http://localhost:8000/dashboard
not
http://localhost:8000/dashboard?timestamp=1550848436
You could use getTargetUrl to extract what is without query string.
public function test_can_redirect()
{
$redirectUrl = 'http://localhost:8000/dashboard';
$res = $this->getJson('users/auth')->assertStatus(302); # Check if redirection
# Or you could use this: $this->assertTrue($res->isRedirection());
$parts = explode('?', $res->getTargetUrl());
$this->assertTrue($parts[0] === $redirectUrl); # parts[0] is the url
wihtout query string
# If you want to check the keys of query string, too
$this->assertTrue(count($parts) === 2); # check query string exists
parse_str($parts[1], $query);
$this->assertArrayKeys($query, ['time', ..., 'the key you want to check']);
}
protected function assertArrayKeys(array $array, array $checks)
{
$keys = array_keys($array);
foreach ($checks as $check) {
$this->assertTrue(in_array($check, $keys));
}
}
The principle behind testing your code is to make sure that what is happening is what is expected, so if you are expecting the query parameter of a timestamp, why not build it up too?
/** #test */
public function test_can_redirect()
{
$url = "/dashboard";
// http://php.net/manual/en/function.time.php
$timestamp = time();
$this->call('GET', "users/auth")
->assertRedirect($url . '?timestamp=' . $time);
}
Related
everyone.
I have created very basic router in PHP and now I am stuck.
The user can navigate to different URLs and pass parameters that can be used to display data for example to get data from an array.
However I am stuck, I do not know how to pass these url parameters so they can be used inside a file.
For example this route
"/user/:id" -> If user navigates to /user/1 -> This executes a callback function and he receives data from an array.
However when the url doesn't have callback function but has a name of a file, the router will load a file, for example the user page.
Router::get("/user/:username", "user.php");
So my question is How can I get the "username" from the route and pass it into the user.php file ?
I have tried using $_GET['username'], however that doesn't work as the url doesn't have ? inside of it.
This is my code
<?php
class Router{
public static $routes = [];
public static function get($route, $callback){
self::$routes[] = [
'route' => $route,
'callback' => $callback,
'method' => 'GET'
];
}
public static function resolve(){
$path = $_SERVER['REQUEST_URI'];
$httpMethod = $_SERVER['REQUEST_METHOD'];
$methodMatch = false;
$routeMatch = false;
foreach(self::$routes as $route){
// convert urls like '/users/:uid/posts/:pid' to regular expression
$pattern = "#^" . preg_replace('/\\\:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', preg_quote($route['route'])) . "$#D";
$matches = Array();
// check if the current request matches the expression
if(preg_match($pattern, $path, $matches) && $httpMethod === $route['method']) {
$routeMatch = true;
// remove the first match
array_shift($matches);
// call the callback with the matched positions as params
if(is_callable($route['callback'])){
call_user_func_array($route['callback'], $matches);
}else{
self::render($route['callback']);
}
}
}
if(!$routeMatch){
self::notFound();
}
}
public static function render($file, $viewsFolder='./views/'){
include($viewsFolder . $file);
}
public static function notFound(){
http_response_code(400);
include('./views/404.php');
exit();
}
}
Router::get("/", "home.php");
Router::get("/user/:id", function($val1) {
$data = array(
"Nicole",
"Sarah",
"Jinx",
"Sarai"
);
echo $data[$val1] ?? "No data";
});
Router::get("/user/:username", "user.php");
Router::get("/user/profile/:id", "admin.php");
Router::resolve();
?>
You could pass $matches to the render() method as second optional parameter, and that's it. As well as these variables are accessible in the method scope, they are accessible in all the files included/required from this scope. I.e.:
self::render($route['callback'], $matches);
and in the included file:
print_r($matches);
UPD: In order to IDE not highlighting "unknown" variable, you can add a phpdoc-block somewhere in the included file, like this:
/** #var array $matches */
I need to simulate routing in Codeigniter 3, so my question is how to get the right-hand side from any URL programmatically?
for example, some routes that I have:
$route["blog"] = "Main/blog/en";
$route["blog/(:any)"] = "Main/blog/en/$1";
$route["novosti"] = "Main/blog/sr";
$route["novosti/(:any)"] = "Main/blog/sr/$1";
$route["contact"] = "Main/contact/en";
$route["kontakt"] = "Main/contact/sr";
Now I need a function that can return right-hand side for a given URL part something like this:
echo $this->route->item("novosti/petar")
should print then Main/blog/sr/$1 or Main/blog/sr/petar
Is there such a function in Codeigniter, because I can't find it in documentation?
UPDATE:
I am looking throughout system/router class and I see that protected function _parse_routes is doing something similar so if there is no function that can give me what I need I will create one based on this one.
use this
$this->router->routes['blog']
you will get
Main/blog/en
Codeigniter is simple, too simple... And because it is not obvious for me where that function is (if exists at all) I've just adopted _parse_routes to parse URL (slug) into the right-hand side from which I can find the corresponding file much easier.
Here it is (if someone gets in the same situation I was).
function parseRoute($uri) {
// Get HTTP verb
$http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli';
// Loop through the route array looking for wildcards
foreach ($this->router->routes as $key => $val) {
// Check if route format is using HTTP verbs
if (is_array($val)) {
$val = array_change_key_case($val, CASE_LOWER);
if (isset($val[$http_verb])) {
$val = $val[$http_verb];
} else {
continue;
}
}
// Convert wildcards to RegEx
$key = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $key);
// Does the RegEx match?
if (preg_match('#^' . $key . '$#', $uri, $matches)) {
// Are we using callbacks to process back-references?
if (!is_string($val) && is_callable($val)) {
// Remove the original string from the matches array.
array_shift($matches);
// Execute the callback using the values in matches as its parameters.
$val = call_user_func_array($val, $matches);
}
// Are we using the default routing method for back-references?
elseif (strpos($val, '$') !== FALSE && strpos($key, '(') !== FALSE) {
$val = preg_replace('#^' . $key . '$#', $val, $uri);
}
return $val;
}
}
// If we got this far it means we didn't encounter a
// matching route so we'll set the site default route
return null;
}
Now, this:
echo parseRoute("novosti/petar")
will produce:
Main/blog/sr/petar
AKA: Controler class / function inside that controller / language param / blog article
You can fetch the required info using the below code.
$this->router->routes['novosti/(:any)'];
I am newly trying out TDD with laravel, and I want to assert if a redirect took a user to a url that has an integer param.
I wonder if I could use regex to catch all positive integers.
I'm running this app with the laravel 5.8 framework and I know that the url parameter is 1 because I refresh the database each for each test, so setting the redirect url as /projects/1 works but this sort of hardcoding feels weird.
I've attached a block of code I tried using regex for but this doesn't work
/** #test */
public function a_user_can_create_projects()
{
// $this->withoutExceptionHandling();
//If i am logged in
$this->signIn(); // A helper fxn in the model
//If i hit the create url, i get a page there
$this->get('/projects/create')->assertStatus(200);
// Assumming the form is ready, if i get the form data
$attributes = [
'title' => $this->faker->sentence,
'description' => $this->faker->paragraph
];
//If we submit the form data, check that we get redirected to the projects path
//$this->post('/projects', $attributes)->assertRedirect('/projects/1');// Currently working
$this->post('/projects', $attributes)->assertRedirect('/^projects/\d+');
// check that the database has the data we just submitted
$this->assertDatabaseHas('projects', $attributes);
// Check that we the title of the project gets rendered on the projects page
$this->get('/projects')->assertSee($attributes['title']);
}
I expected the test to treat the argument in assertRedirect('/^projects/\d+'); as regex and then pass for any url like /projects/1 so far it ends in a number, but it takes it as a raw string and expects a url of /^projects/\d+
I'd appreciate any help.
After watching a tutorial by Jeffery Way, he talked about handling this issue.
Here's how he solves the situation
//If we submit the form data,
$response = $this->post('/projects', $attributes);
//Get the project we just created
$project = \App\Project::where($attributes)->first();
// Check that we get redirected to the project's path
$response->assertRedirect('/projects/'.$project->id);
This is not possible by now. You need to test the Location header in the response with a regular expression.
This is a problem because you cann't use the current route name. That's why I did two functions that bring a little bit of readability to your test. You will use this function like this:
// This will redirect to some route with an numeric ID in the URL.
$response = $this->post(route('groups.create'), [...]);
$this->assertResponseRedirectTo(
$response,
$this->prepareRoute('group.detail', '[0-9]+'),
);
This is the implementation.
/**
* Assert whether the response is redirecting to a given URI that match the pattern.
*/
public function assertResponseRedirectTo(Illuminate\Testing\TestResponse\TestResponse $response, string $url): void
{
$lastOne = $this->oldURL ?: $url;
$this->oldURL = null;
$newLocation = $response->headers->get('Location');
$this->assertEquals(
1,
preg_match($url, $newLocation),
sprintf('Should redirect to %s, but got: %s', $lastOne, $newLocation),
);
}
/**
* Build the pattern that match the given URL.
*
* #param mixed $params
*/
public function prepareRoute(string $name, $params): string
{
if (! is_array($params)) {
$params = [$params];
}
$prefix = 'lovephp';
$rep = sprintf('%s$&%s', $prefix, $prefix);
$valuesToReplace = [];
foreach ($params as $index => $param) {
$valuesToReplace[$index] = str_replace('$&', $index . '', $rep);
}
$url = preg_quote(route($name, $valuesToReplace), '/');
$this->oldURL = route($name, $params);
foreach ($params as $index => $param) {
$url = str_replace(
sprintf('%s%s%s', $prefix, $index, $prefix),
$param,
$url,
);
}
return sprintf('/%s/', $url);
}
I have a Codeigniter controller which takes a full URL as the first argument, but the passed URL inside my controller only is only showing http:
public function mydata($link)
{
echo $link; //then it show only http: rather than the full url http://abc.com
}
How can i solve this issue?
if you want to pass url as parameters then use
urlencode(base64_encode($str))
ie:
$url=urlencode(base64_encode('http://stackoverflow.com/questions/9585034'));
echo $url
result:
aHR0cDovL3N0YWNrb3ZlcmZsb3cuY29tL3F1ZXN0aW9ucy85NTg1MDM0
then you call:
http://example.com/mydata/aHR0cDovL3N0YWNrb3ZlcmZsb3cuY29tL3F1ZXN0aW9ucy85NTg1MDM0
and in your controller
public function mydata($link)
{
$link=base64_decode(urldecode($link));
...
...
...
you have an encoder/decoder here:
http://www.base64decode.org/
In Codeigniter controllers, each method argument comes from the URL separated by a / slash. http://example.com
There are a few different ways to piece together the the arguments into one string:
public function mydata($link)
{
// URL: http://example.com/mysite/mydata/many/unknown/arguments
// Ways to get the string "many/unknown/arguments"
echo implode('/', func_get_args());
echo ltrim($this->uri->uri_string(), '/');
}
However:
In your case, the double slash // may be lost using either of those methods because it will be condensed to one in the URL. In fact, I'm surprised that a URL like:
http://example.com/mydata/http://abc.com
...didn't trigger Codeigniter's "The URI contains disallowed chatacters" error. I'd suggest you use query strings for this task to avoid all these problems:
http://example.com/mydata/?url=http://abc.com
public function mydata()
{
$link = $this->input->get('url');
echo $link;
}
Aside from the issue of whether you should be passing a URL in a URL think about how you are passing it:
example.com/theparameter/
but your URL will actually look like
example.com/http://..../
See where you're going wrong yet? The CodeIgniter framework takes the parameter out of the URL, delimited by slashes. So your function is working exactly as it should.
If this is how you must do it then URL encode your parameter before passing it.
You can try this. It worked fr me.
"encode" the value before passing
$value = str_replace('=', '-', str_replace('/', '_', base64_encode($album)));
"decode" the value after receiving
$value = base64_decode(str_replace('-', '=', str_replace('_', '/', $value)));
reference: https://forum.codeigniter.com/printthread.php?tid=40607
I did like #user72740's until I discovered that it can still produce characters not permitted by CI like %.
What I ended up doing is converting the segment string into a hex, them back.
So I created a MY_URI that extended CI_URI and added these methods:
/**
* Segmentize
*
* Makes URI segments, CI "segment proof"
* Removes dots and forwardslash leaving ONLY hex characters
* Allows to pass "anything" as a CI URI segment and coresponding function param
*
* #access public
* #return string
*/
public function segmentize($segment){
if(empty($segment)){
return '';
}
return bin2hex($segment);
}
/**
* Desegmentize
*
* #access public
* #return string
*/
public function desegmentize($segment){
if(empty($segment)){
return '';
}
return $this->hex2bin($segment);
}
/**
* hex2bin
*
* PHP 5.3 version of 5.4 native hex2bin
*
* #access public
* #return string
*/
public function hex2bin($hex) {
$n = strlen($hex);
$bin = '';
$i = 0;
while($i < $n){
$a = substr($hex, $i, 2);
$c = pack('H*', $a);
if ($i == 0){
$bin = $c;
}
else {
$bin .= $c;
}
$i += 2;
}
return $bin;
}
Then used $this->uri->segmentize($url) to create the segment string and
$this->uri->desegmentize($this->input->post('url', true)) to get it back into readable format.
Thus
https://www.example.com/somewhere/over/the/rainbow
becomes
68747470733a2f2f7777772e6d79736974652e636f6d2f736f6d6577686572652f6f7665722f7468652f7261696e626f77
and back.
I am sure there is a better way, like a base_convert() implementation, because this way the string can get arbitrarily long. But now I dont have to worry about = signs and padding, etc.
I am trying to build a router function to properly match incoming URI's and match them to an array of stored system URI's. I also have wildcards '(:any)' and '(:num)' similar to CodeIgniter.
Basically, I am trying to get the 'admin/stats/(:num)' entry to match on both 'admin/stats' and admin/stats/1'.
While the script is starting I grab all paths from a separate array and use a foreach to save each path:
route('admin/stats/(:num)', array('#title' => 'Statistics',...));
The function is:
function route($path = NULL, $options = NULL) {
static $routes;
//If no arguments are supplied, return all routes stored.
if(!isset($path) && !isset($options)) {
return $routes;
}
//return options for path if $path is set.
if(isset($path) && !isset($options)) {
//If we have an exact match, return it.
if(array_key_exists($path, $routes)) {
return $routes[$path];
}
//Else, we need to use RegEx to find the correct route options.
else {
$regex = str_replace('/', '\/', $path);
$regex = '#^' . $regex . '\/?$#';
//I am trying to get the array key for $route[$path], but it isn't working.
// route_replace('admin/stats/(:num)') = 'admin/stats/([0-9]+)'.
$uri_path = route_replace(key($routes[$path])); //route_replace replaces wildcards for regex.
if(preg_match($regex, $uri_path)) {
return $routes[$path];
}
}
}
$routes[$path] = $options;
return $routes;
}
Route replace function:
function route_replace($path) {
return str_replace(':any', '.+', str_replace(':num', '[0-9]+', $path));
}
A key/value pair in the $routes array looks like:
[admin/stats/(:num)] => Array
(
[#title] => Statistics //Page title
[#access] => user_access //function to check if user is authorized
[#content] => html_stats //function that returns HTML for the page
[#form_submit] => form_stats //Function to handle POST submits.
)
Thanks for the help. This is my first router and I am not that familiar in making proper Regex's.
'admin/stats/(:num)' will never match 'admin/stats' as in your "pattern" the slash is required. In pseduo-regex you need to do something like 'admin/stats(/:num)'.
There does also seem to be a few bugs in your code. This line
$uri_path = route_replace(key($routes[$path]));
is in the block that is executed when $path is not a key that exists in $routes.
I've tried to rewrite it and this seems to work (this is just the else clause):
foreach( array_keys( $routes ) as $route ) {
$regex = '#^' . $route . '?$#';
//I am trying to get the array key for $route'$path', but it isn't working.
// route_replace('admin/stats/(:num)') = 'admin/stats/('0-9'+)'.
$uri_path = route_replace($regex); //route_replace replaces wildcards for regex.
if(preg_match($uri_path,$path)) {
return $routes[$route];
}
}
But this requires 'admin/stats/(:num)' to be 'admin/stats(/:num)'.
btw if you don't have one already, you should get a debugger (Zend and xDebug are two of the most common ones for PHP). They can be invaluable in solving problems like this.
Also, ask yourself if you need to write a router, or whether you can't just use one of the perfectly good ones out there already...