Skip to content
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

Stream only specific responses #18348

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ The present file will list all changes made to the project; according to the
Building and executing raw queries using `DBmysql::request()`, `DBmysqlIterator::buildQuery()` and `DBmysqlIterator::execute()` methods is also prohibited.
To execute DB queries, either `DBmysql::request()` can be used to craft query using the GLPI query builder,
or `DBmysql::doQuery()` can be used for safe queries to execute DB query using a self-crafted a SQL string.
- The dynamic progress bar provided by the `Html` class no longer works. The `TODO` js function can be used as a replacement to display the progress of a process.
- `js/fuzzysearch.js` replaced with `FuzzySearch/Modal` Vue component.
- `Html::fuzzySearch()` replaced with `Html::getMenuFuzzySearchList()` function.
- `NotificationEvent::raiseEvent()` signature cahnged. A new `$trigger` parameter has been added at 4th position, and `$label` is now the 5th parameter.
Expand Down Expand Up @@ -265,17 +266,22 @@ The present file will list all changes made to the project; according to the
- `Glpi\Toolbox\Sanitizer::sanitize()`
- `Glpi\Toolbox\Sanitizer::unsanitize()`
- `Html::ajaxFooter()`
- `Html::changeProgressBarMessage()`
- `Html::changeProgressBarPosition()`
- `Html::cleanInputText()`
- `Html::cleanPostForTextArea()`
- `Html::createProgressBar()`
- `Html::displayErrorAndDie()`. Throw a `Glpi\Exception\Http\BadRequestHttpException` exception instead.
- `Html::displayNotFoundError()`. Throw a `Glpi\Exception\Http\NotFoundHttpException` exception instead.
- `Html::displayProgressBar()`
- `Html::displayRightError()`. Throw a `Glpi\Exception\Http\AccessDeniedHttpException` exception instead.
- `Html::entities_deep()`
- `Html::entity_decode_deep()`
- `Html::glpi_flush()`
- `Html::jsGetElementbyID()`
- `Html::jsGetDropdownValue()`
- `Html::jsSetDropdownValue()`
- `Html::progressBar()`
- `HookManager::enableCSRF()`
- `ITILFollowup::ADDMYTICKET` constant. Use `ITILFollowup::ADDMY`.
- `ITILFollowup::ADDGROUPTICKET` constant. Use `ITILFollowup::ADD_AS_GROUP`.
Expand Down
45 changes: 33 additions & 12 deletions front/cron.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
* ---------------------------------------------------------------------
*/

use Symfony\Component\HttpFoundation\StreamedResponse;

if (PHP_SAPI === 'cli') {
// Check the resources state before trying to instanciate the Kernel.
// It must be done here as this check must be done even when the Kernel
Expand Down Expand Up @@ -71,20 +73,39 @@
}

