Set up Monolog in Laravel global middleware - php

In my local environment I want all logs (all flags) to go the browser's console (BrowserConsoleHandler) and then to the default StreamHandler.
In production, I want the errors and other critical messages to go to an e-mail then stored in the database or (if fails) to a log file (default StreamHandler)
I want to set up this in a global middle-ware that I have created, which looks like this now:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Monolog\Logger;
use Monolog\Handler\BrowserConsoleHandler;
class GlobalConfig
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
// Get Monolog instance
$monolog = Log::getMonolog();
// In local environment the logs will be shown on browser
if (App::environment('local')) {
// Show logs in browser console
$monolog -> pushHandler(new BrowserConsoleHandler());
} else {
// Here we set up for production
}
Log::debug("Browser handler working");
return $next($request);
}
}
This doesn't work (the message is stored in the log file only, not shown on console). What I can't figure out is how to let the Log facade know about this new handler, because here, as is obvious, things are only changed within the function's scope. I know I can do it in bootstrap/app.php but isn't it to early to get the environment? Also, if I need to save logs to the database, it must already be connected, I guess

You can try to do next in bootstrap\app.php
if(env('APP_ENV') == 'local') {
$app->configureMonologUsing(function($monolog) use ($app) {
$monolog->pushHandler(
$handler = new \Monolog\Handler\RotatingFileHandler(
$app->storagePath().'/logs/laravel.log',
$app->make('config')->get('app.log_max_files', 30),
\Monolog\Logger::DEBUG
)
);
$handler->setFormatter(new \Monolog\Formatter\LineFormatter(null, null, true, true));
$monolog->pushHandler(new \Monolog\Handler\NativeMailerHandler(
'to#mail.com',
'Log::error!',
'from#mail.com'
));
});
}
Idea here is to fully control your logging on live/local. Bad news - configureMonologUsing replace all default laravel loggers, so you need to configure all your logs manually here.

Middleware isn't really the place for this, unless you wanted to actually make a particular log entry at some point in the request cycle.
Your call to pushHandler belongs in bootstrap/app.php (in Laravel 5.2), according to the docs.
I would think it would be possible to extract this logic out into a provider in case it becomes complicated and you need to move it out of bootstrap/app.php

solution here
BrowserConsoleHandler sends the script snipped after finishing the php
script by register_shutdown_function(). At this time, Laravel already
sent the full response to the browser. So the script snipped from
BrowseConsoleHandler gets generated but never sent to the browser.

Related

Laravel broadcasting auth route simply returns "true"

