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' : '';
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)): ?> + + + + + 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();