- bump 2.0.0 (final) + CHANGELOG datado - docs/TEMPLATES.md (guia do usuário) + seção no README - docs/API.md: seção do motor de templates (componentes, precedência, descoberta) Retrocompatível: sem template ativo, comportamento idêntico ao 1.x. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 KiB
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) ehook.php(orquestração).
Índice
- Visão geral
- Autenticação e cabeçalhos
- Gatilhos no GLPI
- Resolução do projeto Redmine
- Endpoints do Redmine usados
- Mapeamento de campos GLPI → Redmine
- Impersonation (autoria)
- Idempotência e tabelas de estado
- Tratamento de erros
- 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) |
| GET | /custom_fields.json |
definições de campos personalizados (filtra customized_type=project) |
Escrita
| Método | Endpoint | Uso |
|---|---|---|
| POST | /projects.json |
criar projeto Redmine (aba Projeto/Entidade) — inclui custom_field_values |
| 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) |
Criação de projeto (POST /projects.json — abas Redmine de Projeto/Entidade)
Campo Redmine (project.*) |
Origem |
|---|---|
name, description, is_public |
formulário da aba |
identifier |
formulário, normalizado para minúsculas (regra do Redmine) |
parent_id |
dropdown "Subprojeto de" |
enabled_module_names |
multiselect "Módulos" |
custom_field_values |
{ "<cf_id>": "<valor>", ... } — campos personalizados de projeto |
Os campos personalizados são renderizados dinamicamente a partir de
metadata_cache.project_custom_fields (populado pelo "Sincronizar Metadados" via
GET /custom_fields.json). Em instâncias onde eles são obrigatórios (ex.:
produção Mindtek: "Tipo de Projeto" e "Tipo de Faturamento"), a criação falha com
422 se não forem enviados.
Conversões importantes
- Tempo: GLPI grava
actiontimeem 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. - Identificador de projeto: minúsculas/números/traços — normalizado em
createProjectFull().
Regras que variam por instância/projeto do Redmine (validar em produção)
- Custom fields de projeto podem ser obrigatórios na criação (ver acima).
- Atividade do time entry é por projeto: a
default_activity_idprecisa estar habilitada no projeto de destino (senão 422 "não está incluso na lista"). - Comentário do time entry pode ser obrigatório: task sem descrição usa o fallback "Tempo lançado via GLPI (chamado #N)".
- Tracker é por projeto: default global vazio = Redmine usa o default do projeto.
- Issues só podem ser criadas em projetos ativos (o dropdown de vínculo já filtra).
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):
- Override manual (
glpi_plugin_redmine_usermap), se houver. - Auto-match por e-mail (e-mail do usuário GLPI =
maildo 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 Feito→A 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()retornafalse; criação de issue/tempo é abortada com log. - Todo erro HTTP
>= 400é logado viaToolbox::logInFile('redmine', ...)(emfiles/_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)
- Técnico marca a TicketTask como Feito no GLPI.
- Hook resolve o projeto Redmine pela entidade do chamado.
- (1ª vez)
POST /issues.jsoncria a issue — autor = técnico atribuído (impersonation + auto-membership). POST /time_entries.jsonlança as horas — autor = quem lançou a task.- Ao mudar o status do chamado,
PUT /issues/{id}.jsonreflete no Redmine.
Motor de Templates de Payload (2.0)
A partir da 2.0, o corpo de cada operação pode vir de um template editável em
vez de ser montado no código. Guia do usuário: TEMPLATES.md.
Componentes internos
PluginRedmineTemplateengine(inc/templateengine.class.php): armazenamento (glpi_plugin_redmine_templates),defaultTemplate()/mergeDiscoveredCustomFields()(seeds),variableCatalog()efieldDictionary()(paleta "DE" e dicionário "PARA"),buildContext()/buildFormContext()/sampleContext()(contextos), e o renderizadorrender()(tags mustache-like, tipo nativo p/ tag inteira, filtros,prunede nulos).PluginRedmineRedmineapi::createIssueFromPayload()/logTimeFromPayload(): enviam um payload já renderizado (com fallback de impersonation → admin).PluginRedmineRedmineapi::rawRequest(): requisição crua (código+corpo) usada pelo testador — o erro é a informação.ajax/template_test.php:action=preview(render comsampleContext) eaction=dryrun(cria+apaga objeto de teste, traduz 422 em dicas).
Precedência (motor 2.0 × 1.x)
Em cada operação do hook/controllers:
- Existe template ativo para a operação? Renderiza e envia esse payload.
- Falha no render (JSON/tag inválidos)? Loga e cai no comportamento 1.x.
- Sem template ativo? Comportamento 1.x (idêntico às versões anteriores).
Descoberta de campos
GET /custom_fields.json (admin) é cacheado por syncMetadata() — os campos
personalizados de project, issue e time_entry alimentam tanto o seed do
"Gerar template" (só os obrigatórios) quanto o dicionário "PARA" (todos).