I have my pusher key set and initialized within Laravel 5.3. When I test it on my local environment, it works. When I try to run the exact same code on our production environment, I get this error:
Pusher : Error : {"type":"WebSocketError","error":{"type":"PusherError","data":{"code":null,"message":"Auth info required to subscribe to private-App.User.16"}}}
I've confirmed the Pusher key is identical on both my local and production.
The WS initializes on both environments the same:
wss://ws.pusherapp.com/app/264P9d412196d622od64d?protocol=7&client=js&version=4.1.0&flash=false
The only difference that I can see, is that when our production server contacts the Laravel "broadcasting/auth" route, it simply receives true in the response body.
When my local contacts "broadcasting/auth" it gets this in the response:
{auth: "22459d41299d6228d64d:df5d393fe37df0k3832fa5556098307f145d7e483c07974d8e7b2609200483f8"}
Within my BroadcastServiceProvider.php:
public function boot()
{
Broadcast::routes();
// Authenticate the user's personal channel.
Broadcast::channel('App.User.*', function (User $user, $user_id) {
return (int)$user->id === (int)$user_id;
});
}
What could cause the broadcast/auth route to return simply true instead of the expected auth?
If you check PusherBroadcaster.php file, you will see that the response can be "mixed".
I think the documentation is saying about the default broadcast only.
The channel method accepts two arguments: the name of the channel and
a callback which returns true or false indicating whether the user is
authorized to listen on the channel.
This is the validAuthenticationResponse method inside PusherBroadcast.
/**
* Return the valid authentication response.
*
* #param \Illuminate\Http\Request $request
* #param mixed $result
* #return mixed
*/
public function validAuthenticationResponse($request, $result)
{
if (Str::startsWith($request->channel_name, 'private')) {
return $this->decodePusherResponse(
$this->pusher->socket_auth($request->channel_name, $request->socket_id)
);
}
return $this->decodePusherResponse(
$this->pusher->presence_auth(
$request->channel_name, $request->socket_id, $request->user()->getAuthIdentifier(), $result)
);
}
Just to give you another example, this is inside RedisBroadcast.
if (is_bool($result)) {
return json_encode($result);
}
Short explanation about this "auth request":
BroadcastManager instantiate all "available drivers" (Pusher, Redis, Log,etc) , and create the "auth" route (using BroadcastController + authenticate method).
When you call "auth", this will happen:
Call "broadc.../auth" route.
BroadcastManager will instantiate the proper driver (in your case Pusher)
PusherBroadcaster can throw an exception AccessDeniedHttpException if the user is not authenticated (the "user session" - Auth::user() is not defined/null) and is trying to access a private (or presence) channel type.
If the user is trying to access a private/presence channel and the user is authenticated (Auth::check()), Laravel will check if the auth. user can access the channel. (Check: verifyUserCanAccessChannel method).
After that, validAuthenticationResponse method will be called. This method will make a request to pusher with the user credentials and return an array. This array contains Pusher response (socket auth: https://github.com/pusher/pusher-http-php/blob/03d3417748fc70a889c97271e25e282ff1ff0ae3/src/Pusher.php#L586 / Presence Auth: https://github.com/pusher/pusher-http-php/blob/03d3417748fc70a889c97271e25e282ff1ff0ae3/src/Pusher.php#L615) which is a string.
Short answer:
Soo.. Pusher require this auth response. Otherwise you won't be able to connect/identify the user (wss://ws.pusherapp.com....).
Edit This is from the version 5.5 docs, not applicable here.
I think the issue maybe with using the '*' wildcard in the channel's name.
I use the following in both local and production:
Broadcast::channel("servers.{id}", function (Authenticatable $user, $id) {
return (int)$user->getAuthIdentifier() === (int)$id;
});
The problem happens because you did not set the proper BROADCAST_DRIVER in your production .env file (which is redis by default).
# before
BROADCAST_DRIVER=redis
# after
BROADCAST_DRIVER=pusher

Chrome pre-fetch causes duplicate flash message

In my Symfony 3.0.6 application, certain common routes are not available until a user's registration has been finalised. Until this has been done, I use an AccessDeniedHandlerInterface to redirect them to a route that prompts them to finalise it. I also use a flash message to explain what has happened:
/**
* Handles an access denied failure.
*
* #param Request $request
* #param AccessDeniedException $accessDeniedException
*
* #return Response may return null
*/
public function handle(Request $request, AccessDeniedException $accessDeniedException)
{
if (null === $token = $this->security->getToken()) {
return;
}
if (!is_object($user = $token->getUser())) {
return;
}
/** #var User $user */
if (!$user->isFinalised()) {
$request->getSession()->getFlashBag()->add('info', 'You need to complete your registration before you can do this!');
return new RedirectResponse($this->router->generate('app_registration_complete'));
}
return;
}
However, a problem arises with Chrome's pre-fetch/pre-rendering service - if Chrome decides a URL is likely to be visited, it will pre-fetch it. A pre-fetch results in the flash message above being added to the user's session, so any subsequent page load means there are shown the message. If they do in fact go to a route they are not yet allowed to access, they see the message twice - one generated during the pre-fetch, and one generated when they actually navigated.
A similar issue is described in this question, but the accepted answer (use a POST for logout requests) doesn't really seem like the right solution in this context. The flash message is more of an explanatory addition to a GET redirection, as opposed to something that changes the state of the application. Also, there are many ways a user might reach one of these disallowed routes, and it doesn't make sense to me to convert all of these to trigger a POST, just to get around a pre-fetch issue in certain situations.
So... how should I deal with this?
I see one problem with your code. You should change to this:
if (null === $this->security->getToken()) {
return;
}
I don't think this will resolve the problem though.

Laravel logging with Monolog\Handler\BrowserConsoleHandler

How can Laravel 5's logging be changed to Monolog\Handler\BrowserConsoleHandler?
What doesn't work in Laravel 5 but does work in a standalone PHP file:
use Illuminate\Support\Facades\Log;
use Monolog\Handler\BrowserConsoleHandler;
use Monolog\Logger;
// create a log channel
$log = Log::getMonolog();
// $log = new Logger('Testlogger'); //doesn't make any difference
$log->pushHandler(new BrowserConsoleHandler(\Psr\Log\LogLevel::INFO));
// add records to the log
$log->addWarning('Foo');
$log->addError('Bar');
All that happens is that my logs appear in the logfile but don't find their way to the browser. If I try the code in a single PHP file without framework it works, so I assume it's a Laravel problem.
I get it working with Firebug and FirePHP installed and $log->pushHandler(new FirePHPHandler()); instead of BrowserConsoleHandler but this is not a solution since it sends the logs with headers but I already sent some debug-echos when the logger wants to send the headers.
BrowserConsoleHandler on the other hand adds a JavaScript snippet to the end of the site that perfectly fits my needs.
So, did anyone succeed in adding BrowserConsoleHandler to Laravel's logging? How?
After reading plenty of source code and getting xdebug to work I finally figuered it out:
BrowserConsoleHandler sends the script snipped after finishing the php script by register_shutdown_function(). At this time, Laravel already sent the full response to the browser. So the script snipped from BrowseConsoleHandler gets generated but never sent to the browser.
As a workaround you can build your own Middleware (http://laravel.com/docs/5.0/middleware) that invokes the code generation manually and adds it to the response before it gets sent.
Create app/Http/Middleware/LogBrowserConsole.php:
<?php
namespace App\Http\Middleware;
use Illuminate\Contracts\Routing\Middleware;
use Illuminate\Support\Facades\Log;
use Monolog\Handler\BrowserConsoleHandler;
class LogBrowserConsole implements Middleware {
public function handle($request, \Closure $next)
{
// add BrowserConsoleHandler to Laravel's Logger
$log = Log::getMonolog();
$log->pushHandler(new BrowserConsoleHandler(\Psr\Log\LogLevel::INFO));
// invokes all your stuff like it would do without the middleware but with the new logger
$response = $next($request);
// after the request is done we care about the log entries
$handlers = $log->getHandlers();
$scriptSnippet = "";
foreach($handlers as $handler){ // only handle BrowserConsoleHandler
if($handler instanceof BrowserConsoleHandler){
ob_start(); //start output buffer so we can save echo to variable
$handler->send(); // create the scriptSnipped
$scriptSnippet .= ob_get_clean();
}
}
// write scriptSnippet to end of response content
$content = $response->getContent();
$response->setContent($content.$scriptSnippet);
return $response;
}
}
Register the Middleware in app/Http/Kernel.php:
protected $routeMiddleware = [
'log.browserconsole' => 'App\Http\Middleware\LogBrowserConsole'
];
and invoke your Controller with the Middleware in app/Http/routes.php:
Route::get('test', ['middleware' => 'log.browserconsole', 'uses'=>'TestController#test']);
Alternatively, if you want to use the Middleware for every request you can add it to
protected $middleware = [
'App\Http\Middleware\LogBrowserConsole'
];
in app/Http/Kernel.php.
Your Route would look like Route::get('test', 'TestController#test');
Now, your Log::debug() etc. messages get sent to the logfile (the default LogHandler is still available, you just added another) and the script snipped from BrowserConsoleHandler gets built and sent to the browser with all your log items.
Keep in mind to eventually change the log level \Psr\LogLevel::INFO in app/Http/Middleware/LogBrowserConsole to fit your needs.

In Laravel 5, why is Request::root() different when called during phpunit test?

I defined a test which tests the creation of a user. The controller is set to redirect back to the same page on error (using validation through a generated App\Http\Requests\Request). This works correctly when manually clicking in a browser, but fails during a test. Instead of being redirected to:
http://localhost/account/create
The test redirects to (missing a slash):
http://localhostaccount/create
Neither of these urls are what I have setup in the .htaccess or in the $url variable in config/app.php. Which is (On OSX Yosemite):
http://~username/laravel_projects/projectname/public
I finally pinpointed the issue to have something to do with how the result of Request::root() is generated. Making a call to this outside of a test results in the expected value defined in .htaccess and $url. Inside the test it results in:
http://localhost
What configuration needs to change in order to get this function to return the correct value in both contexts?
I should also mention I made the painful upgrade from Laravel 4 to the current version 5.0.27.
****** UPDATE *******
I was able to figure out an acceptable solution/workaround to this issue!
In Laravel 5, FormRequests were introduced to help move validation logic out of controllers. Once a request is mapped to the controller, if a FormRequest (or just Request) is specified, this is executed before hitting the controller action.
This FormRequest by default handles the response if the validation fails. It attempts to construct a redirect based on the route you posted the form data to. In my case, possibly related to an error of mine updating from Laravel 4 to 5, this default redirect was being constructed incorrectly. The Laravel System code for handling the response looks like this:
/**
* Get the proper failed validation response for the request.
*
* #param array $errors
* #return \Symfony\Component\HttpFoundation\Response
*/
public function response(array $errors)
{
if ($this->ajax() || $this->wantsJson())
{
return new JsonResponse($errors, 422);
}
return $this->redirector->to($this->getRedirectUrl())
->withInput($this->except($this->dontFlash))
->withErrors($errors, $this->errorBag);
}
Notice how the returned redirect is NOT the same as calling Redirect::route('some_route'). You can override this response function by including use Response in your Request class.
After using Redirect::route() to create the redirect, the logic in my tests passed with the expected results. Here is my Request code that worked:
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Http\Requests\Request;
use Response;
class AccountRequest extends FormRequest {
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required|max:50|email|unique:users',
'password' => 'required|min:6',
'password_confirmation' => 'required|same:password'
];
}
public function response(array $errors){
return \Redirect::route('account_create');
}
}
The important part is that I called Redirect::route instead of letting the default response code execute.
Override the response function in the FormRequest validation handler to force the redirect to be constructed with Redirect::route('named_route') instead of allowing the default redirect.
You need to change config/app.php file's url value. Default value is http://localhost
Doc from config/app.php
This URL is used by the console to properly generate URLs when using the Artisan command line tool. You should set this to the root of your application so that it is used when running Artisan tasks.
I know this isn't an exact answer to your question since it is not a configuration update that solves the problem. But I was struggling with a related problem and this seems to be the only post on the internet of someone dealing with something similar - I thought I'd put in my two cents for anyone that wants a different fix.
Please note that I'm using Laravel 4.2 at the moment, so this might have changed in Laravel 5 (although I doubt it).
You can specify the HTTP_HOST header when you're testing a controller using the function:
$response = $this->call($method, $uri, $parameters, $files, $server, $content);
To specify the header just provided the $server variable as an array like so:
array('HTTP_HOST' => 'testing.mydomain.com');
When I did the above, the value produced for my Request::root() was http://testing.mydomain.com.
Again, I know this isn't a configuration update to solve you're issue, but hopefully this can help someone struggling with a semi-related issue.
If you tried changine config/app.php and it did not help.
it is better to use $_ENV - global variable in phpunit.
say, you want Request::root() to return 'my.site'
but you cannot touch phpunit.xml
you can simply set an env param like so
$_ENV['APP_URL'] = 'my.site';
and call $this->refreshApplication(); in your unittest.
viola, your request()->root() is giving you my.site now.

