Redmine/docs/API.md
Gemini ecf175728f fix: compat GLPI 11.0.4 — troca Toolbox::logError por logInFile (1.5.2)
Toolbox::logError não existe no 11.0.4 -> erro fatal ao criar issue/lançar
tempo/garantir membership. Usa Toolbox::logInFile('redmine', ...).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:58:52 -03:00

9.3 KiB
Raw Blame History

Documentação de API — Plugin Redmine Integration

Documento de desenvolvedor. Descreve o que sai do GLPI e como entra no Redmine: gatilhos, chamadas à API REST do Redmine, payloads, cabeçalhos e o mapeamento de campos. Base de código: inc/redmineapi.class.php (cliente HTTP) e hook.php (orquestração).

Índice

  1. Visão geral
  2. Autenticação e cabeçalhos
  3. Gatilhos no GLPI
  4. Resolução do projeto Redmine
  5. Endpoints do Redmine usados
  6. Mapeamento de campos GLPI → Redmine
  7. Impersonation (autoria)
  8. Idempotência e tabelas de estado
  9. Tratamento de erros
  10. Exemplos completos

1. Visão geral

O plugin é um cliente da API REST do Redmine. Ele reage a eventos do GLPI (hooks) e traduz esses eventos em chamadas HTTP ao Redmine.

                 hooks GLPI                       HTTP (REST/JSON)
GLPI (chamados) ───────────► Plugin (PHP/cURL) ──────────────────► Redmine
                             inc/redmineapi.class.php
  • Sentido único (GLPI → Redmine) no fluxo automático. O GLPI é a fonte; o Redmine recebe issues e lançamentos de tempo.
  • A única exceção de leitura é a Sincronização de Metadados (Redmine → cache local).

2. Autenticação e cabeçalhos

Todas as chamadas usam a chave de API de um administrador do Redmine (armazenada cifrada com GLPIKey em glpi_plugin_redmine_configs.api_key).

Cabeçalhos padrão:

Cabeçalho Valor Quando
X-Redmine-API-Key <api_key do admin> sempre
Content-Type application/json POST/PUT
X-Redmine-Switch-User <login do usuário Redmine> impersonation (ver §7)
  • Timeout: 5s por requisição (evita 504).
  • Sucesso = HTTP 2xx. O corpo é JSON.

3. Gatilhos no GLPI

Evento GLPI (hook) Função Ação no Redmine
item_add / item_update em TicketTask plugin_redmine_tickettask_* cria a issue (1ª vez) + lança o tempo
item_update em Ticket plugin_redmine_ticket_update atualiza o status da issue vinculada
Aba Redmine em Projeto/Entidade (form) front/project.form.php, front/entity.form.php cria/vincula projeto, define categoria, sincroniza metadados

Gate do lançamento de tempo: o processamento da TicketTask só ocorre quando task.state == Planning::DONE (Feito) e actiontime > 0. Tarefas não concluídas permanecem apenas no GLPI.


4. Resolução do projeto Redmine

Antes de criar a issue, o plugin descobre em qual projeto Redmine ela entra, a partir do chamado:

Ticket
 ├─ vinculado a um Projeto GLPI? ──► glpi_plugin_redmine_projects ─► redmine_project_id
 │      (projeto via glpi_projecttasks_tickets ou glpi_itils_projects)
 └─ senão ──► entities_id do ticket ─► glpi_plugin_redmine_entities ─► redmine_project_id
                (match EXATO da entidade, sem herança de árvore)

Sem vínculo (nem projeto nem entidade) → nada é enviado (log e retorno).


5. Endpoints do Redmine usados

Leitura (Sincronizar Metadados)

Método Endpoint Uso
GET /projects.json?limit=100 lista de projetos (cache + dropdowns)
GET /trackers.json tipos de tarefa
GET /issue_statuses.json status (alvo do mapa de status)
GET /enumerations/issue_priorities.json prioridades
GET /enumerations/time_entry_activities.json atividades de tempo
GET /roles.json papéis (para auto-membership)
GET /users.json?limit=100&offset=N usuários (cache p/ mapeamento; paginado)
GET /projects/{id}/issue_categories.json categorias (por projeto)

Escrita

Método Endpoint Uso
POST /projects.json criar projeto Redmine (aba Projeto/Entidade)
POST /issues.json criar a issue a partir do chamado
PUT /issues/{id}.json sincronizar status da issue
POST /time_entries.json lançar tempo da TicketTask
POST /projects/{id}/memberships.json adicionar técnico como membro (auto-membership)

6. Mapeamento de campos GLPI → Redmine

Issue (POST /issues.json)