if (!isCommandLine()) {
//The advantage of using background-image is that cron is called in a separate
//request and thus does not slow down output of the main page as it would if called
//from there.
// The advantage of using background-image is that cron is called in a separate
// request and thus does not slow down output of the main page as it would if called
// from there.
$image = pack("H*", "47494638396118001800800000ffffff00000021f90401000000002c0000000" .
"018001800000216848fa9cbed0fa39cb4da8bb3debcfb0f86e248965301003b");
header("Content-Type: image/gif");
header("Content-Length: " . strlen($image));
header("Cache-Control: no-cache,no-store");
header("Pragma: no-cache");
header("Connection: close");
echo $image;
flush();

CronTask::launch(CronTask::MODE_INTERNAL);

// Be sure to remove any unexpected header value.
header_remove();

return new StreamedResponse(
function () use ($image) {
// Be sure to disable the output buffering.
while (ob_get_level() > 0) {
ob_end_clean();
}

echo $image;

// Send headers and image content.
// The browser will consider that the response is complete due to the `Connection: close` header
// and will not have to wait for the cron tasks to be processed to consider the request as ended.
flush();

CronTask::launch(CronTask::MODE_INTERNAL);
},
headers: [
'Content-Type' => 'image/gif',
'Content-Length' => strlen($image),
'Cache-Control' => 'no-cache,no-store',
'Pragma' => 'no-cache',
'Connection' => 'close',
]
);
} else if (isset($_SERVER['argc']) && ($_SERVER['argc'] > 1)) {
// Parse command line options

Expand Down
4 changes: 4 additions & 0 deletions front/document.send.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,16 @@
* ---------------------------------------------------------------------
*/

use Glpi\Application\ErrorHandler;
use Glpi\Inventory\Conf;
use Glpi\Exception\Http\AccessDeniedHttpException;
use Glpi\Exception\Http\BadRequestHttpException;
use Glpi\Exception\Http\HttpException;
use Glpi\Exception\Http\NotFoundHttpException;

// Prevent errors to breaks the output
ErrorHandler::getInstance()->disableOutput();

$doc = new Document();

if (isset($_GET['docid'])) {
Expand Down
6 changes: 6 additions & 0 deletions front/report.dynamic.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* ---------------------------------------------------------------------
*/

use Glpi\Application\ErrorHandler;
use Glpi\Exception\Http\AccessDeniedHttpException;

if (!isset($_GET['item_type']) || !is_string($_GET['item_type']) || !is_a($_GET['item_type'], CommonGLPI::class, true)) {
Expand All @@ -55,6 +56,11 @@
$_GET["export_all"] = 1;
}

if (!in_array($_GET["display_type"], [Search::HTML_OUTPUT, Search::GLOBAL_SEARCH])) {
// Prevent errors to breaks the output
ErrorHandler::getInstance()->disableOutput();
}

switch ($itemtype) {
case 'KnowbaseItem':
KnowbaseItem::showList($_GET, $_GET["is_faq"]);
Expand Down
55 changes: 51 additions & 4 deletions src/Glpi/Controller/LegacyFileLoadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,58 @@ public function __invoke(Request $request): Response
throw new \RuntimeException('Cannot load legacy controller without specifying a file to load.');
}

$callback = function () use ($target_file) {
require $target_file;
};
\ob_start();
$response = require($target_file);
$content = ob_get_clean();

return new HeaderlessStreamedResponse($callback->bindTo($this, self::class));
if ($response instanceof Response) {
// The legacy file contains a return value that corresponds to a valid Symfony response.
// This response is returned and any output is discarded.

if ($content !== '') {
\trigger_error(
sprintf('Unexpected output detected in `%s`.', $target_file),
E_USER_WARNING
);
}

return $response;
}

if (\headers_sent()) {
// Headers are already sent, so we have to use a streamed response without headers.
// This may happen when `flush()`/`ob_flush()`/`ob_clean()`/`ob_end_clean()` functions are used
// in the legacy script.

\trigger_error(
sprintf('Unexpected output detected in `%s`.', $target_file),
E_USER_WARNING
);

return new HeaderlessStreamedResponse(function () use ($content) {
echo $content;
});
}

// Extract already defined headers to set them in the Symfony response.
// This is required as Symfony will set default values for some of them if we do not provide them.
// see `Symfony\Component\HttpFoundation\ResponseHeaderBag`
$headers = [];
foreach (\headers_list() as $header_line) {
[$header_name, $header_value] = \explode(':', $header_line, 2);

$header_name = \trim($header_name);
$header_value = \trim($header_value);

if (!\array_key_exists($header_name, $headers)) {
$headers[$header_name] = [];
}

$headers[$header_name][] = $header_value;
}
\header_remove();

return new Response($content, status: \http_response_code(), headers: $headers);
}

protected function setAjax(): void
Expand Down
5 changes: 0 additions & 5 deletions src/Glpi/Marketplace/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,6 @@ public static function discover(
$nb_plugins
);

// Clear all output buffers
while (ob_get_level()) {
ob_end_clean();
}

header("X-GLPI-Marketplace-Total: $nb_plugins");
self::displayList($plugins, "discover", $only_lis, $nb_plugins, $sort, $api->isListTruncated());
}
Expand Down
10 changes: 0 additions & 10 deletions src/Glpi/Search/Output/Spreadsheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,6 @@ public function displayData(array $data, array $params = [])
return false;
}

