diff --git a/library/Icingadb/Model/CustomvarFlat.php b/library/Icingadb/Model/CustomvarFlat.php index c9d0afe67..97b0254fd 100644 --- a/library/Icingadb/Model/CustomvarFlat.php +++ b/library/Icingadb/Model/CustomvarFlat.php @@ -101,9 +101,19 @@ public function unFlattenVars(Traversable $flattenedVars): array $step = $m[1]; } - while ($source !== null && ! empty($path) && (! is_array($source) || ! isset($source[$step]))) { - $step = sprintf($isIndex ? '[%d].%s' : '%s.%s', $step, array_shift($path)); - $isIndex = false; + if ($source !== null) { + while (! isset($source[$step])) { + if ($isIndex) { + $step = sprintf('[%d]', $step); + $isIndex = false; + } else { + if (empty($path)) { + break; + } + + $step = implode('.', [$step, array_shift($path)]); + } + } } if (! empty($path)) { @@ -113,7 +123,10 @@ public function unFlattenVars(Traversable $flattenedVars): array $registerValue($data[$step], $source[$step] ?? null, $path, $value); } else { - $data[$step] = $value; + // Since empty custom vars of type dictionaries and arrays have null values in customvar_flat table, + // we won't be able to render them as such. Therefore, we have to use the value of the `customvar` + // table if it's not null, otherwise the current value, which is a "null" string. + $data[$step] = $source[$step] ?? $value; } }; @@ -130,7 +143,12 @@ public function unFlattenVars(Traversable $flattenedVars): array $source = [$realName => $var->customvar->value]; $sourcePath = ltrim(substr($var->flatname, strlen($realName)), '.'); - $path = array_merge([$realName], $sourcePath ? explode('.', $sourcePath) : []); + $path = array_merge( + [$realName], + $sourcePath + ? preg_split('/(?<=\w|])\.|(?flatname); $source = null; diff --git a/library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php b/library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php index ae050f130..ad35c7456 100644 --- a/library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php +++ b/library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php @@ -148,7 +148,7 @@ protected function fetchValueSuggestions($column, $searchTerm, Filter\Chain $sea } $columnPath = $query->getResolver()->qualifyPath($column, $model->getTableName()); - list($targetPath, $columnName) = preg_split('/(?<=vars)\.|\.(?=[^.]+$)/', $columnPath); + list($targetPath, $columnName) = preg_split('/(?<=vars)\.|\.(?=[^.]+$)/', $columnPath, 2); $isCustomVar = false; if (substr($targetPath, -5) === '.vars') { @@ -212,15 +212,16 @@ protected function fetchColumnSuggestions($searchTerm) // Custom variables only after the columns are exhausted and there's actually a chance the user sees them $titleAdded = false; + $parsedArrayVars = []; foreach ($this->getDb()->select($this->queryCustomvarConfig($searchTerm)) as $customVar) { $search = $name = $customVar->flatname; - if (preg_match('/\w+\[(\d+)]$/', $search, $matches)) { - // array vars need to be specifically handled - if ($matches[1] !== '0') { + if (preg_match('/\w+(?:\[(\d*)])+$/', $search, $matches)) { + $name = substr($search, 0, -(strlen($matches[1]) + 2)); + if (isset($parsedArrayVars[$name])) { continue; } - $name = substr($search, 0, -3); + $parsedArrayVars[$name] = true; $search = $name . '[*]'; } diff --git a/test/php/library/Icingadb/Model/CustomvarFlatTest.php b/test/php/library/Icingadb/Model/CustomvarFlatTest.php new file mode 100644 index 000000000..b75733c18 --- /dev/null +++ b/test/php/library/Icingadb/Model/CustomvarFlatTest.php @@ -0,0 +1,121 @@ + [ + "not_empty" => [ + "foo" => "bar" + ], + "empty" => [] + ], + "list" => [ + ["foo", "bar"], + [] + ], + "empty_list" => [], + "empty_dict" => [], + "null" => "null" + ]; + + const SPECIAL_CHAR_TEST_SOURCE = [ + [ + "vhosts.xxxxxxxxxxxxx.mgmt.xxxxxx.com.http_port", + "443", + "vhosts", + "{\"xxxxxxxxxxxxx.mgmt.xxxxxx.com\":{\"http_port\":\"443\"}}" + ], + ["ex.ample.com.bla","blub","ex","{\"ample.com\":{\"bla\":\"blub\"}}"], + ["example[1]","zyx","example[1]","\"zyx\""], + ["example.0.org","xyz","example.0.org","\"xyz\""], + ["ob.je.ct","***","ob","{\"je\":{\"ct\":\"tcejbo\"}}"], + ["real_list[2]","three","real_list","[\"one\",\"two\",\"three\"]"], + ["real_list[1]","two","real_list","[\"one\",\"two\",\"three\"]"], + ["real_list[0]","one","real_list","[\"one\",\"two\",\"three\"]"], + ["[1].2.[3].4.[5].6","123456","[1].2","{\"[3].4\":{\"[5].6\":123456}}"], + ["ex.ample.com","cba","ex.ample.com","\"cba\""], + ["[4]","four","[4]","\"four\""] + ]; + + const SPECIAL_CHAR_TEST_RESULT = [ + "vhosts" => [ + "xxxxxxxxxxxxx.mgmt.xxxxxx.com" => [ + "http_port" => 443 + ] + ], + "ex" => [ + "ample.com" => [ + "bla" => "blub" + ] + ], + "example[1]" => "zyx", + "example.0.org" => "xyz", + "ob" => [ + "je" => [ + "ct" => "tcejbo" + ] + ], + "real_list" => [ + "one", + "two", + "three" + ], + "[1].2" => [ + "[3].4" => [ + "[5].6" => "123456" + ] + ], + "ex.ample.com" => "cba", + "[4]" => "four" + ]; + + public function testUnflatteningOfEmptyCustomVariables() + { + $this->assertEquals( + self::EMPTY_TEST_RESULT, + (new CustomvarFlat())->unFlattenVars($this->transformSource(self::EMPTY_TEST_SOURCE)), + "Empty custom variables are not correctly unflattened" + ); + } + + public function testUnflatteningOfCustomVariablesWithSpecialCharacters() + { + $this->assertEquals( + self::SPECIAL_CHAR_TEST_RESULT, + (new CustomvarFlat())->unFlattenVars($this->transformSource(self::SPECIAL_CHAR_TEST_SOURCE)), + "Custom variables with special characters are not correctly unflattened" + ); + } + + protected function transformSource(array $source): \Generator + { + foreach ($source as $data) { + yield (object) [ + 'flatname' => $data[0], + 'flatvalue' => $data[1], + 'customvar' => (object) [ + 'name' => $data[2], + 'value' => $data[3] + ] + ]; + } + } +}