How to integrate CKFinder with Laravel?

I'm trying to integrate CKFinder with Laravel, and I'm about 95% there. I can get everything to work, except for the CheckAuthentication function - I have to make it return true regardless for the upload to work.
What I've tried doing is bootstrapping Laravel in the config.php file and then checking if a user is logged in, like below:
public/packages/ckfinder/config.php
<?php
/*
* ### CKFinder : Configuration File - Basic Instructions
*
* In a generic usage case, the following tasks must be done to configure
* CKFinder:
* 1. Check the $baseUrl and $baseDir variables;
* 2. If available, paste your license key in the "LicenseKey" setting;
* 3. Create the CheckAuthentication() function that enables CKFinder for authenticated users;
*
* Other settings may be left with their default values, or used to control
* advanced features of CKFinder.
*/
/** RIPPED FROM public/index.php **/
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/
require __DIR__.'/../../../bootstrap/autoload.php';
/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let's turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight these users.
|
*/
$app = require __DIR__.'/../../../bootstrap/start.php';
/** END public/index.php **/
/**
* This function must check the user session to be sure that he/she is
* authorized to upload and access files in the File Browser.
*
* #return boolean
*/
function CheckAuthentication()
{
// WARNING : DO NOT simply return "true". By doing so, you are allowing
// "anyone" to upload and list the files in your server. You must implement
// some kind of session validation here. Even something very simple as...
return Auth::check();
}
This always returns false, though. I've also tried directly using Laravel's Session to set a variable to true when someone logs in, and false when they log out, and then checking that in the config.php file, but it always returns the default value in Session::get("IsAuthorized", false);. Can anyone offer some guidance as to -
1) How to authenticate whether the user should be allowed to upload?
2) Why bootstrapping Laravel in another file seems to cause it to use a separate session, even when it's loading the same files?
I tried integrating simogeo's Filemanager and KCFinder into a Laravel project and I found the same problem.
With this code, it's possible to share Laravel's session and check authentication from external projects:
https://gist.github.com/frzsombor/ddd0e11f93885060ef35
From my experience, starting from 4.1.28, Application::boot() does not initialize sensitive session data anymore.
So if you're integrating 3rd party library, which needs external authentification check through sessions, simple checking Auth::check() will not work. However, we can still use old $_SESSION variable.
E.g. this one will not work:
require __DIR__.'/../bootstrap/autoload.php';
$app = require_once __DIR__.'/../bootstrap/start.php';
$app->boot();
return Auth::check();
Session variables for Auth::check() to work are initialized only in $app->run() sequence. But in that case, routing will take place and probably you will get unrecognized page... unless you're using dedicated Laravel package for this.
This one - below - still works:
// Somewhere in your app - e.g. in filters.php, "auth"/"guest" filters declaration
if (session_id() == '') {
#session_start();
/* or Session:start(); */
}
$_SESSION['isLoggedIn'] = Auth::check() ? true : false;
Then in your case, function would be simple as:
function CheckAuthentication()
{
if (session_id() == '') {
#session_start();
}
return isset( $_SESSION['isLoggedIn'] ) && $_SESSION['isLoggedIn'] == true;
}
N.B. If you can use Ajax calls for authorization checks, you can still make a custom API with JSON request to user-logged (as an example) to see if user is authentificated.
To answer your second question - it's not so simple as it sounds. Laravel, as a default, uses file system for session storage. While session data is still accessible, it is encrypted - unless you will write your own Session manager, you can't extract anything from there easily.

Categories