PHP Klein Uncaught Error: Wrong Parameters - php

I'm trying to build a PHP REST API using Klein as the routing library. Trying to test one of the get routes throw the exception below:
<br />
<b>Fatal error</b>: Uncaught Error: Wrong parameters for Klein\Exceptions\UnhandledException([string
$message [, long $code [, Throwable $previous = NULL]]]) in C:\DA4NA4\Web\task-
scheduler\vendor\klein\klein\src\Klein\Klein.php:954
Stack trace:
#0 C:\DA4NA4\Web\task-scheduler\vendor\klein\klein\src\Klein\Klein.php(954): Exception-
>__construct('SQLSTATE[HY000]...', 'HY000', Object(PDOException))
#1 C:\DA4NA4\Web\task-scheduler\vendor\klein\klein\src\Klein\Klein.php(645): Klein\Klein-
>error(Object(PDOException))
#2 C:\DA4NA4\Web\task-scheduler\public\index.php(20): Klein\Klein->dispatch()
#3 {main}
thrown in <b>C:\DA4NA4\Web\task-scheduler\vendor\klein\klein\src\Klein\Klein.php</b> on line
<b>954</b><br />
My routes file contained the following codes:
<?PHP
require_once '../bootstrap.php';
use Api\Controllers\UserController;
$users = new UserController();
/**
* user routes
*/
//1. get user
$klein->respond('GET', '/api/users/[:username]', function($request){
global $users;
return $users->getUser($request->username);
});
//dispatch routes
$klein->dispatch();
And finally, here's the method from the UserController Class:
public function getUser(string $username){
/**
* process requests for a username
* #param string $username
*/
/**
* set http headers
*/
\header('Access-Control-Allow-Origin:*');
\header('Access-Control-Allow-Methods: GET');
\header('Content-Type: application/json; charset=UTF-8');
\header('Access-Control-Max-Age: 3600');
/**
* sanitize username
*/
$username = \test_input($username);
if(empty($username)){
throw new Exception('Provide username');
}
$user = $this->model->getOne($username);
if($user){
/**
* set http status code
* 200 - successfull
*/
\http_response_code(200);
/**
* encode the user records in json format
* send encode records to user
*/
echo \json_encode(['status'=>true, 'message'=>$user]);
}
}
What I'm possibly doing wrong?

The problem is that exception code is expected to be integer (long in the stacktrace), but PDOException returns strings as exception codes.
There is an open issue about this problem since 2015: https://github.com/klein/klein.php/issues/298
A bigger problem is that you are using library which has been unmaintained for 4 years (since February 2017). My advice is to look for alternative to Klein router.

Related

Symfony2 functional test client request returns error 500

I have a controller that i am trying to do a functional test for it.
controller:
<?php
namespace Zanox\AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Exception;
/**
*
* #author Mohamed Ragab Dahab <eng.mohamed.dahab#gmail.com>
*
* #Route("merchant")
*
*/
class ReportController extends Controller {
/**
* Show transaction report regarding to the given merchant ID
* #author Mohamed Ragab Dahab <eng.mohamed.dahab#gmail.com>
* #access public
*
* #Route("/{id}/report", name="merchant-report")
*
* #param int $id Merchant ID
*/
public function showAction($id) {
try {
//Order Service
$orderService = $this->get('zanox_app.orderService');
//merchant Orders
$orders = $orderService->getMerchantOrders($id);
//render view and pass orders array
return $this->render('ZanoxAppBundle:Report:show.html.twig', ['orders' => $orders]);
} catch (Exception $e) {
//log errors
}
}
}
I have created a functional test as following:
namespace Zanox\AppBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ReportControllerTest extends WebTestCase {
/**
*
*/
public function testShow() {
//Client instance
$client = static::createClient();
//Act like browsing the merchant listing page, via GET method
$crawler = $client->request('GET', '/merchant/{id}/report', ['id'=> 1]);
//Getting the response of the requested URL
$crawlerResponse = $client->getResponse();
//Assert that Page is loaded ok
$this->assertEquals(200, $crawlerResponse->getStatusCode());
//Assert that the response content contains 'Merchant Listing' text
$this->assertTrue($crawler->filter('html:contains("Merchant Report")')->count() > 0);
}
}
However this test fails as the first assertion returns status 500 instead of 200
Test log shows:
[2015-07-06 21:00:24] request.INFO: Matched route "merchant-report". {"route_parameters":{"_controller":"Zanox\AppBundle\Controller\ReportController::showAction","id":"{id}","_route":"merchant-report"},"request_uri":"http://localhost/merchant/{id}/report?id=1"} []
Letting you know that ['id' => 1] exists in DB.
First Question: why it fails?
Second Question: am i doing the functional test in a proper way?
If you look at the logs, you see that the {id} parameter is not correctly replaced but is added in the query string of your Uri. So try with:
$crawler = $client->request('GET', sprintf('/merchant/%d/report', 1));
When using GET, the third parameter will add query parameters for the URI, when using POST, these data will be posted.
As to why it fails - you can troubleshoot the problem by using a debugger to step through the controller code when it is executed in your test. For your second question, yes, you are doing a simple functional test correctly.

