Laravel middleware cancel request and keep page in same state - php

Here's my current issue.
At the moment, I have a page with elements that can be added in and appended via AJAX. These elements contain forms, image uploads etc.
I have a middleware on my entire application that checks the size of any image being uploaded at any given time and makes sure its under 5MB (image validation for each image upload on the application is not an option, it has to be 1 controller that maintains all image upload validation).
If the request detects an image thats over 5MB, it will run this code
return redirect()->back()->withInput($request->all())->withErrors(array('Image' => 'Sorry, ' . $file->getClientOriginalName() . ' is too large, maximum file size is 5MB. Please reduce the size of your image!'));
This code is very temperamental, and heres why.
I need the page to be in the EXACT same state i left it in, when its returned. That means all AJAX loaded elements, all images, everything needs to be in the same state, so redirect()->back()->withInput($request->all()) doesn't work, because it still refreshes the page and removes everything loaded appended and added in that instance.
I need to be able to cancel the request if it fails.
In plain english, When this middleware is ran, detect all images. If there is an image that is over 5MB, do not refresh the page or anything. Just error
I know this seems silly, because the request cannot pass something back without refreshing, but I thought id ask / open to suggestions.
Here's my middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\UploadedFile;
use Symfony\Component\HttpFoundation\Response;
class ImageInterceptor
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
foreach (array_filter(array_flatten($request->files->all())) as $file) {
//Check if the file being uploaded is not a csv
if($file->getClientOriginalExtension() != 'csv'){
$filename = $file->getClientOriginalName();
$size = $file->getClientSize(); // size in bytes!
$onemb = pow(1024, 2);
if ($size > $onemb * 5) {
//Return back, image is too big!
return redirect()->back()->withInput($request->all())->withErrors(array('Image' => 'Sorry, ' . $file->getClientOriginalName() . ' is too large, maximum file size is 5MB. Please reduce the size of your image!'));
}
}
}
return $next($request);
}
}

If you plan on having the page on the same state, then you can't tell it to redirect backwards with errors, you would have to return an array, or string, or whatever is your needs. By saying redirect backwards, it's telling the browser where to navigate.
About maintaining the inputs, you can try something among these lines:
<input type="text" name="firstname" id="firstname" class="form-control" value="{{ $user->firstname or old('firstname') }}">
Why don't you create a form request? I really doubt you need that validation for every page you require. Middleware, in my opinion, should handle authentication and authorization .
A formrequest would be something like:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class Example 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 [
'photo' => 'required|mimes:jpeg,bmp,png|size:5000'
];
}
}
And on your controller, you just place a argument on the function (instead of Request $request, you place Example $request). This way, you can access every request information that Illuminate has plus your own validation.
https://laravel.com/docs/5.2/validation#rule-mimetypes

Related

Resize and replace image in Laravel request

