8000 Prohibit error response caching by mvorisek · Pull Request #2118 · atk4/ui · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Prohibit error response caching #2118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Oct 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
8000
Diff view
Diff view
40 changes: 26 additions & 14 deletions demos/_unit-test/late-output-error.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,59 @@
/** @var \Atk4\Ui\App $app */
require_once __DIR__ . '/../init-app.php';

$cbH1 = Callback::addTo($app);
$cbH1->setUrlTrigger('err_headers_already_sent_1');
$modalH1 = Modal::addTo($app, ['cb' => $cbH1]);
$modalH1->set(static function () {
$emitLateErrorHFx = static function () {
header('x-unmanaged-header: test');
flush();
});
};

$cbO1 = Callback::addTo($app);
$cbO1->setUrlTrigger('err_unexpected_output_detected_1');
$modalO1 = Modal::addTo($app, ['cb' => $cbO1]);
$modalO1->set(static function () {
$emitLateErrorOFx = static function () {
// unexpected output can be detected only when output buffering is enabled and not flushed
if (ob_get_level() === 0) {
ob_start();
}
echo 'unmanaged output';
});
};

$cbH1 = Callback::addTo($app);
$cbH1->setUrlTrigger('err_headers_already_sent_1');
$modalH1 = Modal::addTo($app, ['cb' => $cbH1]);
$modalH1->set($emitLateErrorHFx);

$cbO1 = Callback::addTo($app);
$cbO1->setUrlTrigger('err_unexpected_output_detected_1');
$modalO1 = Modal::addTo($app, ['cb' => $cbO1]);
$modalO1->set($emitLateErrorOFx);

$cbH2 = CallbackLater::addTo($app);
$cbH2->setUrlTrigger('err_headers_already_sent_2');
$modalH2 = Modal::addTo($app, ['cb' => $cbH2]);
$modalH2->set($modalH1->fx);
$modalH2->set($emitLateErrorHFx);

$cbO2 = CallbackLater::addTo($app);
$cbO2->setUrlTrigger('err_unexpected_output_detected_2');
$modalO2 = Modal::addTo($app, ['cb' => $cbO2]);
$modalO2->set($modalO1->fx);
$modalO2->set($emitLateErrorOFx);

Header::addTo($app, ['content' => 'Before render (/w Callback)']);
Header::addTo($app, ['content' => 'Modal /w Callback']);

$buttonH1 = Button::addTo($app, ['Test LateOutputError I: Headers already sent']);
$buttonH1->on('click', $modalH1->jsShow());

$buttonO1 = Button::addTo($app, ['Test LateOutputError I: Unexpected output detected']);
$buttonO1->on('click', $modalO1->jsShow());

Header::addTo($app, ['content' => 'After render (/w CallbackLater)']);
Header::addTo($app, ['content' => 'Modal /w CallbackLater']);

$buttonH2 = Button::addTo($app, ['Test LateOutputError II: Headers already sent']);
$buttonH2->on('click', $modalH2->jsShow());

$buttonO2 = Button::addTo($app, ['Test LateOutputError II: Unexpected output detected']);
$buttonO2->on('click', $modalO2->jsShow());

Header::addTo($app, ['content' => 'Button callback']);

$buttonH3 = Button::addTo($app, ['Test LateOutputError III: Headers already sent']);
$buttonH3->on('click', $emitLateErrorHFx);

$buttonO3 = Button::addTo($app, ['Test LateOutputError III: Unexpected output detected']);
$buttonO3->on('click', $emitLateErrorOFx);
19 changes: 15 additions & 4 deletions demos/init-db.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,21 @@ protected function isAllowDbModifications(): bool

