Skip to content
Open
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 @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Fixed

- Fix crash when importing an ISO8601 datetime value with microseconds/timezone

## [2.15.8] - 2026-06-24

### Fixed
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../PluginsMakefile.mk
11 changes: 10 additions & 1 deletion inc/clientinjection.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use function Safe\json_encode;
use function Safe\readfile;
use function Safe\unlink;
use function Safe\unserialize;

class PluginDatainjectionClientInjection
{
Expand Down Expand Up @@ -217,7 +218,15 @@ public static function processBatch(int $offset, int $batch_size): array

for ($i = $offset; $i < $end; $i++) {
$injectionline = $i + $header_offset;
$result = $engine->injectLine($lines[$i][0], $injectionline);
try {
$result = $engine->injectLine($lines[$i][0], $injectionline);
} catch (Throwable $e) {
ErrorHandler::logCaughtException($e);
$result = [
'status' => PluginDatainjectionCommonInjectionLib::FAILED,
'line' => $injectionline,
];
}
$results[] = $result;

if ($result['status'] != PluginDatainjectionCommonInjectionLib::SUCCESS) {
Expand Down
56 changes: 56 additions & 0 deletions inc/commoninjectionlib.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,12 @@ private function reformatThirdPass()
$this->setValueForItemtype($itemtype, $field, $date);
break;

case "datetime":
//Normalize to "Y-m-d H:i:s", stripping ISO8601 microseconds/timezone if present
$datetime = self::reformatDateTime($value, $this->getDateFormat());
$this->setValueForItemtype($itemtype, $field, $datetime);
break;

case "mac":
$this->setValueForItemtype($itemtype, $field, self::reformatMacAddress($value));
break;
Expand Down Expand Up @@ -1228,6 +1234,44 @@ private static function reformatDate($original_date, $date_format)
}


/**
* Reformat a datetime value to the MySQL "Y-m-d H:i:s" format expected by the DB.
* Accepts ISO8601 values (with a "T" separator, microseconds and/or a timezone offset)
* as well as the same date formats supported by reformatDate().
*
* @param string $original_datetime the original datetime
* @param string $date_format the configured date format (dd-mm-yyyy, mm-dd-yyyy, yyyy-mm-dd)
*
* @return string the datetime reformated, if needed
**/
private static function reformatDateTime($original_datetime, $date_format)
{

if (empty($original_datetime)) {
return "NULL"; // required to avoid "0000-00-00 00:00:00" in the DB
}

$original_datetime = trim($original_datetime);
$separator = (str_contains($original_datetime, 'T')) ? 'T' : ' ';
[$date_part, $time_part] = array_pad(explode($separator, $original_datetime, 2), 2, '00:00:00');

// Strip timezone offset (+00:00, -0500, Z) and microseconds from the time part
$time_part = preg_replace('/(Z|[+-]\d{2}:?\d{2})$/', '', $time_part);
$time_part = preg_replace('/\.\d+/', '', $time_part);
$time_part = trim($time_part);
if ($time_part === '') {
$time_part = '00:00:00';
}

$new_date = self::reformatDate($date_part, $date_format);
if ($new_date === "NULL") {
return "NULL";
}

return $new_date . ' ' . $time_part;
}


/**
* Reformat mac adress if mac doesn't contains : or - as seperator
*
Expand Down Expand Up @@ -1387,6 +1431,12 @@ private function checkType($injectionClass, $option, $field_name, $data, $mandat
$res = preg_match($pat, $data, $regs);
return ($res !== 0 ? self::SUCCESS : self::TYPE_MISMATCH);

case 'datetime':
// Datetime is already reformated to "Y-m-d H:i:s" by reformatThirdPass()
$pat = '/^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})$/';
$res = preg_match($pat, $data, $regs);
return ($res !== 0 ? self::SUCCESS : self::TYPE_MISMATCH);

case 'ip':
preg_match("/([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/", $data, $regs);
return ((count($regs) > 0) ? self::SUCCESS : self::TYPE_MISMATCH);
Expand Down Expand Up @@ -2231,6 +2281,12 @@ public static function addToSearchOptions(
$type_searchOptions[$id]['displaytype'] = 'dropdown';
$tmp['displaytype'] = 'dropdown';
}
//Some injection.class files are missing checktype for datetime fields.
//Without it, the value is never reformated/validated before being sent to the DB.
if ((isset($tmp['datatype']) && $tmp['datatype'] == 'datetime') && !isset($tmp['checktype'])) {
$type_searchOptions[$id]['checktype'] = 'datetime';
$tmp['checktype'] = 'datetime';
}
if (isset($tmp['linkfield']) && !isset($tmp['displaytype'])) {
$type_searchOptions[$id]['displaytype'] = 'text';
}
Expand Down
133 changes: 133 additions & 0 deletions tests/unit/CommonInjectionLibDateTimeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

/**
* -------------------------------------------------------------------------
* DataInjection plugin for GLPI
* -------------------------------------------------------------------------
*
* LICENSE
*
* This file is part of DataInjection.
*
* DataInjection is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* DataInjection is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DataInjection. If not, see <http://www.gnu.org/licenses/>.
* -------------------------------------------------------------------------
* @copyright Copyright (C) 2007-2023 by DataInjection plugin team.
* @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html
* @link https://github.com/pluginsGLPI/datainjection
* -------------------------------------------------------------------------
*/

namespace GlpiPlugin\Datainjection\Tests\Unit;

use Glpi\Tests\DbTestCase;
use Computer;
use PluginDatainjectionComputerInjection;
use PluginDatainjectionCommonInjectionLib;
use ReflectionMethod;

final class CommonInjectionLibDateTimeTest extends DbTestCase
{
public static function reformatDateTimeProvider(): array
{
return [
'iso8601 with microseconds and utc offset' => [
'original' => '2026-04-15T13:51:30.935291+00:00',
'date_format' => PluginDatainjectionCommonInjectionLib::DATE_TYPE_YYYYMMDD,
'expected' => '2026-04-15 13:51:30',
],
'iso8601 with z timezone' => [
'original' => '2026-04-15T13:51:30Z',
'date_format' => PluginDatainjectionCommonInjectionLib::DATE_TYPE_YYYYMMDD,
'expected' => '2026-04-15 13:51:30',
],
'space separated, already valid' => [
'original' => '2026-04-15 13:51:30',
'date_format' => PluginDatainjectionCommonInjectionLib::DATE_TYPE_YYYYMMDD,
'expected' => '2026-04-15 13:51:30',
],
'date only, no time part' => [
'original' => '2026-04-15',
'date_format' => PluginDatainjectionCommonInjectionLib::DATE_TYPE_YYYYMMDD,
'expected' => '2026-04-15 00:00:00',
],
'dd-mm-yyyy format with time' => [
'original' => '15-04-2026 13:51:30',
'date_format' => PluginDatainjectionCommonInjectionLib::DATE_TYPE_DDMMYYYY,
'expected' => '2026-04-15 13:51:30',
],
'empty value' => [
'original' => '',
'date_format' => PluginDatainjectionCommonInjectionLib::DATE_TYPE_YYYYMMDD,
'expected' => 'NULL',
],
];
}

/**
* @dataProvider reformatDateTimeProvider
*/
public function testReformatDateTime(string $original, string $date_format, string $expected): void
{
$reformat_datetime = new ReflectionMethod(
PluginDatainjectionCommonInjectionLib::class,
'reformatDateTime',
);

self::assertSame($expected, $reformat_datetime->invoke(null, $original, $date_format));
}

/**
* Reproduces ticket #45007: a datetime value with microseconds and a timezone offset
* must not reach the DB as-is, and must not crash the injection.
*/
public function testDatetimeWithMicrosecondsAndTimezoneIsInjected(): void
{
$computer = $this->createItem(Computer::class, [
'name' => 'Test_Computer_Datetime',
'entities_id' => 0,
]);

$injection_class = new PluginDatainjectionComputerInjection();

$lib = new PluginDatainjectionCommonInjectionLib(
$injection_class,
[
'Computer' => [
'name' => 'Test_Computer_Datetime',
'last_inventory_update' => '2026-04-15T13:51:30.935291+00:00',
],
],
[
'rights' => [
'can_add' => true,
'can_update' => true,
'add_dropdown' => true,
],
'mandatory_fields' => [
'Computer' => ['name' => true],
],
'entities_id' => 0,
],
);

$lib->processAddOrUpdate();
$results = $lib->getInjectionResults();

self::assertSame(PluginDatainjectionCommonInjectionLib::SUCCESS, $results['status']);
self::assertSame(PluginDatainjectionCommonInjectionLib::IMPORT_UPDATE, $results['type']);

$computer->getFromDB($computer->getID());
self::assertSame('2026-04-15 13:51:30', $computer->fields['last_inventory_update']);
}
}
Loading