I need to be able to resize an image and put the resized version back into the $request, does anyone know if thats possible?
Basically I have inherited some code that contains potentially 100+ separate file upload sections, and it is now my task to resize all images on the site if they are above a certain size.
So I now need to intercept ALL image uploads on the application, detect if they are above a set size and if they are, resize them.
All code i've found online only shows how to resize the image then save the resized version straight away, but I need to be able to resize the image then put it BACK into the $request to be processed by the controller.
The images come in the form of arrays of images from separate sections, so i need to be able to loop the entire request, check if any of the inputs contain/are files, then if they are check the sizes of them. If they're above a set size, then resize them and replace them in the $request so that when the request continues, the controller can process the image as normal but it will be processing the new resized version.
I have tried resizing images and then using laravels $request->merge() method but I cannot get it to work.
At the moment I am resizing all images in a middleware, like this
public function handle($request, Closure $next)
{
foreach($request->files as $fileKey => $file){
//Create a new array to add the newly sized images in
$newFileArray = [];
//Get each of the files that are being uploaded in the request, if there are no files this will just be ignored.
foreach ($file as $key => $f) {
if(!is_null($f)){
$image = Image::make($f);
if($image->height() > 500 || $image->width() > 500){
$image->resize(500, null, function ($constraint) {
$constraint->aspectRatio();
});
}
$newFileArray[$key] = $image;
} else {
$newFileArray[$key] = null;
}
}
$request->merge([
$fileKey => $newFileArray
]);
};
return $next($request);
}
I just can't get it to work!
Is this possible?
EDIT
After a great suggestion in the comments of one of the answers below, I've achieved this by editing the temp image file directly so I don't have to mess with the request, this is how i've done it.
public function handle($request, Closure $next)
{
foreach($request->files as $fileKey => $file){
//Get each of the files that are being uploaded in the request, if there are no files this will just be ignored.
foreach ($file as $key => $f) {
if(!is_null($f)){
$image = Image::make($f->getPathName());
if($image->height() > 500 || $image->width() > 500){
$image->resize(500, null, function ($constraint) {
$constraint->aspectRatio();
});
$image->save($f->getPathName());
}
}
}
};
return $next($request);
}
I just read that Laravel uses PSR-7 requests.
https://laravel.com/docs/5.7/requests#psr7-requests
These are immutable. In other words, you can't change the data once set. What you can do however, is get it to create a new request with your new parameters.
Looking at the PSR-7 interface, we see there is a method which looks like exactly what you need:
https://github.com/php-fig/http-message/blob/master/src/ServerRequestInterface.php#L150
/**
* Create a new instance with the specified uploaded files.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated body parameters.
*
* #param array $uploadedFiles An array tree of UploadedFileInterface instances.
* #return static
* #throws \InvalidArgumentException if an invalid structure is provided.
*/
public function withUploadedFiles(array $uploadedFiles);
So, do your thing, create your array, and once it's ready, replace your request like this:
$request = $request->withUploadedFiles($yourNewArray);

Return a default image, if no image is found Laravel Intervention