Campo Redmine (issue.*) Origem no GLPI
project_id projeto Redmine resolvido (§4)
subject Ticket.name (truncado a 255, HTML removido)
description Ticket.content (HTML removido)
tracker_id config.default_tracker_id
priority_id config.default_priority_id
status_id config.status_map[Ticket.status]
category_id default_category_id do vínculo (projeto/entidade)
watcher_user_ids config.default_watcher_ids (array)
autor técnico atribuído ao chamado → X-Redmine-Switch-User (§7)

Lançamento de tempo (POST /time_entries.json)

Campo Redmine (time_entry.*) Origem no GLPI
issue_id issue da issue do chamado
hours TicketTask.actiontime / 3600 (segundos → horas decimais)
comments TicketTask.content (HTML removido)
activity_id config.default_activity_id
autor quem lançou a task (TicketTask.users_id) → X-Redmine-Switch-User

Status (PUT /issues/{id}.json)

Campo Origem
status_id config.status_map[Ticket.status] (no item_update do Ticket)

Conversões importantes

  • Tempo: GLPI grava actiontime em segundos (int); Redmine espera horas decimais. 5400 → 1.5.
  • Subject: limite de 255 chars no Redmine → truncado.
  • HTML: descrições do GLPI (TinyMCE) passam por RichText::getTextFromHtml.

7. Impersonation (autoria)

Por padrão, tudo criado via API nasce como o admin (dono da chave). Para atribuir o autor correto, o plugin usa o cabeçalho X-Redmine-Switch-User: <login>.

Pré-requisito: o usuário impersonado precisa ser membro do projeto Redmine com um papel — senão o Redmine retorna 403. Por isso, antes de impersonar, o plugin chama ensureProjectMembership() (adiciona o técnico com config.default_role_id).

Mapeamento GLPI → usuário Redmine (resolveRedmineUser):

  1. Override manual (glpi_plugin_redmine_usermap), se houver.
  2. Auto-match por e-mail (e-mail do usuário GLPI = mail do usuário Redmine, do cache).

Sem correspondência → cai no admin (fallback, §9).


8. Idempotência e tabelas de estado

Tabela Garante
glpi_plugin_redmine_tickets (tickets_id único) 1 issue por chamado (reuso)
glpi_plugin_redmine_tasks (tickettasks_id único) tempo lançado 1× por task (sem duplicar)
glpi_plugin_redmine_projects / _entities vínculo GLPI ↔ projeto Redmine
glpi_plugin_redmine_users / _usermap cache de usuários + overrides de mapeamento

Editar horas de uma task já lançada não re-sincroniza. Reverter FeitoA fazer não apaga o time entry.


9. Tratamento de erros

  • Impersonation falha (403/412 — usuário sem par no Redmine ou não-membro): o plugin refaz a chamada como admin (não perde a issue/tempo) e registra aviso em files/_log/redmine*.
  • API inacessível / chave inválida: syncMetadata() retorna false; criação de issue/tempo é abortada com log.
  • Todo erro HTTP >= 400 é logado via Toolbox::logInFile('redmine', ...) (em files/_log/redmine*).

10. Exemplos completos

Criar issue (com impersonation)

POST /issues.json HTTP/1.1
X-Redmine-API-Key: <admin_key>
X-Redmine-Switch-User: joao.silva
Content-Type: application/json

{
  "issue": {
    "project_id": 3,
    "subject": "Erro no servidor de e-mail",
    "description": "Cliente relata falha no envio.",
    "tracker_id": 1,
    "priority_id": 2,
    "status_id": 1,
    "category_id": 7,
    "watcher_user_ids": [5]
  }
}

Resposta 201: { "issue": { "id": 42, "author": { "id": 9, "name": "João Silva" }, ... } }

Lançar tempo (com impersonation)

POST /time_entries.json HTTP/1.1
X-Redmine-API-Key: <admin_key>
X-Redmine-Switch-User: joao.silva
Content-Type: application/json

{
  "time_entry": {
    "issue_id": 42,
    "hours": 1.5,
    "activity_id": 1,
    "comments": "Reinício do serviço e validação."
  }
}

Sincronizar status da issue

PUT /issues/42.json
X-Redmine-API-Key: <admin_key>
Content-Type: application/json

{ "issue": { "status_id": 5 } }

Auto-membership (antes de impersonar)

POST /projects/3/memberships.json
X-Redmine-API-Key: <admin_key>
Content-Type: application/json

{ "membership": { "user_id": 9, "role_ids": [3] } }

Resumo do fluxo end-to-end (suporte recorrente / entidade)

  1. Técnico marca a TicketTask como Feito no GLPI.
  2. Hook resolve o projeto Redmine pela entidade do chamado.
  3. (1ª vez) POST /issues.json cria a issue — autor = técnico atribuído (impersonation + auto-membership).
  4. POST /time_entries.json lança as horas — autor = quem lançou a task.
  5. Ao mudar o status do chamado, PUT /issues/{id}.json reflete no Redmine.