diff --git a/CHANGELOG.md b/CHANGELOG.md index 76d19af4..f4e9e073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ef1bed5a --- /dev/null +++ b/Makefile @@ -0,0 +1 @@ +include ../../PluginsMakefile.mk diff --git a/ajax/injection.php b/ajax/injection.php index 7282477a..3725c856 100644 --- a/ajax/injection.php +++ b/ajax/injection.php @@ -28,6 +28,8 @@ * ------------------------------------------------------------------------- */ +use function Safe\unserialize; + // Direct access to file if (strpos($_SERVER['PHP_SELF'], "injection.php")) { header("Content-Type: text/html; charset=UTF-8"); diff --git a/ajax/results.php b/ajax/results.php index c1e28b31..704c30d4 100644 --- a/ajax/results.php +++ b/ajax/results.php @@ -28,6 +28,8 @@ * ------------------------------------------------------------------------- */ +use function Safe\unserialize; + // Direct access to file if (strpos($_SERVER['PHP_SELF'], "results.php")) { header("Content-Type: text/html; charset=UTF-8"); diff --git a/front/clientinjection.form.php b/front/clientinjection.form.php index 7eff9e6e..5dbf9789 100644 --- a/front/clientinjection.form.php +++ b/front/clientinjection.form.php @@ -28,6 +28,8 @@ * ------------------------------------------------------------------------- */ +use function Safe\unserialize; + Session::checkRight("plugin_datainjection_use", READ); Html::header( diff --git a/inc/clientinjection.class.php b/inc/clientinjection.class.php index 66b4e210..561604a8 100644 --- a/inc/clientinjection.class.php +++ b/inc/clientinjection.class.php @@ -41,6 +41,7 @@ use function Safe\json_encode; use function Safe\readfile; use function Safe\unlink; +use function Safe\unserialize; class PluginDatainjectionClientInjection { @@ -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) { diff --git a/inc/commoninjectionlib.class.php b/inc/commoninjectionlib.class.php index a628cdb9..501fae9e 100644 --- a/inc/commoninjectionlib.class.php +++ b/inc/commoninjectionlib.class.php @@ -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; @@ -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 * @@ -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); @@ -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'; } diff --git a/inc/mapping.class.php b/inc/mapping.class.php index 98f059d1..19d87f47 100644 --- a/inc/mapping.class.php +++ b/inc/mapping.class.php @@ -32,6 +32,7 @@ use function Safe\ob_get_clean; use function Safe\ob_start; +use function Safe\unserialize; class PluginDatainjectionMapping extends CommonDBTM { diff --git a/inc/model.class.php b/inc/model.class.php index 29b03cf1..7c6f790a 100644 --- a/inc/model.class.php +++ b/inc/model.class.php @@ -33,6 +33,7 @@ use function Safe\json_decode; use function Safe\realpath; use function Safe\tempnam; +use function Safe\unserialize; /** * ------------------------------------------------------------------------- diff --git a/tests/unit/CommonInjectionLibDateTimeTest.php b/tests/unit/CommonInjectionLibDateTimeTest.php new file mode 100644 index 00000000..d93fabcd --- /dev/null +++ b/tests/unit/CommonInjectionLibDateTimeTest.php @@ -0,0 +1,133 @@ +. + * ------------------------------------------------------------------------- + * @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']); + } +}