public function atomic(\Closure $fx)
{
$connection = $this->getModel(true)->getPersistence()->getConnection(); // @phpstan-ignore-line
$eRollback = !$connection->inTransaction()
? new \Exception('Prevent modification')
: null; // TODO replace with atk4/data Connection before commit hook
$eRollback = true;
foreach (array_slice(debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT), 1) as $frame) {
if ($frame['function'] === 'atomic'
&& ($frame['class'] ?? null) === self::class
&& $frame['object']->getModel(true)->getPersistence() === $this->getModel(true)->getPersistence()
) {
$eRollback = null;

break;
}
}
if ($eRollback === true) {
$eRollback = new \Exception('Prevent modification');
}

$res = null;
try {
parent::atomic(function () use ($fx, $eRollback, &$res) {
Expand Down
2 changes: 2 additions & 0 deletions demos/layout/layouts_error.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@
/** @var \Atk4\Ui\App $app */
require_once __DIR__ . '/../init-app.php';

$app->setResponseHeader('Cache-Control', ''); // test if no-store header is sent even if removed

// next line produces exception, which Agile UI will catch and display nicely
View::addTo($app, ['foo' => 'bar']);
4 changes: 2 additions & 2 deletions js/src/helpers/table-dropdown.helper.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import $ from 'external/jquery';
import throttle from 'lodash/throttle';
import lodashThrottle from 'lodash/throttle';

/**
* Simple helper to help displaying Fomantic-UI Dropdown within an atk table.
Expand Down Expand Up @@ -48,7 +48,7 @@ function showTableDropdown() {
}

setCssPosition();
$(window).on('scroll.atktable', throttle(setCssPosition, 10));
$(window).on('scroll.atktable', lodashThrottle(setCssPosition, 10));
$(window).on('resize.atktable', () => {
$that.dropdown('hide');
});
Expand Down
39 changes: 20 additions & 19 deletions js/src/services/api.service.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import $ from 'external/jquery';
import atk from 'atk';
import lodashEscape from 'lodash/escape';

/**
* Handle Fomantic-UI API functionality throughout the app.
Expand Down Expand Up @@ -103,7 +104,7 @@ class ApiService {
throw new Error(response.message);
}
} catch (e) {
atk.apiService.showErrorModal(atk.apiService.getErrorHtml(e.message));
atk.apiService.showErrorModal(atk.apiService.getErrorHtml('API JavaScript Error', e.message));
}
}

Expand All @@ -124,13 +125,13 @@ class ApiService {
atk.apiService.showErrorModal(response.message);
} else {
// check if we have HTML returned by server with <body> content
// TODO test together /w onError using non-200 HTTP AJAX response code
const body = response.match(/<body[^>]*>[\S\s]*<\/body>/gi);
if (body) {
atk.apiService.showErrorModal(body);
} else {
atk.apiService.showErrorModal(response);
}
const body = response.match(/<html[^>]*>.*<body[^>]*>[\S\s]*<\/body>/gi);

atk.apiService.showErrorModal(atk.apiService.getErrorHtml('API Server Error', '') + '<div>' + (
body
? 'body'
: '<pre style="margin-bottom: 0px;"><code style="display: block; padding: 1em; color: #adbac7; background: #22272e;">' + lodashEscape(response) + '</code></pre>'
) + '</div>');
}
}

Expand All @@ -151,7 +152,7 @@ class ApiService {
* Will wrap Fomantic-UI api call into a Promise.
* Can be used to retrieve JSON data from the server.
* Using this will bypass regular successTest i.e. any
* atkjs (javascript) return from server will not be evaluated.
* atkjs (JavaScript) return from server will not be evaluated.
*
* Make sure to control the server output when using
* this function. It must at least return { success: true } in order for
Expand Down Expand Up @@ -193,7 +194,7 @@ class ApiService {
/**
* Display App error in a Fomantic-UI modal.
*/
showErrorModal(errorMsg) {
showErrorModal(contentHtml) {
if (atk.modalService.modals.length > 0) {
const $modal = $(atk.modalService.modals.at(-1));
if ($modal.data('closeOnLoadingError')) {
Expand All @@ -206,18 +207,18 @@ class ApiService {
.appendTo('body')
.addClass('ui scrolling modal')
.css('padding', '1em')
.html(errorMsg);
.html(contentHtml);
m.data('needRemove', true).modal().modal('show');
}

getErrorHtml(error) {
return `<div class="ui negative icon message">
<i class="warning sign icon"></i>
<div class="content">
<div class="header">Javascript Error</div>
<div>${error}</div>
</div>
</div>`;
getErrorHtml(titleHtml, messageHtml) {
return `<div class="ui negative icon message" style="margin: 0px;">
<i class="warning sign icon"></i>
<div class="content">
<div class="header">${titleHtml}</div>
<div>${messageHtml}</div>
</div>
</div>`;
}
}

Expand Down
2 changes: 1 addition & 1 deletion js/src/services/modal.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class ModalService {
response.success = false;
response.isServiceError = true;
response.message = 'Modal service error: Empty HTML, unable to replace modal content from server response';
} else {
} else if (response.id) {
// content is replace no need to do it in api
response.id = null;
}
Expand Down
99 changes: 79 additions & 20 deletions public/js/atkjs-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -1908,6 +1908,8 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var external_jquery__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! external/jquery */ "external/jquery");
/* harmony import */ var external_jquery__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(external_jquery__WEBPACK_IMPORTED_MODULE_5__);
/* harmony import */ var atk__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! atk */ "./src/setup-atk.js");
/* harmony import */ var lodash_escape__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! lodash/escape */ "./node_modules/lodash/escape.js");




Expand Down Expand Up @@ -2011,7 +2013,7 @@ class ApiService {
throw new Error(response.message);
}
} catch (e) {
atk__WEBPACK_IMPORTED_MODULE_6__["default"].apiService.showErrorModal(atk__WEBPACK_IMPORTED_MODULE_6__["default"].apiService.getErrorHtml(e.message));
atk__WEBPACK_IMPORTED_MODULE_6__["default"].apiService.showErrorModal(atk__WEBPACK_IMPORTED_MODULE_6__["default"].apiService.getErrorHtml('API JavaScript Error', e.message));
}
}

Expand All @@ -2032,13 +2034,8 @@ class ApiService {
atk__WEBPACK_IMPORTED_MODULE_6__["default"].apiService.showErrorModal(response.message);
} else {
// check if we have HTML returned by server with <body> content
// TODO test together /w onError using non-200 HTTP AJAX response code
const body = response.match(/<body[^>]*>[\S\s]*<\/body>/gi);
if (body) {
atk__WEBPACK_IMPORTED_MODULE_6__["default"].apiService.showErrorModal(body);
} else {
atk__WEBPACK_IMPORTED_MODULE_6__["default"].apiService.showErrorModal(response);
}
const body = response.match(/<html[^>]*>.*<body[^>]*>[\S\s]*<\/body>/gi);
atk__WEBPACK_IMPORTED_MODULE_6__["default"].apiService.showErrorModal(atk__WEBPACK_IMPORTED_MODULE_6__["default"].apiService.getErrorHtml('API Server Error', '') + '<div>' + (body ? 'body' : '<pre style="margin-bottom: 0px;"><code style="display: block; padding: 1em; color: #adbac7; background: #22272e;">' + (0,lodash_escape__WEBPACK_IMPORTED_MODULE_7__["default"])(response) + '</code></pre>') + '</div>');
}
}

Expand All @@ -2060,7 +2057,7 @@ class ApiService {
* Will wrap Fomantic-UI api call into a Promise.
* Can be used to retrieve JSON data from the server.
* Using this will bypass regular successTest i.e. any
* atkjs (javascript) return from server will not be evaluated.
* atkjs (JavaScript) return from server will not be evaluated.
*
* Make sure to control the server output when using
* this function. It must at least return { success: true } in order for
Expand Down Expand Up @@ -2100,7 +2097,7 @@ class ApiService {
/**
* Display App error in a Fomantic-UI modal.
*/
showErrorModal(errorMsg) {
showErrorModal(contentHtml) {
if (atk__WEBPACK_IMPORTED_MODULE_6__["default"].modalService.modals.length > 0) {
const $modal = external_jquery__WEBPACK_IMPORTED_MODULE_5___default()(atk__WEBPACK_IMPORTED_MODULE_6__["default"].modalService.modals.at(-1));
if ($modal.data('closeOnLoadingError')) {
Expand All @@ -2109,17 +2106,17 @@ class ApiService {
}

// catch application error and display them in a new modal window
const m = external_jquery__WEBPACK_IMPORTED_MODULE_5___default()('<div>').appendTo('body').addClass('ui scrolling modal').css('padding', '1em').html(errorMsg);
const m = external_jquery__WEBPACK_IMPORTED_MODULE_5___default()('<div>').appendTo('body').addClass('ui scrolling modal').css('padding', '1em').html(contentHtml);
m.data('needRemove', true).modal().modal('show');
}
getErrorHtml(error) {
return `<div class="ui negative icon message">
<i class="warning sign icon"></i>
<div class="content">
<div class="header">Javascript Error</div>
<div>${error}</div>
</div>
</div>`;
getErrorHtml(titleHtml, messageHtml) {
return `<div class="ui negative icon message" style="margin: 0px;">
<i class="warning sign icon"></i>
<div class="content">
<div class="header">${titleHtml}</div>
<div>${messageHtml}</div>
</div>
</div>`;
}
}
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Object.freeze(new ApiService()));
Expand Down Expand Up @@ -2587,7 +2584,7 @@ class ModalService {
response.success = false;
response.isServiceError = true;
response.message = 'Modal service error: Empty HTML, unable to replace modal content from server response';
} else {
} else if (response.id) {
// content is replace no need to do it in api
response.id = null;
}
Expand Down Expand Up @@ -43142,6 +43139,68 @@ function debounce(func, wait, options) {
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (debounce);


/***/ }),

/***/ "./node_modules/lodash/escape.js":
/*!***************************************!*\
!*** ./node_modules/lodash/escape.js ***!
\***************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/** Used to map characters to HTML entities. */
const htmlEscapes = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}

/** Used to match HTML entities and HTML characters. */
const reUnescapedHtml = /[&<>"']/g
const reHasUnescapedHtml = RegExp(reUnescapedHtml.source)

/**
* Converts the characters "&", "<", ">", '"', and "'" in `string` to their
* corresponding HTML entities.
*
* **Note:** No other characters are escaped. To escape additional
* characters use a third-party library like [_he_](https://mths.be/he).
*
* Though the ">" character is escaped for symmetry, characters like
* ">" and "/" don't need escaping in HTML and have no special meaning
* unless they're part of a tag or unquoted attribute value. See
* [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands)
* (under "semi-related fun fact") for more details.
*
* When working with HTML you should always
* [quote attribute values](http://wonko.com/post/html-escaping) to reduce
* XSS vectors.
*
* @since 0.1.0
* @category String
* @param {string} [string=''] The string to escape.
* @returns {string} Returns the escaped string.
* @see escapeRegExp, unescape
* @example
*
* escape('fred, barney, & pebbles')
* // => 'fred, barney, &amp; pebbles'
*/
function escape(string) {
return (string && reHasUnescapedHtml.test(string))
? string.replace(reUnescapedHtml, (chr) => htmlEscapes[chr])
: (string || '')
}

/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (escape);


/***/ }),

/***/ "./node_modules/lodash/isObject.js":
Expand Down
2 changes: 1 addition & 1 deletion public/js/atkjs-ui.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/js/atkjs-ui.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/js/atkjs-ui.min.js.map
494F

Large diffs are not rendered by default.

Loading
0