icingadb-web/library/Icingadb/Data/JsonResultSetUtils.php
Sukhwinder Dhillon 15cb8d97f8 Fix leading comma in JSON export (#1349)
When exporting JSON with a limit and a page > 1 set, a leading comma is added
before the first item because `$offset !== 0` evaluates to true, resulting in
invalid JSON.

Replace the offset‑based condition with a `$first` flag to track whether any
item has been written yet.

(cherry picked from commit b60448bc77)
2026-04-01 10:40:26 +02:00

142 lines
4 KiB
PHP

<?php
/* Icinga DB Web | (c) 2024 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Data;
use DateTime;
use DateTimeZone;
use Icinga\Module\Icingadb\Model\DependencyNode;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Util\Json;
use ipl\Orm\Model;
use ipl\Orm\Query;
trait JsonResultSetUtils
{
/**
* @return array<string, ?string>
*/
public function current(): array
{
return $this->createObject(parent::current());
}
protected function formatValue(string $key, $value)
{
if (
$value
&& (
$key === 'id'
|| substr($key, -3) === '_id'
|| substr($key, -3) === '.id'
|| substr($key, -9) === '_checksum'
|| substr($key, -4) === '_bin'
)
) {
$value = bin2hex($value);
}
if ($value instanceof DateTime) {
return $value->setTimezone(new DateTimeZone('UTC'))
->format('Y-m-d\TH:i:s.vP');
}
return $value;
}
protected function createObject(Model $model): array
{
$keysAndValues = [];
foreach ($model as $key => $value) {
if ($value instanceof Model) {
$object = $this->createObject($value);
// If there is no value in the model or it's descendents,
// it was not a part of the query, so no JSON object will be created for this model.
if (! empty($object)) {
$keysAndValues[$key] = $object;
}
} else {
$keysAndValues[$key] = $this->formatValue($key, $value);
}
}
return $keysAndValues;
}
public static function stream(Query $query): void
{
$model = $query->getModel();
if ($model instanceof Host || $model instanceof Service || $model instanceof DependencyNode) {
$query->setResultSetClass(VolatileJsonResults::class);
} else {
$query->setResultSetClass(__CLASS__);
}
if ($query->hasLimit()) {
// Custom limits should still apply
$query->peekAhead(false);
$offset = $query->getOffset() ?? 0;
} else {
$query->limit(1000);
$query->peekAhead();
$offset = 0;
}
$first = true;
echo '[';
do {
$query->offset($offset);
$result = $query->execute()->disableCache();
foreach ($result as $object) {
if ($first) {
$first = false;
} else {
echo ",\n";
}
echo Json::sanitize($object);
self::giveMeMoreTime();
}
$offset += 1000;
} while ($result->hasMore());
echo ']';
exit;
}
/**
* Grant the caller more time to work with
*
* This resets the execution time before it runs out. The advantage of this, compared with no execution time
* limit at all, is that only the caller can bypass the limit. Any other (faulty) code will still be stopped.
*
* @internal Don't use outside of {@see JsonResultSet::stream()} or {@see CsvResultSet::stream()}
*
* @return void
*/
public static function giveMeMoreTime()
{
$spent = getrusage();
if ($spent !== false) {
$maxExecutionTime = ini_get('max_execution_time');
if (! $maxExecutionTime || ! is_numeric($maxExecutionTime)) {
$maxExecutionTime = 30;
} else {
$maxExecutionTime = (int) $maxExecutionTime;
}
if ($maxExecutionTime > 0) {
$timeRemaining = $maxExecutionTime - $spent['ru_utime.tv_sec'] % $maxExecutionTime;
if ($timeRemaining <= 5) {
set_time_limit($maxExecutionTime);
}
}
}
}
}