4 * Zend Framework (http://framework.zend.com/)
6 * @link http://github.com/zendframework/zf2 for the canonical source repository
7 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
8 * @license http://framework.zend.com/license/new-bsd New BSD License
11 namespace Zend\Mvc\Controller\Plugin
;
13 use Zend\Filter\FilterChain
;
14 use Zend\Form\FormInterface
;
15 use Zend\Http\Response
;
16 use Zend\InputFilter\FileInput
;
17 use Zend\InputFilter\InputFilterInterface
;
18 use Zend\Mvc\Exception\RuntimeException
;
19 use Zend\Session\Container
;
20 use Zend\Stdlib\ArrayUtils
;
21 use Zend\Validator\ValidatorChain
;
24 * Plugin to help facilitate Post/Redirect/Get for file upload forms
25 * (http://en.wikipedia.org/wiki/Post/Redirect/Get)
27 * Requires that the Form's File inputs contain a 'fileRenameUpload' filter
28 * with the target option set: 'target' => /valid/target/path'.
29 * This is so the files are moved to a new location between requests.
30 * If this filter is not added, the temporary upload files will disappear
33 class FilePostRedirectGet
extends AbstractPlugin
38 protected $sessionContainer;
41 * @param FormInterface $form
42 * @param string $redirect Route or URL string (default: current route)
43 * @param bool $redirectToUrl Use $redirect as a URL string (default: false)
44 * @return bool|array|Response
46 public function __invoke(FormInterface
$form, $redirect = null, $redirectToUrl = false)
48 $request = $this->getController()->getRequest();
49 if ($request->isPost()) {
50 return $this->handlePostRequest($form, $redirect, $redirectToUrl);
52 return $this->handleGetRequest($form);
57 * @param FormInterface $form
58 * @param string $redirect Route or URL string (default: current route)
59 * @param bool $redirectToUrl Use $redirect as a URL string (default: false)
62 protected function handlePostRequest(FormInterface
$form, $redirect, $redirectToUrl)
64 $container = $this->getSessionContainer();
65 $request = $this->getController()->getRequest();
66 $postFiles = $request->getFiles()->toArray();
67 $postOther = $request->getPost()->toArray();
68 $post = ArrayUtils
::merge($postOther, $postFiles, true);
70 // Fill form with the data first, collections may alter the form/filter structure
71 $form->setData($post);
73 // Change required flag to false for any previously uploaded files
74 $inputFilter = $form->getInputFilter();
75 $previousFiles = ($container->files
) ?
: [];
76 $this->traverseInputs(
79 function ($input, $value) {
80 if ($input instanceof FileInput
) {
81 $input->setRequired(false);
87 // Run the form validations/filters and retrieve any errors
88 $isValid = $form->isValid();
89 $data = $form->getData(FormInterface
::VALUES_AS_ARRAY
);
90 $errors = (!$isValid) ?
$form->getMessages() : null;
92 // Merge and replace previous files with new valid files
93 $prevFileData = $this->getEmptyUploadData($inputFilter, $previousFiles);
94 $newFileData = $this->getNonEmptyUploadData($inputFilter, $data);
95 $postFiles = ArrayUtils
::merge(
100 $post = ArrayUtils
::merge($postOther, $postFiles, true);
102 // Save form data in session
103 $container->setExpirationHops(1, ['post', 'errors', 'isValid']);
104 $container->post
= $post;
105 $container->errors
= $errors;
106 $container->isValid
= $isValid;
107 $container->files
= $postFiles;
109 return $this->redirect($redirect, $redirectToUrl);
113 * @param FormInterface $form
116 protected function handleGetRequest(FormInterface
$form)
118 $container = $this->getSessionContainer();
119 if (null === $container->post
) {
120 // No previous post, bail early
121 unset($container->files
);
125 // Collect data from session
126 $post = $container->post
;
127 $errors = $container->errors
;
128 $isValid = $container->isValid
;
129 unset($container->post
);
130 unset($container->errors
);
131 unset($container->isValid
);
133 // Fill form with the data first, collections may alter the form/filter structure
134 $form->setData($post);
136 // Remove File Input validators and filters on previously uploaded files
137 // in case $form->isValid() or $form->bindValues() is run
138 $inputFilter = $form->getInputFilter();
139 $this->traverseInputs(
142 function ($input, $value) {
143 if ($input instanceof FileInput
) {
144 $input->setAutoPrependUploadValidator(false)
145 ->setValidatorChain(new ValidatorChain())
146 ->setFilterChain(new FilterChain
);
152 // set previous state
153 $form->isValid(); // re-validate to bind values
154 if (null !== $errors) {
155 $form->setMessages($errors); // overwrite messages
157 $this->setProtectedFormProperty($form, 'isValid', $isValid); // force previous state
159 // Clear previous files from session data if form was valid
161 unset($container->files
);
170 public function getSessionContainer()
172 if (!$this->sessionContainer
) {
173 $this->sessionContainer
= new Container('file_prg_post1');
175 return $this->sessionContainer
;
179 * @param Container $container
180 * @return FilePostRedirectGet
182 public function setSessionContainer(Container
$container)
184 $this->sessionContainer
= $container;
189 * @param FormInterface $form
190 * @param string $property
191 * @param mixed $value
192 * @return FilePostRedirectGet
194 protected function setProtectedFormProperty(FormInterface
$form, $property, $value)
196 $formClass = new \
ReflectionClass($form);
197 $property = $formClass->getProperty($property);
198 $property->setAccessible(true);
199 $property->setValue($form, $value);
204 * Traverse the InputFilter and run a callback against each Input and associated value
206 * @param InputFilterInterface $inputFilter
207 * @param array $values
208 * @param callable $callback
211 protected function traverseInputs(InputFilterInterface
$inputFilter, $values, $callback)
213 $returnValues = null;
214 foreach ($values as $name => $value) {
215 if (!$inputFilter->has($name)) {
219 $input = $inputFilter->get($name);
220 if ($input instanceof InputFilterInterface
&& is_array($value)) {
221 $retVal = $this->traverseInputs($input, $value, $callback);
222 if (null !== $retVal) {
223 $returnValues[$name] = $retVal;
228 $retVal = $callback($input, $value);
229 if (null !== $retVal) {
230 $returnValues[$name] = $retVal;
233 return $returnValues;
237 * Traverse the InputFilter and only return the data of FileInputs that have an upload
239 * @param InputFilterInterface $inputFilter
243 protected function getNonEmptyUploadData(InputFilterInterface
$inputFilter, $data)
245 return $this->traverseInputs(
248 function ($input, $value) {
249 $messages = $input->getMessages();
250 if (is_array($value) && $input instanceof FileInput
&& empty($messages)) {
251 $rawValue = $input->getRawValue();
253 (isset($rawValue['error']) && $rawValue['error'] !== UPLOAD_ERR_NO_FILE
)
254 ||
(isset($rawValue[0]['error']) && $rawValue[0]['error'] !== UPLOAD_ERR_NO_FILE
)
265 * Traverse the InputFilter and only return the data of FileInputs that are empty
267 * @param InputFilterInterface $inputFilter
271 protected function getEmptyUploadData(InputFilterInterface
$inputFilter, $data)
273 return $this->traverseInputs(
276 function ($input, $value) {
277 $messages = $input->getMessages();
278 if (is_array($value) && $input instanceof FileInput
&& empty($messages)) {
279 $rawValue = $input->getRawValue();
280 if ((isset($rawValue['error']) && $rawValue['error'] === UPLOAD_ERR_NO_FILE
)
281 ||
(isset($rawValue[0]['error']) && $rawValue[0]['error'] === UPLOAD_ERR_NO_FILE
)
292 * TODO: Good candidate for traits method in PHP 5.4 with PostRedirectGet plugin
294 * @param string $redirect
295 * @param bool $redirectToUrl
297 * @throws \Zend\Mvc\Exception\RuntimeException
299 protected function redirect($redirect, $redirectToUrl)
301 $controller = $this->getController();
304 $reuseMatchedParams = false;
306 if (null === $redirect) {
307 $routeMatch = $controller->getEvent()->getRouteMatch();
309 $redirect = $routeMatch->getMatchedRouteName();
310 //null indicates to redirect for self.
311 $reuseMatchedParams = true;
314 if (method_exists($controller, 'getPluginManager')) {
315 // get the redirect plugin from the plugin manager
316 $redirector = $controller->getPluginManager()->get('Redirect');
319 * If the user wants to redirect to a route, the redirector has to come
320 * from the plugin manager -- otherwise no router will be injected
322 if ($redirectToUrl === false) {
323 throw new RuntimeException('Could not redirect to a route without a router');
326 $redirector = new Redirect();
329 if ($redirectToUrl === false) {
330 $response = $redirector->toRoute($redirect, $params, $options, $reuseMatchedParams);
331 $response->setStatusCode(303);
335 $response = $redirector->toUrl($redirect);
336 $response->setStatusCode(303);