// Clear output buffers
$ob_config = ini_get('output_buffering');
$max_level = filter_var($ob_config, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
while (ob_get_level() > $max_level) {
ob_end_clean();
}
if (ob_get_level() > 0) {
ob_clean();
}

$spread = $this->getSpreasheet();
$writer = $this->getWriter();

Expand Down
66 changes: 49 additions & 17 deletions src/Html.php
Original file line number Diff line number Diff line change
Expand Up @@ -747,9 +747,17 @@ public static function getConfirmationOnActionScript($string, $additionalactions
*
*
* @return string|void Generated HTML if `display` param is true, void otherwise.
**/
*
* @deprecated 11.0.0
*/
public static function progressBar($id, array $options = [])
{
Toolbox::deprecated(
'`Html::progressBar()` is deprecated.'
. ' Use the `Html::getProgressBar()` method to get a static progress bar HTML snippet,'
. ' or the `TODO` JS function to display a progress bar related to a process progression.'
);

$params = [
'create' => false,
'message' => null,
Expand Down Expand Up @@ -850,12 +858,17 @@ public static function progressBar($id, array $options = [])
* @param array $options See {@link Html::progressBar()} for available options (excluding message)
*
* @return string|void
*
* @deprecated 11.0.0
* @see Html::progressBar()
**/
*/
public static function createProgressBar($msg = null, array $options = [])
{
Toolbox::deprecated('Html::createProgressBar is deprecated. Use Html::progressBar instead.');
Toolbox::deprecated(
'`Html::createProgressBar()` is deprecated.'
. ' Use the `Html::getProgressBar()` method to get a static progress bar HTML snippet,'
. ' or the `TODO` JS function to display a progress bar related to a process progression.'
);

$options = array_replace([
'create' => true,
'display' => true
Expand All @@ -874,9 +887,16 @@ public static function createProgressBar($msg = null, array $options = [])
* @param string $msg message under the bar
*
* @return void
**/
*
* @deprecated 11.0.0
*/
public static function changeProgressBarMessage($msg = " ")
{
Toolbox::deprecated(
'`Html::changeProgressBarMessage()` is deprecated.'
. ' Use the `TODO` JS function to display a progress bar related to a process progression.'
);

self::progressBar('doaction_progress', ['message' => $msg]);
self::glpi_flush();
}
Expand All @@ -890,9 +910,16 @@ public static function changeProgressBarMessage($msg = " ")
* @param string $msg message inside the bar (default is %)
*
* @return void
**/
*
* @deprecated 11.0.0
*/
public static function changeProgressBarPosition($crt, $tot, $msg = "")
{
Toolbox::deprecated(
'`Html::changeProgressBarPosition()` is deprecated.'
. ' Use the `TODO` JS function to display a progress bar related to a process progression.'
);

$options = [];

if (!$tot) {
Expand Down Expand Up @@ -923,9 +950,17 @@ public static function changeProgressBarPosition($crt, $tot, $msg = "")
* - forcepadding : boolean force str_pad to force refresh (default true)
*
* @return void
**/
*
* @deprecated 11.0.0
*/
public static function displayProgressBar($width, $percent, $options = [])
{
Toolbox::deprecated(
'`Html::displayProgressBar()` is deprecated.'
. ' Use the `Html::getProgressBar()` method to get a static progress bar HTML snippet,'
. ' or the `TODO` JS function to display a progress bar related to a process progression.'
);

$param['title'] = __('Progress');
$param['simple'] = false;
$param['forcepadding'] = true;
Expand Down Expand Up @@ -2109,18 +2144,15 @@ public static function popFooter()
* Flushes the system write buffers of PHP and whatever backend PHP is using (CGI, a web server, etc).
* This attempts to push current output all the way to the browser with a few caveats.
* @see https://www.sitepoint.com/php-streaming-output-buffering-explained/
**/
*
* @deprecated 11.0.0
*/
public static function glpi_flush()
{

if (
function_exists("ob_flush")
&& (ob_get_length() !== false)
) {
ob_flush();
}

flush();
trigger_error(
'`Html::glpi_glush()` no longer has any effect.',
E_USER_WARNING
);
}


Expand Down
15 changes: 0 additions & 15 deletions src/Toolbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -592,21 +592,6 @@ public static function sendFile($file, $filename, $mime = null, $expires_headers
$etag = md5_file($file);
$lastModified = filemtime($file);

// Make sure there is nothing in the output buffer (In case stuff was added by core or misbehaving plugin).
// If there is any extra data, the sent file will be corrupted.
// 1. Turn off any extra buffering level. Keep one buffering level if PHP output_buffering directive is not "off".
$ob_config = ini_get('output_buffering');
$max_buffering_level = $ob_config !== false && (strtolower($ob_config) === 'on' || (is_numeric($ob_config) && (int)$ob_config > 0))
? 1
: 0;
while (ob_get_level() > $max_buffering_level) {
ob_end_clean();
}
// 2. Clean any buffered output in remaining level (output_buffering="on" case).
if (ob_get_level() > 0) {
ob_clean();
}

$headers = [
'Last-Modified' => gmdate("D, d M Y H:i:s", $lastModified) . " GMT",
'Etag' => $etag,
Expand Down
Loading