Update GLPI-Knowledge
release-tag / release-image (push) Successful in 1m35s

This commit is contained in:
2026-07-28 14:35:03 +02:00
parent 8ac1f97174
commit 4cfdac042d
18 changed files with 992 additions and 29 deletions
+168 -1
View File
@@ -10,6 +10,7 @@ import (
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
@@ -247,11 +248,177 @@ func (c *Client) GetCategories(ctx context.Context) ([]model.Category, error) {
}
out := make([]model.Category, 0, len(arr))
for _, r := range arr {
out = append(out, model.Category{ID: int64Val(r["id"]), Name: strVal(r["name"]), CompleteName: strVal(r["completename"])})
out = append(out, model.Category{ID: int64Val(r["id"]), Name: strVal(r["name"]), CompleteName: strVal(r["completename"]), KnowbaseCategoryID: firstRefID(r, "knowbase_category", "knowbasecategory")})
}
return out, nil
}
// DiscoverKnowledgeBasePath finds the read-only KnowbaseItem collection route
// from the OpenAPI document of the installed GLPI instance. This avoids
// hard-coding a path that may move between high-level API versions.
func (c *Client) DiscoverKnowledgeBasePath(ctx context.Context, configured string) (string, error) {
configured = strings.TrimSpace(configured)
if configured != "" && !strings.EqualFold(configured, "auto") {
if err := c.ValidateReadRoutes(ctx, []string{configured}); err != nil {
return "", err
}
return configured, nil
}
doc, err := c.FetchOpenAPI(ctx)
if err != nil {
return "", fmt.Errorf("fetch GLPI OpenAPI: %w", err)
}
paths, ok := doc["paths"].(map[string]any)
if !ok {
return "", errors.New("GLPI OpenAPI document has no paths map")
}
type candidate struct {
path string
score int
}
var candidates []candidate
for path, raw := range paths {
if strings.Contains(path, "{") {
continue
}
ops, ok := raw.(map[string]any)
if !ok || ops["get"] == nil {
continue
}
l := strings.ToLower(path)
score := 0
if strings.Contains(l, "knowbaseitem") {
score += 100
}
if strings.Contains(l, "knowledge") {
score += 40
}
if strings.Contains(l, "knowbase") {
score += 40
}
if strings.HasSuffix(l, "/knowbaseitem") {
score += 30
}
if score > 0 {
candidates = append(candidates, candidate{path: path, score: score})
}
}
if len(candidates) == 0 {
return "", errors.New("GLPI OpenAPI exposes no readable KnowbaseItem collection route; verify GLPI version and service-account knowledge-base rights")
}
sort.Slice(candidates, func(i, j int) bool {
if candidates[i].score == candidates[j].score {
return candidates[i].path < candidates[j].path
}
return candidates[i].score > candidates[j].score
})
path := candidates[0].path
// The OpenAPI document may include /api.php/vX.Y in documented paths while
// c.do already prepends the configured API base. Keep only the route suffix.
if i := strings.Index(path, "/api.php/"); i >= 0 {
rest := path[i+len("/api.php/"):]
if slash := strings.Index(rest, "/"); slash >= 0 {
path = rest[slash:]
}
}
versionPrefix := "/" + strings.Trim(c.version, "/")
if strings.HasPrefix(path, versionPrefix+"/") {
path = strings.TrimPrefix(path, versionPrefix)
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return path, nil
}
// ListKnowledgeBaseItems reads only items visible to the authenticated GLPI
// service account. Visibility is therefore enforced by GLPI itself; an
// optional filter can further restrict the collection on a per-instance basis.
func (c *Client) ListKnowledgeBaseItems(ctx context.Context, path string, limit int, filter string) ([]model.GLPIKnowledgeItem, error) {
q := url.Values{"limit": {strconv.Itoa(limit)}, "sort": {"date_mod"}, "order": {"DESC"}}
if strings.TrimSpace(filter) != "" {
q.Set("filter", filter)
}
b, _, err := c.do(ctx, http.MethodGet, path, q, nil)
if err != nil {
return nil, err
}
arr, err := extractArray(b)
if err != nil {
return nil, err
}
out := make([]model.GLPIKnowledgeItem, 0, len(arr))
for _, r := range arr {
id := int64Val(r["id"])
if id <= 0 {
continue
}
if firstString(r, "answer", "content", "text", "description") == "" {
if detail, _, e := c.do(ctx, http.MethodGet, strings.TrimRight(path, "/")+"/"+strconv.FormatInt(id, 10), nil, nil); e == nil {
var full map[string]any
if json.Unmarshal(detail, &full) == nil {
for k, v := range full {
r[k] = v
}
}
}
}
title := firstString(r, "name", "title", "subject")
content := firstString(r, "answer", "content", "text", "description")
if title == "" || content == "" {
continue
}
out = append(out, model.GLPIKnowledgeItem{
ID: id,
Title: title,
Content: content,
CategoryIDs: knowledgeCategoryIDs(r),
Language: firstString(r, "language", "locale"),
ModifiedAt: firstString(r, "date_mod", "modified_at", "date_creation"),
})
}
return out, nil
}
func knowledgeCategoryIDs(r map[string]any) []int64 {
seen := map[int64]struct{}{}
var out []int64
var add func(any)
add = func(v any) {
switch x := v.(type) {
case []any:
for _, e := range x {
add(e)
}
case map[string]any:
if id := int64Val(x["id"]); id > 0 {
if _, ok := seen[id]; !ok {
seen[id] = struct{}{}
out = append(out, id)
}
return
}
for _, v2 := range x {
add(v2)
}
default:
if id := int64Val(x); id > 0 {
if _, ok := seen[id]; !ok {
seen[id] = struct{}{}
out = append(out, id)
}
}
}
}
for _, k := range []string{"knowbase_category", "knowbase_categories", "knowbaseitemcategory", "knowbaseitemcategories", "categories", "category"} {
if v, ok := r[k]; ok {
add(v)
}
}
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out
}
func extractArray(b []byte) ([]map[string]any, error) {
var arr []map[string]any
if json.Unmarshal(b, &arr) == nil {
+36
View File
@@ -53,3 +53,39 @@ func TestValidateContractAcceptsVersionPrefixedOpenAPIPaths(t *testing.T) {
t.Fatal(err)
}
}
func TestDiscoverAndListKnowledgeBase(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api.php/token":
_ = json.NewEncoder(w).Encode(map[string]any{"access_token": "x", "expires_in": 3600})
case "/api.php/doc.json":
_ = json.NewEncoder(w).Encode(map[string]any{"paths": map[string]any{
"/v2.3/Knowledge/KnowbaseItem": map[string]any{"get": map[string]any{}},
}})
case "/api.php/v2.3/Knowledge/KnowbaseItem":
_ = json.NewEncoder(w).Encode([]map[string]any{{
"id": 7, "name": "Konto gesperrt", "answer": "<p>Konto entsperren</p>", "date_mod": "2026-07-27 10:00:00",
"categories": []any{map[string]any{"id": 4}},
}})
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
c := New(srv.URL, "v2.3", "cid", "sec", "u", "p", time.Second)
path, err := c.DiscoverKnowledgeBasePath(context.Background(), "auto")
if err != nil {
t.Fatal(err)
}
if path != "/Knowledge/KnowbaseItem" {
t.Fatalf("path=%q", path)
}
items, err := c.ListKnowledgeBaseItems(context.Background(), path, 50, "")
if err != nil {
t.Fatal(err)
}
if len(items) != 1 || items[0].ID != 7 || len(items[0].CategoryIDs) != 1 || items[0].CategoryIDs[0] != 4 {
t.Fatalf("unexpected items: %+v", items)
}
}