diff --git a/application/layouts/scripts/layout.phtml b/application/layouts/scripts/layout.phtml
index cbfce7714..1d766b505 100644
--- a/application/layouts/scripts/layout.phtml
+++ b/application/layouts/scripts/layout.phtml
@@ -54,6 +54,7 @@ $iframeClass = $isIframe ? ' iframe' : '';
= $this->render('body.phtml') ?>
+
diff --git a/application/layouts/scripts/wrapped.phtml b/application/layouts/scripts/wrapped.phtml
new file mode 100644
index 000000000..25a7cf2e4
--- /dev/null
+++ b/application/layouts/scripts/wrapped.phtml
@@ -0,0 +1,10 @@
+
+
+ layout()->redirectUrl)): ?>
+
+
+
+
+ = $this->render('inline.phtml'); ?>
+
+
\ No newline at end of file
diff --git a/library/Icinga/Web/Controller/ActionController.php b/library/Icinga/Web/Controller/ActionController.php
index 571f0792f..750c101f3 100644
--- a/library/Icinga/Web/Controller/ActionController.php
+++ b/library/Icinga/Web/Controller/ActionController.php
@@ -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());
diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php
index 326730ac3..dc3eaf1cc 100644
--- a/library/Icinga/Web/Form.php
+++ b/library/Icinga/Web/Form.php
@@ -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
diff --git a/library/Icinga/Web/Form/ErrorLabeller.php b/library/Icinga/Web/Form/ErrorLabeller.php
index f66260149..803e1d80d 100644
--- a/library/Icinga/Web/Form/ErrorLabeller.php
+++ b/library/Icinga/Web/Form/ErrorLabeller.php
@@ -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
diff --git a/library/Icinga/Web/Widget/Tabs.php b/library/Icinga/Web/Widget/Tabs.php
index aa5e7cda6..220393aff 100644
--- a/library/Icinga/Web/Widget/Tabs.php
+++ b/library/Icinga/Web/Widget/Tabs.php
@@ -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) {
diff --git a/public/css/icinga/layout-structure.less b/public/css/icinga/layout-structure.less
index 2b4228b5f..3f9d4858e 100644
--- a/public/css/icinga/layout-structure.less
+++ b/public/css/icinga/layout-structure.less
@@ -47,6 +47,10 @@ html {
}
}
+#fileupload-frame-target {
+ display: none;
+}
+
#responsive-debug {
font-size: 0.9em;
font-family: Courier new, monospace;
diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js
index 00a0ff9f4..000ecf103 100644
--- a/public/js/icinga/events.js
+++ b/public/js/icinga/events.js
@@ -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;
},
diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js
index b2e8061e2..da23ebea8 100644
--- a/public/js/icinga/loader.js
+++ b/public/js/icinga/loader.js
@@ -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) {
diff --git a/test/php/library/Icinga/Web/FormTest.php b/test/php/library/Icinga/Web/FormTest.php
index 0fff50a3f..df17c4118 100644
--- a/test/php/library/Icinga/Web/FormTest.php
+++ b/test/php/library/Icinga/Web/FormTest.php
@@ -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();