Merge branch 'feature/support-for-file-uploads-8758'

resolves #8758
This commit is contained in:
Johannes Meyer 2015-07-22 13:32:17 +02:00
commit 1e9ce1d0d5
10 changed files with 178 additions and 40 deletions

View File

@ -54,6 +54,7 @@ $iframeClass = $isIframe ? ' iframe' : '';
<div id="layout" class="default-layout<?php if ($showFullscreen): ?> fullscreen-layout<?php endif ?>">
<?= $this->render('body.phtml') ?>
</div>
<iframe id="fileupload-frame-target" name="fileupload-frame-target"></iframe>
<!--[if IE 8]>
<script type="text/javascript" src="<?= $this->href($ie8jsfile) ?>"></script>
<![endif]-->

View File

@ -0,0 +1,10 @@
<html>
<head>
<?php if (isset($this->layout()->redirectUrl)): ?>
<meta name="redirectUrl" content="<?= $this->layout()->redirectUrl; ?>">
<?php endif ?>
</head>
<body>
<?= $this->render('inline.phtml'); ?>
</body>
</html>

View File

@ -96,6 +96,9 @@ class ActionController extends Zend_Controller_Action
if ($this->rerenderLayout = $request->getUrl()->shift('renderLayout')) {
$this->xhrLayout = 'body';
}
if ($request->getUrl()->shift('_disableLayout')) {
$this->_helper->layout()->disableLayout();
}
if ($this->requiresLogin()) {
$this->redirectToLogin(Url::fromRequest());

View File

@ -3,13 +3,13 @@
namespace Icinga\Web;
use LogicException;
use Zend_Config;
use Zend_Form;
use Zend_Form_Element;
use Zend_View_Interface;
use Icinga\Application\Icinga;
use Icinga\Authentication\Manager;
use Icinga\Exception\ProgrammingError;
use Icinga\Security\SecurityException;
use Icinga\Util\Translator;
use Icinga\Web\Form\ErrorLabeller;
@ -84,7 +84,7 @@ class Form extends Zend_Form
/**
* The url to redirect to upon success
*
* @var string|Url
* @var Url
*/
protected $redirectUrl;
@ -229,12 +229,12 @@ class Form extends Zend_Form
*
* @return $this
*
* @throws LogicException If the callback is not callable
* @throws ProgrammingError If the callback is not callable
*/
public function setOnSuccess($onSuccess)
{
if (! is_callable($onSuccess)) {
throw new LogicException('The option `onSuccess\' is not callable');
throw new ProgrammingError('The option `onSuccess\' is not callable');
}
$this->onSuccess = $onSuccess;
return $this;
@ -269,9 +269,17 @@ class Form extends Zend_Form
* @param string|Url $url The url to redirect to
*
* @return $this
*
* @throws ProgrammingError In case $url is neither a string nor a instance of Icinga\Web\Url
*/
public function setRedirectUrl($url)
{
if (is_string($url)) {
$url = Url::fromPath($url, array(), $this->getRequest());
} elseif (! $url instanceof Url) {
throw new ProgrammingError('$url must be a string or instance of Icinga\Web\Url');
}
$this->redirectUrl = $url;
return $this;
}
@ -279,12 +287,12 @@ class Form extends Zend_Form
/**
* Return the url to redirect to upon success
*
* @return string|Url
* @return Url
*/
public function getRedirectUrl()
{
if ($this->redirectUrl === null) {
$url = Url::fromRequest(array(), $this->getRequest());
$url = $this->getRequest()->getUrl();
// Be sure to remove all form dependent params because we do not want to submit it again
$this->redirectUrl = $url->without(array_keys($this->getElements()));
}
@ -665,7 +673,7 @@ class Form extends Zend_Form
// TODO(el): Re-evalute this necessity. JavaScript could use the container's URL if there's no action set.
// We MUST set an action as JS gets confused otherwise, if
// this form is being displayed in an additional column
$this->setAction(Url::fromRequest()->without(array_keys($this->getElements())));
$this->setAction($this->getRequest()->getUrl()->without(array_keys($this->getElements())));
}
$this->created = true;
@ -996,12 +1004,20 @@ class Form extends Zend_Form
$formData = $this->getRequestData();
if ($this->getUidDisabled() || $this->wasSent($formData)) {
if (($frameUpload = (bool) $request->getUrl()->shift('_frameUpload', false))) {
$this->getView()->layout()->setLayout('wrapped');
}
$this->populate($formData); // Necessary to get isSubmitted() to work
if (! $this->getSubmitLabel() || $this->isSubmitted()) {
if ($this->isValid($formData)
&& (($this->onSuccess !== null && false !== call_user_func($this->onSuccess, $this))
|| ($this->onSuccess === null && false !== $this->onSuccess()))) {
$this->getResponse()->redirectAndExit($this->getRedirectUrl());
if (! $frameUpload) {
$this->getResponse()->redirectAndExit($this->getRedirectUrl());
} else {
$this->getView()->layout()->redirectUrl = $this->getRedirectUrl()->getAbsoluteUrl();
}
}
} elseif ($this->getValidatePartial()) {
// The form can't be processed but we may want to show validation errors though

View File

@ -6,6 +6,7 @@ namespace Icinga\Web\Form;
use BadMethodCallException;
use Zend_Translate_Adapter;
use Zend_Validate_NotEmpty;
use Zend_Validate_File_MimeType;
use Icinga\Web\Form\Validator\DateTimeValidator;
use Icinga\Web\Form\Validator\ReadablePathValidator;
use Icinga\Web\Form\Validator\WritablePathValidator;
@ -42,10 +43,15 @@ class ErrorLabeller extends Zend_Translate_Adapter
$label = $element->getLabel() ?: $element->getName();
return array(
Zend_Validate_NotEmpty::IS_EMPTY => sprintf(t('%s is required and must not be empty'), $label),
WritablePathValidator::NOT_WRITABLE => sprintf(t('%s is not writable', 'config.path'), $label),
WritablePathValidator::DOES_NOT_EXIST => sprintf(t('%s does not exist', 'config.path'), $label),
ReadablePathValidator::NOT_READABLE => sprintf(t('%s is not readable', 'config.path'), $label),
Zend_Validate_NotEmpty::IS_EMPTY => sprintf(t('%s is required and must not be empty'), $label),
Zend_Validate_File_MimeType::FALSE_TYPE => sprintf(
t('%s (%%value%%) has a false MIME type of "%%type%%"'),
$label
),
Zend_Validate_File_MimeType::NOT_DETECTED => sprintf(t('%s (%%value%%) has no MIME type'), $label),
WritablePathValidator::NOT_WRITABLE => sprintf(t('%s is not writable', 'config.path'), $label),
WritablePathValidator::DOES_NOT_EXIST => sprintf(t('%s does not exist', 'config.path'), $label),
ReadablePathValidator::NOT_READABLE => sprintf(t('%s is not readable', 'config.path'), $label),
DateTimeValidator::INVALID_DATETIME_FORMAT => sprintf(
t('%s not in the expected format: %%value%%'),
$label

View File

@ -309,7 +309,7 @@ EOT;
private function renderRefreshTab()
{
$url = Url::fromRequest()->without('renderLayout');
$url = Icinga::app()->getFrontController()->getRequest()->getUrl();
$tab = $this->get($this->getActiveName());
if ($tab !== null) {

View File

@ -47,6 +47,10 @@ html {
}
}
#fileupload-frame-target {
display: none;
}
#responsive-debug {
font-size: 0.9em;
font-family: Courier new, monospace;

View File

@ -192,13 +192,13 @@
*
*/
submitForm: function (event, autosubmit) {
//return false;
var self = event.data.self;
var icinga = self.icinga;
// .closest is not required unless subelements to trigger this
var $form = $(event.currentTarget).closest('form');
var url = $form.attr('action');
var method = $form.attr('method');
var encoding = $form.attr('enctype');
var $button = $('input[type=submit]:focus', $form).add('button[type=submit]:focus', $form);
var $target;
var data;
@ -230,13 +230,14 @@
method = method.toUpperCase();
}
if (typeof encoding === 'undefined') {
encoding = 'application/x-www-form-urlencoded';
}
if ($button.length === 0) {
$button = $('input[type=submit]', $form).add('button[type=submit]', $form).first();
}
event.stopPropagation();
event.preventDefault();
if ($button.length) {
// Activate spinner
if ($button.hasClass('spinner')) {
@ -266,14 +267,47 @@
url = icinga.utils.addUrlParams(url, dataObj);
} else {
data = $form.serializeArray();
if (encoding === 'multipart/form-data') {
if (typeof window.FormData === 'undefined') {
icinga.loader.submitFormToIframe($form, url, $target);
// Disable all form controls to prevent resubmission as early as possible.
// (This relies on native form submission, so using setTimeout is the only possible solution)
setTimeout(function () {
$form.find(':input:not(:disabled)').prop('disabled', true);
}, 0);
if (! typeof autosubmit === 'undefined' && autosubmit) {
if ($button.length) {
// We're autosubmitting the form so the button has not been clicked, however,
// to be really safe, we're disabling the button explicitly, just in case..
$button.prop('disabled', true);
}
$form[0].submit(); // This should actually not trigger the onSubmit event, let's hope that this is true for all browsers..
event.stopPropagation();
event.preventDefault();
return false;
} else {
return true;
}
}
data = new window.FormData($form[0]);
} else {
data = $form.serializeArray();
}
if (typeof autosubmit === 'undefined' || ! autosubmit) {
if ($button.length && $button.attr('name') !== 'undefined') {
data.push({
name: $button.attr('name'),
value: $button.attr('value')
});
if (encoding === 'multipart/form-data') {
data.append($button.attr('name'), $button.attr('value'));
} else {
data.push({
name: $button.attr('name'),
value: $button.attr('value')
});
}
}
}
}
@ -284,6 +318,8 @@
icinga.loader.loadUrl(url, $target, data, method);
event.stopPropagation();
event.preventDefault();
return false;
},

View File

@ -100,13 +100,25 @@
headers['X-Icinga-WindowId'] = 'undefined';
}
// This is jQuery's default content type
var contentType = 'application/x-www-form-urlencoded; charset=UTF-8';
var isFormData = typeof window.FormData !== 'undefined' && data instanceof window.FormData;
if (isFormData) {
// Setting false is mandatory as the form's data
// won't be recognized by the server otherwise
contentType = false;
}
var self = this;
var req = $.ajax({
type : method,
url : url,
data : data,
headers: headers,
context: self
context: self,
contentType: contentType,
processData: ! isFormData
});
req.$target = $target;
@ -128,6 +140,41 @@
return req;
},
/**
* Mimic XHR form submission by using an iframe
*
* @param {object} $form The form being submitted
* @param {string} action The form's action URL
* @param {object} $target The target container
*/
submitFormToIframe: function ($form, action, $target) {
var self = this;
$form.prop('action', self.icinga.utils.addUrlParams(action, {
'_frameUpload': true
}));
$form.prop('target', 'fileupload-frame-target');
$('#fileupload-frame-target').on('load', function (event) {
var $frame = $(event.target);
var $contents = $frame.contents();
var $redirectMeta = $contents.find('meta[name="redirectUrl"]');
if ($redirectMeta.length) {
self.redirectToUrl($redirectMeta.attr('content'), $target);
} else {
// Fetch the frame's new content and paste it into the target
self.renderContentToContainer(
$contents.find('body').html(),
$target,
'replace'
);
}
$frame.prop('src', 'about:blank'); // Clear the frame's dom
$frame.off('load'); // Unbind the event as it's set on demand
});
},
/**
* Create an URL relative to the Icinga base Url, still unused
*
@ -279,16 +326,34 @@
}
}
this.redirectToUrl(redirect, req.$target, req.getResponseHeader('X-Icinga-Rerender-Layout'));
return true;
},
/**
* Redirect to the given url
*
* @param {string} url
* @param {object} $target
* @param {boolean} rerenderLayout
*/
redirectToUrl: function (url, $target, rerenderLayout) {
var icinga = this.icinga;
if (typeof rerenderLayout === 'undefined') {
rerenderLayout = false;
}
icinga.logger.debug(
'Got redirect for ', req.$target, ', URL was ' + redirect
'Got redirect for ', $target, ', URL was ' + url
);
if (req.getResponseHeader('X-Icinga-Rerender-Layout')) {
var parts = redirect.split(/#!/);
redirect = parts.shift();
var redirectionUrl = this.addUrlFlag(redirect, 'renderLayout');
if (rerenderLayout) {
var parts = url.split(/#!/);
url = parts.shift();
var redirectionUrl = this.addUrlFlag(url, 'renderLayout');
var r = this.loadUrl(redirectionUrl, $('#layout'));
r.url = redirect;
r.url = url;
if (parts.length) {
r.loadNext = parts;
} else if (!! document.location.hash) {
@ -298,28 +363,24 @@
r.loadNext = parts;
}
}
} else {
if (redirect.match(/#!/)) {
var parts = redirect.split(/#!/);
if (url.match(/#!/)) {
var parts = url.split(/#!/);
icinga.ui.layout2col();
this.loadUrl(parts.shift(), $('#col1'));
this.loadUrl(parts.shift(), $('#col2'));
} else {
if (req.$target.attr('id') === 'col2') { // TODO: multicol
if ($('#col1').data('icingaUrl').split('?')[0] === redirect.split('?')[0]) {
if ($target.attr('id') === 'col2') { // TODO: multicol
if ($('#col1').data('icingaUrl').split('?')[0] === url.split('?')[0]) {
icinga.ui.layout1col();
req.$target = $('#col1');
$target = $('#col1');
delete(this.requests['col2']);
}
}
this.loadUrl(redirect, req.$target);
this.loadUrl(url, $target);
}
}
return true;
},
cacheLoadedIcons: function($container) {

View File

@ -92,7 +92,8 @@ class FormTest extends BaseTestCase
public function testWhetherAnExplicitlySetRedirectUrlIsUsedForRedirection()
{
$this->getResponseMock()->shouldReceive('redirectAndExit')->atLeast()->once()->with('special/route');
$this->getResponseMock()->shouldReceive('redirectAndExit')->atLeast()->once()
->with(Mockery::on(function ($url) { return $url->getRelativeUrl() === 'special/route'; }));
$form = new SuccessfulForm();
$form->setTokenDisabled();