Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ 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]
## [Unreleased]

### Fixed

- Fix crash when importing an ISO8601 datetime value with microseconds/timezone
- Fix : confirm dialog js escaping

## [2.15.8] - 2026-06-24
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
10 changes: 9 additions & 1 deletion inc/clientinjection.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,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',
],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
],
],
'iso8601 with negative utc offset' => [
'original' => '2026-04-15T13:51:30.000000-05:00',
'date_format' => PluginDatainjectionCommonInjectionLib::DATE_TYPE_YYYYMMDD,
'expected' => '2026-04-15 13:51:30',
],

No test case for a datetime with a negative UTC offset (e.g. -05:00), the regex handles it but it is untested.

'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',
],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
],
],
'mm-dd-yyyy format with time' => [
'original' => '04-15-2026 13:51:30',
'date_format' => PluginDatainjectionCommonInjectionLib::DATE_TYPE_MMDDYYYY,
'expected' => '2026-04-15 13:51:30',
],

Missing DATE_TYPE_MMDDYYYY format

];
}

/**
* @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']);
}
}