Laravel, WebSockets - Verify user on server

I currently ran into the problem of handling the authentification of a user on the server, using Laravel and RachetPHP.
What I tried so far:
I changed the driver type of the session to database, giving me an id and payload column. Using \Session::getId() returns a 40 character string.
The cookie information, sent by the WebSocket-Connection does contain a XSRF-TOKEN and a laravel_session, both containing > 200 characters string. The database ID of the users session differs from the id, returned by \Session::getId().
I am already sending the current CSRF-token via the websocket message, but I have no clue how to verify it (the built-in verifier uses requests - which I don't have in the websocket server scope).
Generic Use case:
A User posts a comment in thread. The payload of the sent object would then be:
Something to verify the user (an ID or a token).
The comment itself
If you were to send the user ID, anyone could temper the packet and send the message under another ones user.
My use case:
A user can have n-characters. A character has an avatar, an id, a name, etc.
The user is only used to:
authenticate at the server.
access his characters, and thus perform basic CRUD operations on his characters.
I also have a table locations - a "virtual place", a character can be in... so I got a one-to-one relationship between character and location. The user (character) can then send messages in a location via websocket. The message is inserted at the database on the server. At this point, I need to know:
If the user is authenticated (csrf-token ?)
If the user is the owner of the character (it's very simple to spoof the request with another user's character id)
If you need more information, please let me know.
So this is how I solved this a while ago. In my example, I'm working with Socket.IO, but I'm pretty sure you can easily rewrite the Socket.IO part to get it to work with RachetPHP as well.
Socket Server
The socket server depends on the files cookie.js and array.js, and the node modules express, http, socket.io, request and dotenv. I'm not the original author of cookie.js, but there is no author mentioned in the comments, so I'm not able to give any credits for this, sorry.
This is the server.js file which starts the server. It is a simple socket server that tracks who is currently online. The interesting part however is when the server makes a POST request to socket/auth on the Laravel application:
var express = require('express');
var app = express();
var server = require('http').Server(app)
var io = require('socket.io')(server);
var request = require('request');
var co = require('./cookie.js');
var array = require('./array.js');
// This loads the Laravel .env file
require('dotenv').config({path: '../.env'});
server.listen(process.env.SOCKET_SERVER_PORT);
var activeSockets = {};
var disconnectTimeouts = {};
// When a client connects
io.on('connection', function(socket)
{
console.log('Client connected...');
// Read the laravel_session cookie.
var cookieManager = new co.cookie(socket.handshake.headers.cookie);
var sess = cookieManager.get("laravel_session"); // Rename "laravel_session" to whatever you called it
// This is where the socket asks the Laravel app to authenticate the user
request.post('http://' + process.env.SOCKET_SERVER_HOST + '/socket/auth?s=' + sess, function(error, response, body)
{
try {
// Parse the response from the server
body = JSON.parse(body);
}
catch(e)
{
console.log('Error while parsing JSON', e);
error = true;
}
if ( ! error && response.statusCode == 200 && body.authenticated)
{
// Assign users ID to the socket
socket.userId = body.user.id;
if ( ! array.contains(activeSockets, socket.userId))
{
// The client is now 'active'
activeSockets.push(socket.userId);
var message = body.user.firstname + ' is now online!';
console.log(message);
// Tell everyone that the user has joined
socket.broadcast.emit('userJoined', socket.userId);
}
else if (array.hasKey(disconnectTimeouts, 'user_' + socket.userId))
{
clearTimeout(disconnectTimeouts['user_' + socket.userId]);
delete disconnectTimeouts['user_id' + socket.userId];
}
socket.on('disconnect', function()
{
// The client is 'inactive' if he doesn't reastablish the connection within 10 seconds
// For a 'who is online' list, this timeout ensures that the client does not disappear and reappear on each page reload
disconnectTimeouts['user_' + socket.userId] = setTimeout(function()
{
delete disconnectTimeouts['user_' + socket.userId];
array.remove(activeSockets, socket.userId);
var message = body.user.firstname + ' is now offline.';
console.log(message);
socket.broadcast.emit('userLeft', socket.userId);
}, 10000);
});
}
});
});
I added some comments to the code, so it should be pretty self-explanatory. Please note that I added SOCKET_SERVER_HOST and SOCKET_SERVER_PORT to my Laravel .env-file in order to be able to change the host and port without editing the code and run the server on different environments.
SOCKET_SERVER_HOST = localhost
SOCKET_SERVER_PORT = 1337
Authenticating a user by a session cookie with Laravel
This is the SocketController which parses the cookie and responds whether the user could be authenticated or not (JSON response). Its the same mechanism that you described in your answer. It's not the best design to handle the cookie parsing in the controller, but it should be OK in this case, because the controller only handles that one thing and its functionality isn't used at another point in the application.
/app/Http/Controllers/SocketController.php
<?php namespace App\Http\Controllers;
use App\Http\Requests;
use App\Users\UserRepositoryInterface;
use Illuminate\Auth\Guard;
use Illuminate\Database\DatabaseManager;
use Illuminate\Encryption\Encrypter;
use Illuminate\Http\Request;
use Illuminate\Routing\ResponseFactory;
/**
* Class SocketController
* #package App\Http\Controllers
*/
class SocketController extends Controller {
/**
* #var Encrypter
*/
private $encrypter;
/**
* #var DatabaseManager
*/
private $database;
/**
* #var UserRepositoryInterface
*/
private $users;
/**
* Initialize a new SocketController instance.
*
* #param Encrypter $encrypter
* #param DatabaseManager $database
* #param UserRepositoryInterface $users
*/
public function __construct(Encrypter $encrypter, DatabaseManager $database, UserRepositoryInterface $users)
{
parent::__construct();
$this->middleware('internal');
$this->encrypter = $encrypter;
$this->database = $database;
$this->users = $users;
}
/**
* Authorize a user from node.js socket server.
*
* #param Request $request
* #param ResponseFactory $response
* #param Guard $auth
* #return \Symfony\Component\HttpFoundation\Response
*/
public function authenticate(Request $request, ResponseFactory $response, Guard $auth)
{
try
{
$payload = $this->getPayload($request->get('s'));
} catch (\Exception $e)
{
return $response->json([
'authenticated' => false,
'message' => $e->getMessage()
]);
}
$user = $this->users->find($payload->{$auth->getName()});
return $response->json([
'authenticated' => true,
'user' => $user->toArray()
]);
}
/**
* Get session payload from encrypted laravel session.
*
* #param $session
* #return object
* #throws \Exception
*/
private function getPayload($session)
{
$sessionId = $this->encrypter->decrypt($session);
$sessionEntry = $this->getSession($sessionId);
$payload = base64_decode($sessionEntry->payload);
return (object) unserialize($payload);
}
/**
* Fetches base64 encoded session string from the database.
*
* #param $sessionId
* #return mixed
* #throws \Exception
*/
private function getSession($sessionId)
{
$sessionEntry = $this->database->connection()
->table('sessions')->select('*')->whereId($sessionId)->first();
if (is_null($sessionEntry))
{
throw new \Exception('The session could not be found. [Session ID: ' . $sessionId . ']');
}
return $sessionEntry;
}
}
In the constructor you can see that I refer to the internal middleware. I added this middleware to only allow the socket server to make requests to socket/auth.
This is what the middleware looks like:
/app/Http/Middleware/InternalMiddleware.php
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Routing\ResponseFactory;
class InternalMiddleware {
/**
* #var ResponseFactory
*/
private $response;
/**
* #param ResponseFactory $response
*/
public function __construct(ResponseFactory $response)
{
$this->response = $response;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if (preg_match(env('INTERNAL_MIDDLEWARE_IP'), $request->ip()))
{
return $next($request);
}
return $this->response->make('Unauthorized', 401);
}
}
To get this middleware to work, register it in the Kernel and add the INTERNAL_MIDDLEWARE_IP property - that is just a regular expression defining which IP addresses are allowed - to your .env-file:
Local testing (any IP):
INTERNAL_MIDDLEWARE_IP = /^.*$/
Production env:
INTERNAL_MIDDLEWARE_IP = /^192\.168\.0\.1$/
I'm sorry I could not help you out with RachetPHP, but I think you get a good idea how this can be solved.
I think I found a solution. Although not very clean, it does what it's supposed to do (I guess...)
The WebSocket-Server gets started by an Artisan Command (by mmochetti#github). I inject these classes into the Command:
Illuminate\Contracts\Encryption\Encrypter
App\Contracts\CsrfTokenVerifier - a custom CsrfTokenVerifier, that simply compares 2 strings (going to put more of the follow logic code in there)
I pass these instances from the command to the server. On the onMessage method, I parse the message sent, containing:
The CSRF-Token of the user
The character-id of the user
I then check if the token is valid, and if the user is the owner of the character.
public function onMessage(ConnectionInterface $from, NetworkMessage $message) {
if (!$this->verifyCsrfToken($from, $message)) {
throw new TokenMismatchException;
}
if (!$this->verifyUser($from, $message)) {
throw new \Exception('test');
}
...
}
private function verifyUser(ConnectionInterface $conn, NetworkMessage $message) {
$cookies = $conn->WebSocket->request->getCookies();
$laravel_session = rawurldecode($cookies['laravel_session']);
$id = $this->encrypter->decrypt($laravel_session);
$session = Session::find($id);
$payload = unserialize(base64_decode($session->payload));
$user_id = $payload['user_id'];
$user = User::find($user_id);
$characters = $this->characterService->allFrom($user);
$character_id = $message->getHeader()['character_id'];
return $characters->contains($character_id);
}
private function verifyCsrfToken($from, NetworkMessage $message) {
$header = $this->getHeaderToken($from);
return $this->verifier->tokensMatch($header, $message->getId());
}
The code could be cleaner for sure, but as a quick hack, it works. I think, instead of using a model for the Session, I should use the Laravel DatabaseSessionHandler.
For Laravel > 5 i use this code:
$cookies = $conn->WebSocket->request->getCookies();
$laravel_session = rawurldecode($cookies['laravel_session']);
$id = $this->encrypter->decrypt($laravel_session);
if(Config::get('session.driver', 'file') == 'file')
{
$session = File::get(storage_path('framework/sessions/' . $id));
}
$session = array_values(unserialize($session));
return $session[4]; // todo: Hack, please think another solution
To get cookies from client through websocket you must change domain in session config and change everywhere websocket host to your domain:
'domain' => 'your.domain.com',

Why PHPUnit does not assert correctly a request with an unvalid method?

what I am trying to do is asserting a request with an unvalid method.
What I testing is a Guest attempting to logging out as GET (unvalid).
This is the code:
/**
* Enables router filters.
*
* #return void
*/
public function enableRouterFilters()
{
$this->app['router']->enableFilters();
}
[...]
/**
* Tests users/logout route as a Guest with GET.
*
* #return void
*/
public function testRouteToUsersLogoutAsGuestWithGetMethod()
{
$this->enableRouterFilters();
$response = $this->call('GET', '/users/logout');
$this->assertResponseStatus(405);
}
[...]
Instead of asserting true, PHPUnit complains about it:
[...]
There was 1 error:
1) RoutesTest::testRouteToUsersLogoutAsGuestWithGetMethod
Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException:
[...]
FAILURES!
Tests: 7, Assertions: 11, Errors: 1.
Why it gives me an error if the assertion should be indeed true?
The exception being thrown is "MethodNotAllowedHttpException". This means that you do not have a route set up to catch requests to this particular URL & request type (GET).
What you are actually looking to do is tell PHPUnit that you are expecting a certain exception be thrown, in this case: Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
Try annotating your method with:
/**
* #expectsException Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
**/
For full docs: https://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.exceptions.examples.ExceptionTest.php

Customize Exception Template by Bundle

How to customize exception by bundle?
Example:
I have two bundles: BackendBundle and FrontEndBundle. I want this two bundles to be handled by two different templates when error 404 is thrown.
How can I do that?
I had read http://symfony.com/doc/current/cookbook/controller/error_pages.html but still got no clues.
Like in the cookbook article mentioned, extend the TwigBundle and the Symfony\Bundle\TwigBundle\Controller\ExceptionController:findTemplate. There you can decide (if it's not in debug) which 404 to show.
This example assumes all you backend routes are reachable under /backend. Change it to your needs, or use other things from the request to determine your backend 404s.
namespace Acme\ErrorBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController as BaseController;
/**
* ExceptionController.
*/
class ExceptionController extends BaseController
{
/**
* #param Request $request
* #param string $format
* #param integer $code An HTTP response status code
* #param Boolean $debug
*
* #return TemplateReference
*/
protected function findTemplate(Request $request, $format, $code, $debug)
{
// find template for backend 404 errors
if (!$this->debug && 404 == $code && false !== strpos($request->getPathInfo(), '/backend')) {
$template = new TemplateReference('TwigBundle', 'Exception', 'backend404', $format, 'twig');
if ($this->templateExists($template)) {
return $template;
}
}
// the parent method finds the error404.html.twig for the frontend
return parent::findTemplate($request, $format, $code, $debug);
}
}
Also to mention, the ErrorBundle must inherit from the TwigBundle.
You can hook to the KernelEvents::EXCEPTION event and override the response that will be sent to the browser. I wrote a quick gist for you:
https://gist.github.com/bezhermoso/87716a9c72a1d12c5036
However, $event->getRequest()->get('_controller') will return null on 404 errors, obviously. So you have to account for that instance.

Symfony2 FOS RestBundle Test

I`m trying to write some functional tests for a REST API, created using FOS Rest Bundle.
The problem is that when I use the Symfony\Component\BrowserKit, symfony throws me the following error:
{"message":"Unable to find template \"AccountBundle:Account:list.html.twig\". .. }
The code that I run is:
$client = static::createClient();
$client->request('GET','/account');
When I run the request from the browser, it works fine.
Here is the controller:
/**
* Get channel by ID
* #Secure(roles="ROLE_USER")
* #RestView()
* #ApiDoc(
* resource=true,
* description="Get channel by id",
* section="Channel",
* output="Channel"
* )
*/
public function getAction(Channel $channel)
{
return array('channel' => $channel);
}
So when in test scenario, instead of returning the JSON tries to load the template.
You should use the $server parameter of the $client-request() method to set the Accept header to application/json. FOSRestBundle has a listener that returns JSON only if the corresponding Accept header is received, otherwise it will search for the template corresponding to the controller.
$client->request('GET', '/account', array(), array(), array('HTTP_ACCEPT' => 'application/json'));

Categories