For a travel application, the mobile application needs to get a default image for each city from their city code.
For example: example.com/imageCache/thumbnail/JFK.png
Where thumbnail is custom filter defined as:
/**
* Sample filter for image manipulation
* via image cache
*/
namespace App\ImageFilters;
use Intervention\Image\Filters\FilterInterface;
use Intervention\Image\Image;
use Intervention\Image\ImageManagerStatic;
class Thumbnail implements FilterInterface
{
/**
* Applies filter to given image
*
* #param Image $image
* #return Image
*/
public function applyFilter(Image $image)
{
//TODO: Do something to check if the image doesn't exist.
$gradient = ImageManagerStatic::make(public_path('images/gradient.png'));
return $image->fit(200, 200)->insert($gradient,'center')->blur();
}
}
The application however throws 404 even before this function is called.
I would like to show a default image, if the image is not found.
Thanks in Advance.
The URL manipulation as it is may not work in this case.
Write a route getCityImage/{cityCode} as:
public function getCityImage($cityCode){
if(file_exists('path_to_city_images/'.$cityCode.'.png'){
$image = Intervention\Image\Image::make('path_to_city_images/'.$cityCode.'.png');
return $image->filter(new Thumbnail());
}
else {
return $your_default_image;
}
}

Symfony run code after response was sent

I took a look at this other question. I am looking for a way to do what the OP of that question wants as well, and that is to continue processing php after sending http response, but in Symfony2.
I implemented an event that fires after every kernel termination. So far so good, but what I want is for it to fire after CERTAIN terminations, in specific controller actions, for instance after a form was sent, not every single time at every request. That is because I want to do some heavy tasks at certain times and don't want the end user to wait for the page to load.
Any idea how can I do that?
<?php
namespace MedAppBundle\Event;
use JMS\DiExtraBundle\Annotation\InjectParams;
use JMS\DiExtraBundle\Annotation\Service;
use JMS\DiExtraBundle\Annotation\Tag;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use JMS\DiExtraBundle\Annotation\Inject;
/**
* Class MedicListener
* #package MedAppBundle\EventListener
* #Service("medapp_test.listener")
* #Tag(name="kernel.event_subscriber")
*/
class TestListener implements EventSubscriberInterface
{
private $container;
private $logger;
/**
* Constructor.
*
* #param ContainerInterface $container A ContainerInterface instance
* #param LoggerInterface $logger A LoggerInterface instance
* #InjectParams({
* "container" = #Inject("service_container"),
* "logger" = #Inject("logger")
* })
*/
public function __construct(ContainerInterface $container, LoggerInterface $logger = null)
{
$this->container = $container;
$this->logger = $logger;
}
public function onTerminate()
{
$this->logger->notice('fired');
}
public static function getSubscribedEvents()
{
$listeners = array(KernelEvents::TERMINATE => 'onTerminate');
if (class_exists('Symfony\Component\Console\ConsoleEvents')) {
$listeners[ConsoleEvents::TERMINATE] = 'onTerminate';
}
return $listeners;
}
}
So far I've subscribed the event to the kernel.terminate one, but obviously this fires it at each request. I made it similar to Swiftmailer's EmailSenderListener
It feels kind of strange that the kernel must listen each time for this event even when it's not triggered. I'd rather have it fired only when needed, but not sure how to do that.
In the onTerminate callback you get an instance of PostResponseEvent as first parameter. You can get the Request as well as the Response from that object.
Then you should be able to decide if you want to run the actual termination code.
Also you can store custom data in the attributes bag of the Request. See this link: Symfony and HTTP Fundamentals
The Request class also has a public attributes property, which holds special data related to how the application works internally. For the Symfony Framework, the attributes holds the values returned by the matched route, like _controller, id (if you have an {id} wildcard), and even the name of the matched route (_route). The attributes property exists entirely to be a place where you can prepare and store context-specific information about the request.
Your code could look something like this:
// ...
class TestListener implements EventSubscriberInterface
{
// ...
public function onTerminate(PostResponseEvent $event)
{
$request = $event->getRequest();
if ($request->attributes->get('_route') == 'some_route_name') {
// do stuff
}
}
// ...
}
Edit:
The kernel.terminate event is designed to run after the response is sent. But the symfony documentation is saying the following (taken from here):
Internally, the HttpKernel makes use of the fastcgi_finish_request PHP function. This means that at the moment, only the PHP FPM server API is able to send a response to the client while the server's PHP process still performs some tasks. With all other server APIs, listeners to kernel.terminate are still executed, but the response is not sent to the client until they are all completed.
Edit 2:
To use the solution from here, you could either directly edit the web/app.php file to add it there (but this is some kind of "hacking core" imo, even though it would be easier to use than the following). Or you could do it like this:
Add a listener to kernel.request event with a high priority and start output buffering (ob_start).
Add a listener to kernel.response and add the header values to the response.
Add another listener with highest priority to kernel.terminate and do the flushing (ob_flush, flush).
Run your code in a separate listener with lower priority to kernel.terminate
I did not try it, but it should actually work.
To solve this issue for some of my use cases I simply create symfony commands to do the heavy tasks, and call them via exec() to make them run in a separate process.
I used these answers to write a Response class that has this functionality:
https://stackoverflow.com/a/28738208/1153227
This implementation will work on Apache and not just PHP FPM. However, to make this work we must prevent Apache from using gzip (by using an invalid Content-Encoding) so it makes sense to have a custom Response class to specify exactly when having an early response is more important than compression.
use Symfony\Component\HttpFoundation\Response;
class EarlyResponse extends Response
{
// Functionality adapted from this answer: https://stackoverflow.com/a/7120170/1153227
protected $callback = null;
/**
* Constructor.
*
* #param mixed $content The response content, see setContent()
* #param int $status The response status code
* #param array $headers An array of response headers
*
* #throws \InvalidArgumentException When the HTTP status code is not valid
*/
public function __construct($content = '', $status = 200, $headers = array(), $callback = null)
{
if (null !== $callback) {
$this->setTerminateCallback($callback);
}
parent::__construct($content, $status, $headers);
}
/**
* Sets the PHP callback associated with this Response.
* It will be called after the terminate events fire and thus after we've sent our response and closed the connection
*
* #param callable $callback A valid PHP callback
*
* #throws \LogicException
*/
public function setTerminateCallback($callback)
{
//Copied From Symfony\Component\HttpFoundation\StreamedResponse
if (!is_callable($callback)) {
throw new \LogicException('The Response callback must be a valid PHP callable.');
}
$this->callback = $callback;
}
/**
* #return Current_Class_Name
*/
public function send() {
if (function_exists('fastcgi_finish_request') || 'cli' === PHP_SAPI) { // we don't need the hack when using fast CGI
return parent::send();
}
ignore_user_abort(true);//prevent apache killing the process
if (!ob_get_level()) { // Check if an ob buffer exists already.
ob_start();//start the output buffer
}
$this->sendContent(); //Send the content to the buffer
static::closeOutputBuffers(1, true); //flush all but the last ob buffer level
$this->headers->set('Content-Length', ob_get_length()); // Set the content length using the last ob buffer level
$this->headers->set('Connection', 'close'); // Close the Connection
$this->headers->set('Content-Encoding', 'none');// This invalid header value will make Apache not delay sending the response while it is
// See: https://serverfault.com/questions/844526/apache-2-4-7-ignores-response-header-content-encoding-identity-instead-respect
$this->sendHeaders(); //Now that we have the headers, we can send them (which will avoid the ob buffers)
static::closeOutputBuffers(0, true); //flush the last ob buffer level
flush(); // After we flush the OB buffer to the normal buffer, we still need to send the normal buffer to output
session_write_close();//close session file on server side to avoid blocking other requests
return $this;
}
/**
* #return Current_Class_Name
*/
public function callTerminateCallback() {
if ($this->callback) {
call_user_func($this->callback);
}
return $this;
}
}
You also need to add a method to your AppKernel.php to make this work (don't forget to add a use statement for your EarlyResponse class)
public function terminate(Request $request, Response $response)
{
ob_start();
//Run this stuff before the terminate events
if ($response instanceof EarlyResponse) {
$response->callTerminateCallback();
}
//Trigger the terminate events
parent::terminate($request, $response);
//Optionally, we can output the beffer that will get cleaned to a file before discarding its contents
//file_put_contents('/tmp/process.log', ob_get_contents());
ob_end_clean();
}

Symfony embedded controller form

I've got a search form with some select boxes. I render it in the headnavi on every page using ebedded controllers. (http://symfony.com/doc/current/book/templating.html#embedding-controllers)
I want to use the form output to redirect to my list-view page like this:
/list-view/{city}/{category}?q=searchQuery
My form and the request is working well when I call the controller through a route, but unfortunately when I embed the controller, I'm stumblig over two problems. Like I've read here (Symfony 2 - Layout embed "no entity/class form" validation isn't working) my request isn't handeled by my form because of the sub-request. There is a solution in the answer, but its not very detailed.
The other problem, after fixing the first one, will be that I can't do a redirect from an embedded controller (Redirect from embedded controller).
Maybe anyone has an easier solution for having a form on every page that lets me do a redirect to its data?
Many thanks and greetings
Raphael
The answer of Symfony 2 - Layout embed "no entity/class form" validation isn't working is 100% correct, but we use contexts and isolate them, so an action which always uses the master request would break the rules. You have all requests (one master and zero or more subrequests) in the request_stack. Injecting Request $request into your controller action is the current request which is the subrequest with only max=3 (injecting the Request is deprecated now). Thus you have to use the 'correct' request.
Performing a redirection can be done in many ways, like return some JS script code to redirect (which is quite ugly imho). I would not use subrequests from twig because it's too late to start a redirection then, but make the subrequest in the action. I didn't test the code, but it should work. Controller::forward is your friend, since it duplicatest the current request for performing a subrequest.
Controller.php (just to see the implementation).
/**
* Forwards the request to another controller.
*
* #param string $controller The controller name (a string like BlogBundle:Post:index)
* #param array $path An array of path parameters
* #param array $query An array of query parameters
*
* #return Response A Response instance
*/
protected function forward($controller, array $path = array(), array $query = array())
{
$path['_controller'] = $controller;
$subRequest = $this->container->get('request_stack')->getCurrentRequest()->duplicate($query, null, $path);
return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}
YourController.php
public function pageAction() {
$formResponse = $this->forward('...:...:form'); // e.g. formAction()
if($formResponse->isRedirection()) {
return $formResponse; // just the redirection, no content
}
$this->render('...:...:your.html.twig', [
'form_response' => $formResponse
]);
}
public function formAction() {
$requestStack = $this->get('request_stack');
/* #var $requestStack RequestStack */
$masterRequest = $requestStack->getCurrentRequest();
\assert(!\is_null($masterRequest));
$form = ...;
$form->handleRequest($masterRequest);
if($form->isValid()) {
return $this->redirect(...); // success
}
return $this->render('...:...:form.html.twig', [
'form' => $form->createView()
]);
}
your.html.twig
{{ form_response.content | raw }}

Kohana 3.2. - How can I use hyphens in URIs

Recently I've been doing some research into SEO and how URIs that use hyphens or underscores are treated differently, particularly by Google who view hyphens as separators.
Anyway, eager to adapt my current project to meet this criteria I found that because Kohana uses function names to define pages I was receiving the unexpected '-' warning.
I was wondering whether there was any way to enable the use of URIs in Kohana like:
http://www.mysite.com/controller/function-name
Obviously I could setup a routeHandler for this... but if I was to have user generated content, i.e. news. I'd then have to get all articles from the database, produce the URI, and then do the routing for each one.
Are there any alternative solutions?
Note: This is the same approach as in Laurent's answer, just slightly more OOP-wise. Kohana allows one to very easily overload any system class, so we can use it to save us some typing and also to allow for cleaner updates in the future.
We can plug-in into the request flow in Kohana and fix the dashes in the action part of the URL. To do it we will override Request_Client_Internal system class and it's execute_request() method. There we'll check if request->action has dashes, and if so we'll switch them to underscores to allow php to call our method properly.
Step 1. Open your application/bootstrap.php and add this line:
define('URL_WITH_DASHES_ONLY', TRUE);
You use this constant to quickly disable this feature on some requests, if you need underscores in the url.
Step 2. Create a new php file in: application/classes/request/client/internal.php and paste this code:
<?php defined('SYSPATH') or die('No direct script access.');
class Request_Client_Internal extends Kohana_Request_Client_Internal {
/**
* We override this method to allow for dashes in the action part of the url
* (See Kohana_Request_Client_Internal::execute_request() for the details)
*
* #param Request $request
* #return Response
*/
public function execute_request(Request $request)
{
// Check the setting for dashes (the one set in bootstrap.php)
if (defined('URL_WITH_DASHES_ONLY') and URL_WITH_DASHES_ONLY == TRUE)
{
// Block URLs with underscore in the action to avoid duplicated content
if (strpos($request->action(), '_') !== false)
{
throw new HTTP_Exception_404('The requested URL :uri was not found on this server.', array(':uri' => $request->uri()));
}
// Modify action part of the request: transform all dashes to underscores
$request->action( strtr($request->action(), '-', '_') );
}
// We are done, let the parent method do the heavy lifting
return parent::execute_request($request);
}
} // end_class Request_Client_Internal
What this does is simply replacing all the dashes in the $request->action with underscores, thus if url was /something/foo-bar, Kohana will now happily route it to our action_foo_bar() method.
In the same time we block all the actions with underscores, to avoid the duplicated content problems.
No way to directly map a hyphenated string to a PHP function so you will have to do routing.
As far as user generated content, you could do something like Stack Exchange does. Each time user content is saved to the database, generated a slug for it (kohana-3-2-how-can-i-use-hyphens-in-uris) and save it along with the other information. Then when you need to link to it, use the unique id and append the slug to the end (ex:http://stackoverflow.com/questions/7404646/kohana-3-2-how-can-i-use-hyphens-in-uris) for readability.
You can do this with lambda functions: http://forum.kohanaframework.org/discussion/comment/62581#Comment_62581
You could do something like
Route::set('route', '<controller>/<identifier>', array(
'identifier' => '[a-zA-Z\-]*'
))
->defaults(array(
'controller' => 'Controller',
'action' => 'show',
));
Then receive your content identifier in the function with Request::current()->param('identifier') and parse it manually to find the relating data.
After having tried various solutions, I found that the easiest and most reliable way is to override Kohana_Request_Client_Internal::execute_request. To do so, add a file in your application folder in "application\classes\kohana\request\client\internal.php" then set its content to:
<?php defined('SYSPATH') or die('No direct script access.');
class Kohana_Request_Client_Internal extends Request_Client {
/**
* #var array
*/
protected $_previous_environment;
/**
* Processes the request, executing the controller action that handles this
* request, determined by the [Route].
*
* 1. Before the controller action is called, the [Controller::before] method
* will be called.
* 2. Next the controller action will be called.
* 3. After the controller action is called, the [Controller::after] method
* will be called.
*
* By default, the output from the controller is captured and returned, and
* no headers are sent.
*
* $request->execute();
*
* #param Request $request
* #return Response
* #throws Kohana_Exception
* #uses [Kohana::$profiling]
* #uses [Profiler]
* #deprecated passing $params to controller methods deprecated since version 3.1
* will be removed in 3.2
*/
public function execute_request(Request $request)
{
// Create the class prefix
$prefix = 'controller_';
// Directory
$directory = $request->directory();
// Controller
$controller = $request->controller();
if ($directory)
{
// Add the directory name to the class prefix
$prefix .= str_replace(array('\\', '/'), '_', trim($directory, '/')).'_';
}
if (Kohana::$profiling)
{
// Set the benchmark name
$benchmark = '"'.$request->uri().'"';
if ($request !== Request::$initial AND Request::$current)
{
// Add the parent request uri
$benchmark .= ' « "'.Request::$current->uri().'"';
}
// Start benchmarking
$benchmark = Profiler::start('Requests', $benchmark);
}
// Store the currently active request
$previous = Request::$current;
// Change the current request to this request
Request::$current = $request;
// Is this the initial request
$initial_request = ($request === Request::$initial);
try
{
if ( ! class_exists($prefix.$controller))
{
throw new HTTP_Exception_404('The requested URL :uri was not found on this server.',
array(':uri' => $request->uri()));
}
// Load the controller using reflection
$class = new ReflectionClass($prefix.$controller);
if ($class->isAbstract())
{
throw new Kohana_Exception('Cannot create instances of abstract :controller',
array(':controller' => $prefix.$controller));
}
// Create a new instance of the controller
$controller = $class->newInstance($request, $request->response() ? $request->response() : $request->create_response());
$class->getMethod('before')->invoke($controller);
// Determine the action to use
/* ADDED */ if (strpos($request->action(), '_') !== false) throw new HTTP_Exception_404('The requested URL :uri was not found on this server.', array(':uri' => $request->uri()));
/* MODIFIED */ $action = str_replace('-', '_', $request->action()); /* ORIGINAL: $action = $request->action(); */
$params = $request->param();
// If the action doesn't exist, it's a 404
if ( ! $class->hasMethod('action_'.$action))
{
throw new HTTP_Exception_404('The requested URL :uri was not found on this server.',
array(':uri' => $request->uri()));
}
$method = $class->getMethod('action_'.$action);
$method->invoke($controller);
// Execute the "after action" method
$class->getMethod('after')->invoke($controller);
}
catch (Exception $e)
{
// Restore the previous request
if ($previous instanceof Request)
{
Request::$current = $previous;
}
if (isset($benchmark))
{
// Delete the benchmark, it is invalid
Profiler::delete($benchmark);
}
// Re-throw the exception
throw $e;
}
// Restore the previous request
Request::$current = $previous;
if (isset($benchmark))
{
// Stop the benchmark
Profiler::stop($benchmark);
}
// Return the response
return $request->response();
}
} // End Kohana_Request_Client_Internal
Then to add an action with hyphens, for example, "controller/my-action", create an action called "my_action()".
This method will also throw an error if the user tries to access "controller/my_action" (to avoid duplicate content).
I know some developers don't like this method but the advantage of it is that it doesn't rename the action, so if you check the current action it will be consistently called "my-action" everywhere. With the Route or lambda function method, the action will sometime be called "my_action", sometime "my-action" (since both methods rename the action).

Categories