Initial commit — knowledge base migrada do dev local
This commit is contained in:
commit
2b10435643
26 changed files with 2864 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.DS_Store
|
||||
*.swp
|
||||
*.tmp
|
||||
*.bak
|
||||
53
README.md
Normal file
53
README.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Base de Conhecimento para Agentes de IA
|
||||
|
||||
Esta base foi projetada para leitura eficiente por agentes e humanos.
|
||||
|
||||
## Arquitetura recomendada
|
||||
- `index.json`: catalogo central para busca semantica rapida por agente.
|
||||
- `records/`: um registro por arquivo Markdown, com metadados em front matter YAML.
|
||||
- `schemas/`: contrato JSON Schema para validar metadados e manter consistencia.
|
||||
|
||||
## Por que esse formato e eficiente
|
||||
- **JSON para roteamento**: agentes localizam o registro certo sem ler toda a base.
|
||||
- **Markdown para contexto**: explica causa, impacto e procedimento com baixa ambiguidade.
|
||||
- **Metadados padronizados**: permite filtro por `tags`, `severity`, `domain` e `status`.
|
||||
- **Versionamento simples**: cada incidente vira um arquivo independente e auditavel no Git.
|
||||
|
||||
## Convencao de registros
|
||||
- ID: `KB-<DOMINIO>-NNN` (ex.: `KB-INFRA-001`)
|
||||
- Nome de arquivo: `<ID>-<slug>.md`
|
||||
- Campos obrigatorios de metadados:
|
||||
- `id`
|
||||
- `title`
|
||||
- `domain`
|
||||
- `tags`
|
||||
- `status`
|
||||
- `severity`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `applies_to`
|
||||
|
||||
## Fluxo de atualizacao
|
||||
1. Criar novo registro em `records/<dominio>/`.
|
||||
2. Atualizar `index.json` com resumo e caminho.
|
||||
3. Validar aderencia ao schema em `schemas/`.
|
||||
4. Referenciar o registro em documentacao operacional quando necessario.
|
||||
|
||||
## Validacao automatica
|
||||
Use:
|
||||
|
||||
```bash
|
||||
make kb-check
|
||||
make kb-fix
|
||||
```
|
||||
|
||||
A verificacao valida:
|
||||
- metadados obrigatorios conforme schema;
|
||||
- padrao de ID de registro;
|
||||
- sincronizacao entre `index.json` e arquivos em `records/`.
|
||||
|
||||
`make kb-fix` faz ajustes automaticos no `index.json`:
|
||||
- inclui registros faltantes;
|
||||
- remove entradas orfas;
|
||||
- sincroniza campos principais;
|
||||
- atualiza `last_updated`.
|
||||
389
index.json
Normal file
389
index.json
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"last_updated": "2026-05-21",
|
||||
"records": [
|
||||
{
|
||||
"id": "KB-INFRA-001",
|
||||
"title": "Caminho correto de plugins no container GLPI oficial",
|
||||
"domain": "infrastructure",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"docker",
|
||||
"plugins",
|
||||
"mount",
|
||||
"orbstack"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "high",
|
||||
"path": "records/infrastructure/KB-INFRA-001-glpi-plugin-path.md",
|
||||
"summary": "id: KB-INFRA-001"
|
||||
},
|
||||
{
|
||||
"id": "KB-INFRA-002",
|
||||
"title": "Icone de plugin GLPI 11 requer logo.png PNG na raiz do diretorio",
|
||||
"domain": "infrastructure",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"plugin",
|
||||
"icon",
|
||||
"logo",
|
||||
"marketplace"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "low",
|
||||
"path": "records/infrastructure/KB-INFRA-002-plugin-icon-logo-png.md",
|
||||
"summary": "id: KB-INFRA-002"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-001",
|
||||
"title": "Instalacao de plugin GLPI requer callbacks install/uninstall",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"plugin",
|
||||
"install",
|
||||
"setup.php",
|
||||
"hooks"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "high",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-001-install-hooks-required.md",
|
||||
"summary": "id: KB-PLUGIN-001"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-002",
|
||||
"title": "getCategory() deve ser sobrescrito em QuestionType de plugin GLPI 11",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"form-builder",
|
||||
"question-type",
|
||||
"javascript",
|
||||
"plugin"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "critical",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-002-questiontype-getcategory-override.md",
|
||||
"summary": "id: KB-PLUGIN-002"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-003",
|
||||
"title": "JSON em <script type=\"application/json\"> nao deve usar htmlspecialchars",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"form-builder",
|
||||
"javascript",
|
||||
"json",
|
||||
"xss",
|
||||
"php"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "critical",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-003-json-script-tag-no-htmlspecialchars.md",
|
||||
"summary": "id: KB-PLUGIN-003"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-004",
|
||||
"title": "Chave do hook ADD_JAVASCRIPT deve ser o plugin key exato",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"hooks",
|
||||
"javascript",
|
||||
"setup.php",
|
||||
"plugin-key"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "high",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-004-plugin-hook-key-must-match-plugin-key.md",
|
||||
"summary": "id: KB-PLUGIN-004"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-005",
|
||||
"title": "ITILCategoryFieldStrategy LAST_VALID_ANSWER ignora subclasses de QuestionTypeItemDropdown",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"form-builder",
|
||||
"form-destination",
|
||||
"itil-category",
|
||||
"answer-pipeline",
|
||||
"question-type"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "critical",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-005-last-valid-answer-exact-class-match.md",
|
||||
"summary": "id: KB-PLUGIN-005"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-006",
|
||||
"title": "Procedimento completo de renomeacao de plugin GLPI",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"plugin",
|
||||
"rename",
|
||||
"refactor",
|
||||
"namespace",
|
||||
"database"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "medium",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-006-plugin-rename-full-procedure.md",
|
||||
"summary": "id: KB-PLUGIN-006"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-007",
|
||||
"title": "GLPI 11 marca QuestionTypeItemDropdown como final",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"form-builder",
|
||||
"question-type",
|
||||
"fatal-error"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "critical",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-007-questiontypeitemdropdown-final.md",
|
||||
"summary": "id: KB-PLUGIN-007"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-008",
|
||||
"title": "Interfaces de Validacao Customizada no Form Builder do GLPI 11",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"form-builder",
|
||||
"validation",
|
||||
"javascript"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "medium",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-008-questiontype-validation-interfaces.md",
|
||||
"summary": "id: KB-PLUGIN-008"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-009",
|
||||
"title": "Validacao mandatory ignora respostas que sao arrays nao-vazios com chaves em branco",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"form-builder",
|
||||
"validation",
|
||||
"answers-handler"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "high",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-009-transformconditionvalue-empty-array.md",
|
||||
"summary": "id: KB-PLUGIN-009"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-010",
|
||||
"title": "Padrao correto para chamadas AJAX autenticadas em plugins GLPI 11",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"ajax",
|
||||
"csrf",
|
||||
"security",
|
||||
"symfony",
|
||||
"fetch",
|
||||
"plugin"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "critical",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-010-ajax-csrf-x-glpi-csrf-token.md",
|
||||
"summary": "id: KB-PLUGIN-010"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-011",
|
||||
"title": "\"Procedimento de Release do Plugin Mindplace\"",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"github",
|
||||
"release",
|
||||
"secrets"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "medium",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-011-mindplace-release-zip-procedure.md",
|
||||
"summary": "id: KB-PLUGIN-011"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-012",
|
||||
"title": "Estendendo o sistema de Tiles do Helpdesk no GLPI 11 — padrões e armadilhas",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"helpdesk",
|
||||
"tiles",
|
||||
"dom-manipulation",
|
||||
"mutation-observer",
|
||||
"twig",
|
||||
"singleton",
|
||||
"plugin"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "high",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-012-helpdesk-tiles-extension-patterns.md",
|
||||
"summary": "id: KB-PLUGIN-012"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-013",
|
||||
"title": "Runbook — Publicação de plugin no Mindplace (procedimento padrão)",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"mindplace",
|
||||
"runbook",
|
||||
"publish",
|
||||
"license",
|
||||
"forgejo",
|
||||
"release",
|
||||
"kill-switch",
|
||||
"workflow",
|
||||
"automation",
|
||||
"script",
|
||||
"bash"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "high",
|
||||
"updated_at": "2026-05-21",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-013-mindplace-plugin-publication-runbook.md",
|
||||
"summary": "Runbook completo de publicação. As Fases 2-5 (Forgejo + ZIP + Release + plugins.json) são automatizadas pelo script bin/mindplace-release.sh. Uso: ./bin/mindplace-release.sh ./docker/glpi/plugins/<plugin>. Requer PAT do Forgejo com escopo write:repository."
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-014",
|
||||
"title": "getFromDBByCrit() não limpa $fields em falha — reutilizar a mesma instância vaza estado entre iterações",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"commondbtm",
|
||||
"getfromdbbycrit",
|
||||
"bug",
|
||||
"iteration",
|
||||
"state-leak"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "critical",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-014-getfromdbbycrit-stale-fields.md",
|
||||
"summary": "id: KB-PLUGIN-014"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-015",
|
||||
"title": "querySelector('.row') colide com elementos de outros plugins no Helpdesk portal",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"helpdesk",
|
||||
"tilesections",
|
||||
"dom",
|
||||
"selectors",
|
||||
"bootstrap",
|
||||
"cross-plugin-interference"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "high",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-015-querySelector-row-collision.md",
|
||||
"summary": "id: KB-PLUGIN-015"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-016",
|
||||
"title": "\"Bypass de CSRF e Autenticação Nativa (Bearer Token) para APIs Customizadas no GLPI 11\"",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi-11",
|
||||
"csrf",
|
||||
"api",
|
||||
"jwt",
|
||||
"oauth",
|
||||
"auth",
|
||||
"stateless"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "high",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-016-glpi11-csrf-bypass-and-api-auth.md",
|
||||
"summary": "id: KB-PLUGIN-016"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-017",
|
||||
"title": "Bug do detector de wrapper directory no instalador Mindplace mutilava nomes de arquivos",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"mindplace",
|
||||
"marketplace",
|
||||
"zip",
|
||||
"extractor",
|
||||
"install-bug"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "high",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-017-mindplace-zip-wrapper-detector-bug.md",
|
||||
"summary": "id: KB-PLUGIN-017"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-018",
|
||||
"title": "Convenção de wrapper directory para ZIPs de release de plugin GLPI distribuídos via Mindplace",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"mindplace",
|
||||
"release",
|
||||
"zip",
|
||||
"github-actions",
|
||||
"workflow"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "medium",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-018-plugin-release-zip-wrapper-convention.md",
|
||||
"summary": "id: KB-PLUGIN-018"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-019",
|
||||
"title": "Arquitetura BFF (Backend For Frontend) e Seguranca via API no GLPI 11 (Plugin MCProtocol)",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"mcp",
|
||||
"api",
|
||||
"rbac",
|
||||
"security",
|
||||
"bff"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "high",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-019-mcprotocol-bff-architecture.md",
|
||||
"summary": "id: KB-PLUGIN-019"
|
||||
},
|
||||
{
|
||||
"id": "KB-PLUGIN-020",
|
||||
"title": "Roadmap de Evolucao do MCProtocol (Views Semanticas API)",
|
||||
"domain": "plugin-dev",
|
||||
"tags": [
|
||||
"glpi",
|
||||
"glpi11",
|
||||
"mcp",
|
||||
"api",
|
||||
"roadmap",
|
||||
"bff"
|
||||
],
|
||||
"status": "active",
|
||||
"severity": "medium",
|
||||
"path": "records/plugin-dev/KB-PLUGIN-020-mcprotocol-bff-roadmap.md",
|
||||
"summary": "id: KB-PLUGIN-020"
|
||||
}
|
||||
]
|
||||
}
|
||||
66
records/infrastructure/KB-INFRA-001-glpi-plugin-path.md
Normal file
66
records/infrastructure/KB-INFRA-001-glpi-plugin-path.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
id: KB-INFRA-001
|
||||
title: Caminho correto de plugins no container GLPI oficial
|
||||
domain: infrastructure
|
||||
tags:
|
||||
- glpi
|
||||
- docker
|
||||
- plugins
|
||||
- mount
|
||||
- orbstack
|
||||
status: active
|
||||
severity: high
|
||||
created_at: 2026-05-01
|
||||
updated_at: 2026-05-01
|
||||
applies_to:
|
||||
- glpi/glpi:latest
|
||||
- macOS + OrbStack
|
||||
- ambiente local de desenvolvimento
|
||||
related_records: []
|
||||
---
|
||||
|
||||
# Contexto
|
||||
O plugin `GLPI Matrix` nao era exibido na interface do GLPI, mesmo existindo no host e no container.
|
||||
|
||||
# Sintoma observavel
|
||||
- Plugin presente em `docker/glpi/plugins/glpimatrix`.
|
||||
- Interface de plugins do GLPI nao listava o plugin.
|
||||
|
||||
# Causa raiz
|
||||
O bind mount estava apontando para caminho incorreto do container:
|
||||
- **Incorreto**: `/var/www/html/glpi/plugins`
|
||||
- **Correto (nesta imagem)**: `/var/www/glpi/plugins`
|
||||
|
||||
A imagem `glpi/glpi:latest` usada neste projeto executa o core em `/var/www/glpi`.
|
||||
|
||||
# Correcao aplicada
|
||||
No `docker-compose.yml`, atualizar volumes do servico `glpi`:
|
||||
- `./docker/glpi/plugins:/var/www/glpi/plugins`
|
||||
- `./docker/glpi/files:/var/glpi/files`
|
||||
- `./docker/glpi/config:/var/glpi/config`
|
||||
- `./docker/glpi/marketplace:/var/glpi/marketplace`
|
||||
|
||||
# Validacao padrao
|
||||
Executar:
|
||||
1. `docker compose up -d --force-recreate glpi`
|
||||
2. `docker exec glpi11-app ls -la /var/www/glpi/plugins`
|
||||
3. `curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8081`
|
||||
|
||||
Esperado:
|
||||
- diretorio do plugin visivel em `/var/www/glpi/plugins`;
|
||||
- HTTP `200`;
|
||||
- plugin listavel em `Configuracao > Plugins` apos refresh.
|
||||
|
||||
# Impacto
|
||||
- Sem essa correcao, agentes podem interpretar incorretamente que o plugin esta invalido.
|
||||
- O problema e de infraestrutura/container path, nao de logica do plugin.
|
||||
|
||||
# Regra para agentes
|
||||
Antes de diagnosticar erro funcional de plugin, validar:
|
||||
1. caminho de mount do plugin;
|
||||
2. estrutura interna da imagem em uso;
|
||||
3. cache e estado do container.
|
||||
|
||||
# Classificacao
|
||||
- Tipo: Licao aprendida de infraestrutura.
|
||||
- Reutilizacao: obrigatoria em novos setups locais GLPI.
|
||||
89
records/infrastructure/KB-INFRA-002-plugin-icon-logo-png.md
Normal file
89
records/infrastructure/KB-INFRA-002-plugin-icon-logo-png.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
---
|
||||
id: KB-INFRA-002
|
||||
title: Icone de plugin GLPI 11 requer logo.png PNG na raiz do diretorio
|
||||
domain: infrastructure
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- plugin
|
||||
- icon
|
||||
- logo
|
||||
- marketplace
|
||||
status: active
|
||||
severity: low
|
||||
created_at: 2026-05-02
|
||||
updated_at: 2026-05-02
|
||||
applies_to:
|
||||
- GLPI 11.x lista de plugins e marketplace
|
||||
- qualquer plugin local instalado manualmente
|
||||
related_records:
|
||||
- KB-INFRA-001
|
||||
- KB-PLUGIN-006
|
||||
---
|
||||
|
||||
# Contexto
|
||||
Ao instalar um plugin localmente, o GLPI exibe um avatar gerado com as iniciais do nome
|
||||
do plugin no lugar de um ícone real. Isso ocorre quando nenhum arquivo de logo está presente.
|
||||
|
||||
# Sintoma observável
|
||||
- Lista de plugins exibe um quadrado colorido com 1-2 letras (ex.: "SI" para "Split ITIL").
|
||||
- Mesmo com `<logo>` definido no XML do plugin, o ícone remoto pode não carregar.
|
||||
|
||||
# Como o GLPI resolve o ícone (ordem de prioridade)
|
||||
|
||||
1. **Arquivo local `logo.png`** na raiz do diretório do plugin → servido via `LogoController`
|
||||
2. URL remota definida em `<logo>` no arquivo `{plugin_key}.xml`
|
||||
3. **Fallback**: avatar gerado com iniciais das palavras do nome do plugin
|
||||
|
||||
Código relevante (`Marketplace/View.php`):
|
||||
```php
|
||||
if (Document::isImage(sprintf('%s/logo.png', Plugin::getPhpDir($key)))) {
|
||||
$logo_url = sprintf('%s/Plugin/%s/Logo', $CFG_GLPI['root_doc'], $key);
|
||||
}
|
||||
```
|
||||
|
||||
# Especificação do arquivo
|
||||
|
||||
| Atributo | Valor |
|
||||
|-------------|------------------------------------------------|
|
||||
| Nome | `logo.png` (fixo — hardcoded no LogoController)|
|
||||
| Localização | raiz do diretório do plugin |
|
||||
| Formato | **PNG** obrigatório |
|
||||
| SVG | Não funciona — `Document::isImage()` rejeita |
|
||||
| Tamanho rec. | 128×128 px ou 256×256 px (quadrado) |
|
||||
|
||||
# Rota do LogoController
|
||||
|
||||
```
|
||||
GET /Plugin/{plugin_key}/Logo
|
||||
```
|
||||
- Requer autenticação admin (`STRATEGY_ADMIN_ACCESS`)
|
||||
- Serve `BinaryFileResponse` do arquivo `logo.png`
|
||||
- Se não existir: retorna 1×1 PNG transparente com HTTP 404
|
||||
|
||||
# Verificar se o GLPI aceita o PNG
|
||||
|
||||
```bash
|
||||
docker exec glpi-app php -r "
|
||||
require '/var/www/glpi/vendor/autoload.php';
|
||||
echo Document::isImage('/var/www/glpi/plugins/myplugin/logo.png') ? 'VALID' : 'INVALID';
|
||||
"
|
||||
```
|
||||
|
||||
# Gerar PNG placeholder sem dependências externas
|
||||
|
||||
Com Python stdlib (sem Pillow ou ImageMagick):
|
||||
```python
|
||||
import struct, zlib
|
||||
# Gerar array de pixels RGBA, encodar com struct.pack e zlib.compress
|
||||
# Ver: knowledge-base/samples/generate_plugin_logo.py (a criar)
|
||||
```
|
||||
|
||||
# Regra para agentes
|
||||
Para qualquer plugin, sempre criar `plugins/{key}/logo.png` (PNG, 128×128).
|
||||
O arquivo tem prioridade sobre o `<logo>` do XML e elimina o fallback de iniciais.
|
||||
Não usar SVG — o GLPI não reconhece SVG como imagem válida neste contexto.
|
||||
|
||||
# Classificação
|
||||
- Tipo: Configuração de apresentação do plugin.
|
||||
- Reutilização: aplicável a qualquer plugin GLPI 11 com presença na lista de plugins.
|
||||
57
records/plugin-dev/KB-PLUGIN-001-install-hooks-required.md
Normal file
57
records/plugin-dev/KB-PLUGIN-001-install-hooks-required.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
id: KB-PLUGIN-001
|
||||
title: Instalacao de plugin GLPI requer callbacks install/uninstall
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- plugin
|
||||
- install
|
||||
- setup.php
|
||||
- hooks
|
||||
status: active
|
||||
severity: high
|
||||
created_at: 2026-05-01
|
||||
updated_at: 2026-05-01
|
||||
applies_to:
|
||||
- plugins locais GLPI 11.x
|
||||
- estrutura setup.php
|
||||
related_records:
|
||||
- KB-INFRA-001
|
||||
---
|
||||
|
||||
# Contexto
|
||||
O plugin `GLPI Matrix` aparecia na lista de plugins, mas falhava ao instalar pela interface.
|
||||
|
||||
# Sintoma observavel
|
||||
- Notificacao generica de falha na instalacao.
|
||||
- Plugin permanecia em status "Nao instalado".
|
||||
|
||||
# Causa raiz
|
||||
Ausencia dos callbacks de ciclo de vida no `setup.php`:
|
||||
- `plugin_<chave>_install(): bool`
|
||||
- `plugin_<chave>_uninstall(): bool`
|
||||
|
||||
Sem esses callbacks, o GLPI nao consegue concluir o fluxo de instalacao/desinstalacao.
|
||||
|
||||
# Correcao aplicada
|
||||
Adicionar no `setup.php`:
|
||||
- `plugin_glpimatrix_install(): bool { return true; }`
|
||||
- `plugin_glpimatrix_uninstall(): bool { return true; }`
|
||||
|
||||
# Validacao padrao
|
||||
Executar:
|
||||
1. `php /var/www/glpi/bin/console plugin:install glpimatrix -n`
|
||||
2. `php /var/www/glpi/bin/console plugin:activate glpimatrix -n`
|
||||
3. `php /var/www/glpi/bin/console plugin:list`
|
||||
|
||||
Esperado:
|
||||
- instalacao concluida;
|
||||
- ativacao concluida;
|
||||
- status final "Habilitado".
|
||||
|
||||
# Regra para agentes
|
||||
Ao criar plugin base, sempre incluir no `setup.php`:
|
||||
1. `plugin_<chave>_check_prerequisites`
|
||||
2. `plugin_<chave>_check_config`
|
||||
3. `plugin_<chave>_install`
|
||||
4. `plugin_<chave>_uninstall`
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
id: KB-PLUGIN-002
|
||||
title: getCategory() deve ser sobrescrito em QuestionType de plugin GLPI 11
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- form-builder
|
||||
- question-type
|
||||
- javascript
|
||||
- plugin
|
||||
status: active
|
||||
severity: critical
|
||||
created_at: 2026-05-02
|
||||
updated_at: 2026-05-02
|
||||
applies_to:
|
||||
- GLPI 11.x form builder nativo
|
||||
- plugins que estendem QuestionTypeItemDropdown ou qualquer QuestionType nativo
|
||||
related_records:
|
||||
- KB-PLUGIN-003
|
||||
- KB-PLUGIN-004
|
||||
- KB-PLUGIN-005
|
||||
---
|
||||
|
||||
# Contexto
|
||||
Ao criar um QuestionType de plugin que estende uma classe nativa (ex.: `QuestionTypeItemDropdown`),
|
||||
o método `getCategory()` herdado retorna a categoria nativa (ex.: `QuestionTypeCategory::ITEM`).
|
||||
O plugin registra sua própria categoria via `QuestionTypesManager::registerPluginCategory()`.
|
||||
|
||||
# Sintoma observável
|
||||
- Questão adicionada ao formulário dispara erros JS:
|
||||
- `Error fetching supported value operators` (400)
|
||||
- `TypeError: can't access property 'convertDefaultValue', this[#options][type] is undefined`
|
||||
- Painel de configuração da questão não renderiza nenhum input.
|
||||
|
||||
# Causa raiz
|
||||
`QuestionTypesManager::getCategoryKey()` gera uma chave por categoria.
|
||||
O JS do editor (`EditorController.js`) usa `#changeQuestionTypeCategory()` para listar os tipos
|
||||
disponíveis na categoria selecionada pelo usuário. Se `getCategory()` retorna a categoria nativa
|
||||
em vez da do plugin, o mapa `this.#options` não contém o tipo do plugin para aquela categoria,
|
||||
e qualquer tentativa de acessar `this.#options[type]` resulta em `undefined`.
|
||||
|
||||
# Correção aplicada
|
||||
Sobrescrever `getCategory()` na classe do plugin para retornar a instância da categoria registrada:
|
||||
|
||||
```php
|
||||
#[Override]
|
||||
public function getCategory(): QuestionTypeCategoryInterface
|
||||
{
|
||||
return new QuestionTypeCategorySplititil();
|
||||
}
|
||||
```
|
||||
|
||||
# Validação padrão
|
||||
1. Abrir o form builder do GLPI 11.
|
||||
2. Criar um formulário e adicionar uma questão do tipo do plugin.
|
||||
3. Verificar no console do browser: zero erros JS.
|
||||
4. Verificar que o painel lateral exibe os inputs de configuração corretos.
|
||||
|
||||
# Regra para agentes
|
||||
**Todo QuestionType de plugin DEVE sobrescrever `getCategory()`** para retornar a instância
|
||||
da sua própria categoria, mesmo que estenda um tipo nativo.
|
||||
Nunca confiar no `getCategory()` herdado quando a categoria do plugin é diferente da categoria pai.
|
||||
|
||||
# Classificação
|
||||
- Tipo: Armadilha arquitetural crítica do GLPI 11 form builder.
|
||||
- Reutilização: obrigatória em qualquer plugin que registre QuestionType no GLPI 11.
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
id: KB-PLUGIN-003
|
||||
title: JSON em <script type="application/json"> nao deve usar htmlspecialchars
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- form-builder
|
||||
- javascript
|
||||
- json
|
||||
- xss
|
||||
- php
|
||||
status: active
|
||||
severity: critical
|
||||
created_at: 2026-05-02
|
||||
updated_at: 2026-05-02
|
||||
applies_to:
|
||||
- GLPI 11.x renderEndUserTemplate
|
||||
- qualquer contexto PHP que embuta JSON em tag <script>
|
||||
related_records:
|
||||
- KB-PLUGIN-002
|
||||
---
|
||||
|
||||
# Contexto
|
||||
Ao embutir dados PHP como JSON dentro de um elemento `<script type="application/json">`,
|
||||
é comum aplicar `htmlspecialchars()` por hábito de segurança HTML. Isso é incorreto neste contexto.
|
||||
|
||||
# Sintoma observável
|
||||
- Widget cascading não renderiza — nenhum `<select>` é exibido ao usuário.
|
||||
- Console do browser exibe: `SyntaxError: JSON.parse: unexpected character`.
|
||||
- O JSON inspecionado no DOM contém `"` no lugar de `"`, `<` no lugar de `<`.
|
||||
|
||||
# Causa raiz
|
||||
O browser NÃO decodifica entidades HTML dentro de `<script type="application/json">`.
|
||||
O conteúdo é tratado como texto puro (não como HTML). Aplicar `htmlspecialchars()` sobre o JSON
|
||||
transforma `"chave": "valor"` em `"chave": "valor"`, que é JSON inválido.
|
||||
|
||||
```php
|
||||
// ERRADO — corrompe as aspas do JSON
|
||||
$json = htmlspecialchars(json_encode($data, JSON_UNESCAPED_UNICODE), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// CORRETO — JSON_HEX_TAG escapa < e > para evitar </script> acidental
|
||||
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP);
|
||||
```
|
||||
|
||||
# Por que JSON_HEX_TAG e JSON_HEX_AMP
|
||||
- `JSON_HEX_TAG`: converte `<` → `<` e `>` → `>`, impedindo que `</script>` apareça
|
||||
literalmente e quebre o bloco script. Isso SIM é necessário para segurança.
|
||||
- `JSON_HEX_AMP`: converte `&` → `&`, evita problemas em contextos XML/XHTML.
|
||||
- `htmlspecialchars()` não deve ser usado — ele gera entidades HTML, não sequências JSON.
|
||||
|
||||
# Validação padrão
|
||||
1. Inspecionar o DOM e copiar o conteúdo do `<script type="application/json">`.
|
||||
2. Colar no console: `JSON.parse('<conteudo>')` — deve retornar o objeto sem erro.
|
||||
3. Verificar que nenhum `"`, `<`, `>` ou `&` aparece no JSON.
|
||||
|
||||
# Regra para agentes
|
||||
Ao embutir JSON em `<script type="application/json">`:
|
||||
- Usar `json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP)`.
|
||||
- **Nunca** passar o resultado de `json_encode()` por `htmlspecialchars()`.
|
||||
- `htmlspecialchars()` é correto para atributos HTML e conteúdo de texto. Não para JSON em script.
|
||||
|
||||
# Classificação
|
||||
- Tipo: Bug de segurança/encoding com impacto funcional crítico.
|
||||
- Reutilização: obrigatória em qualquer renderização PHP de dados JSON para JS.
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
id: KB-PLUGIN-004
|
||||
title: Chave do hook ADD_JAVASCRIPT deve ser o plugin key exato
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- hooks
|
||||
- javascript
|
||||
- setup.php
|
||||
- plugin-key
|
||||
status: active
|
||||
severity: high
|
||||
created_at: 2026-05-02
|
||||
updated_at: 2026-05-02
|
||||
applies_to:
|
||||
- GLPI 11.x
|
||||
- qualquer plugin que injete JS via PLUGIN_HOOKS
|
||||
related_records:
|
||||
- KB-PLUGIN-001
|
||||
- KB-PLUGIN-002
|
||||
---
|
||||
|
||||
# Contexto
|
||||
Ao registrar arquivos JS em `setup.php` via `$PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]`,
|
||||
a chave do array deve ser exatamente o plugin key — o mesmo nome do diretório do plugin.
|
||||
|
||||
# Sintoma observável
|
||||
- O arquivo JS do plugin nunca é incluído nas páginas do GLPI.
|
||||
- Nenhum erro é exibido — a falha é silenciosa.
|
||||
- Funcionalidades que dependem do JS simplesmente não funcionam.
|
||||
|
||||
# Causa raiz
|
||||
`getPluginsJsScriptsFiles()` itera sobre `$PLUGIN_HOOKS['add_javascript']` e chama
|
||||
`Plugin::isPluginActive($plugin)` usando a **chave** do array como nome do plugin.
|
||||
Se a chave for diferente do plugin key real, `isPluginActive()` retorna `false`
|
||||
e o arquivo JS é ignorado sem qualquer aviso.
|
||||
|
||||
```php
|
||||
// ERRADO — chave diferente do plugin key
|
||||
$PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['splititil_form'] = ['js/splititil-form.js'];
|
||||
|
||||
// CORRETO — chave = plugin key exato
|
||||
$PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['splititil'] = ['js/splititil-form.js'];
|
||||
```
|
||||
|
||||
# Para formulários públicos (anônimos)
|
||||
Usar `ADD_JAVASCRIPT_ANONYMOUS_PAGE` além de `ADD_JAVASCRIPT` para que o JS
|
||||
seja carregado em páginas de formulário acessadas sem login:
|
||||
|
||||
```php
|
||||
$PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['splititil'] = ['js/splititil-form.js'];
|
||||
$PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT_ANONYMOUS_PAGE]['splititil'] = ['js/splititil-form.js'];
|
||||
```
|
||||
|
||||
# Validação padrão
|
||||
1. Verificar no HTML renderizado se o `<script src="...splititil-form.js">` está presente.
|
||||
2. Ou via `grep`: `curl -s http://localhost/glpi/ | grep "splititil-form"`.
|
||||
|
||||
# Regra para agentes
|
||||
Ao registrar hooks de JS/CSS em `setup.php`, a chave do array **sempre** deve ser
|
||||
o plugin key exato (= nome do diretório do plugin). Nunca usar sufixos, prefixos ou variações.
|
||||
Para formulários públicos, registrar também em `ADD_JAVASCRIPT_ANONYMOUS_PAGE`.
|
||||
|
||||
# Classificação
|
||||
- Tipo: Armadilha de configuração — falha silenciosa.
|
||||
- Reutilização: obrigatória em qualquer plugin que injete assets no GLPI 11.
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
id: KB-PLUGIN-005
|
||||
title: ITILCategoryFieldStrategy LAST_VALID_ANSWER ignora subclasses de QuestionTypeItemDropdown
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- form-builder
|
||||
- form-destination
|
||||
- itil-category
|
||||
- answer-pipeline
|
||||
- question-type
|
||||
status: active
|
||||
severity: critical
|
||||
created_at: 2026-05-02
|
||||
updated_at: 2026-05-02
|
||||
applies_to:
|
||||
- GLPI 11.x form builder — destino de formulário (FormDestinationTicket/Change/Problem)
|
||||
- plugins com QuestionType que estendem QuestionTypeItemDropdown para ITILCategory
|
||||
related_records:
|
||||
- KB-PLUGIN-002
|
||||
- KB-PLUGIN-006
|
||||
---
|
||||
|
||||
# Contexto
|
||||
Ao criar um QuestionType de plugin que retorna uma ITILCategory como resposta,
|
||||
o GLPI não aplica automaticamente essa categoria ao ticket criado pelo formulário.
|
||||
|
||||
# Sintoma observável
|
||||
- O usuário seleciona uma categoria no formulário.
|
||||
- O ticket é criado sem `itilcategories_id` (campo vazio/zero).
|
||||
- Nenhum erro é exibido — a categoria simplesmente é ignorada.
|
||||
|
||||
# Causa raiz
|
||||
`ITILCategoryFieldStrategy::LAST_VALID_ANSWER` filtra respostas por tipo assim:
|
||||
|
||||
```php
|
||||
$valid_answers = $answers_set->getAnswersByType(QuestionTypeItemDropdown::class);
|
||||
```
|
||||
|
||||
`AnswersSet::getAnswersByType()` usa **comparação exata** (`==`), não `is_a()`:
|
||||
|
||||
```php
|
||||
fn(Answer $answer) => $answer->getRawType() == $type
|
||||
```
|
||||
|
||||
O `raw_question_type` de um `QuestionTypeSplititil` é armazenado como
|
||||
`'GlpiPlugin\Splititil\QuestionTypeSplititil'`, que não é igual a
|
||||
`'Glpi\Form\QuestionType\QuestionTypeItemDropdown'`.
|
||||
Resultado: a resposta é invisível para a estratégia padrão.
|
||||
|
||||
# Solução — AbstractConfigField silencioso
|
||||
|
||||
Registrar um campo de destino de plugin que roda **após** `ITILCategoryField` e
|
||||
sobrescreve `itilcategories_id` quando encontra uma resposta da nossa classe:
|
||||
|
||||
```php
|
||||
// Em setup.php (dentro de plugin_init_*)
|
||||
FormDestinationManager::getInstance()->registerPluginCommonITILConfigField(
|
||||
AbstractCommonITILFormDestination::class, // aplica a Ticket, Change e Problem
|
||||
new \GlpiPlugin\Splititil\SplititilITILCategoryApplicator()
|
||||
);
|
||||
```
|
||||
|
||||
O `SplititilITILCategoryApplicator` estende `AbstractConfigField` com:
|
||||
- `renderConfigForm()` retorna `''` → invisível na UI de configuração
|
||||
- `applyConfiguratedValueToInputUsingAnswers()` varre as respostas buscando pelo
|
||||
tipo exato da nossa classe e seta `itilcategories_id` no input do ticket
|
||||
|
||||
# Por que esse campo roda após ITILCategoryField
|
||||
Em `createDestinationItems()`, os campos `Entity`, `Template` e `ITILCategoryField`
|
||||
são aplicados antecipadamente e adicionados a `$already_applied_fields`.
|
||||
Os campos de plugin são aplicados no `foreach` subsequente e **não** são filtrados
|
||||
(porque têm uma classe diferente de `ITILCategoryField`), sobrescrevendo o valor anterior.
|
||||
|
||||
# Mínimo para implementar AbstractConfigField de plugin
|
||||
Além dos defaults de `AbstractConfigField`, implementar obrigatoriamente:
|
||||
`getLabel()`, `renderConfigForm()`, `getConfigClass()`, `getDefaultConfig()`,
|
||||
`getWeight()`, `getCategory()`
|
||||
+ uma classe config mínima implementando `JsonFieldInterface`
|
||||
(`jsonDeserialize()` + `jsonSerialize()`).
|
||||
|
||||
# Validação padrão
|
||||
1. Submeter formulário com questão de categoria preenchida.
|
||||
2. Abrir o ticket criado e verificar que `itilcategories_id` está preenchido.
|
||||
3. Confirmar no banco: `SELECT itilcategories_id FROM glpi_tickets ORDER BY id DESC LIMIT 1;`
|
||||
|
||||
# Regra para agentes
|
||||
Qualquer QuestionType de plugin que retorne ITILCategory DEVE implementar um
|
||||
`AbstractConfigField` silencioso registrado via `registerPluginCommonITILConfigField()`.
|
||||
Não existe forma nativa de fazer o `LAST_VALID_ANSWER` reconhecer subclasses
|
||||
sem modificar o core do GLPI.
|
||||
|
||||
# Classificação
|
||||
- Tipo: Limitação arquitetural do GLPI 11 + solução via ponto de extensão oficial.
|
||||
- Reutilização: obrigatória em qualquer plugin de categoria ITIL no form builder.
|
||||
107
records/plugin-dev/KB-PLUGIN-006-plugin-rename-full-procedure.md
Normal file
107
records/plugin-dev/KB-PLUGIN-006-plugin-rename-full-procedure.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
---
|
||||
id: KB-PLUGIN-006
|
||||
title: Procedimento completo de renomeacao de plugin GLPI
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- plugin
|
||||
- rename
|
||||
- refactor
|
||||
- namespace
|
||||
- database
|
||||
status: active
|
||||
severity: medium
|
||||
created_at: 2026-05-02
|
||||
updated_at: 2026-05-02
|
||||
applies_to:
|
||||
- GLPI 11.x
|
||||
- qualquer plugin sendo renomeado ou criado como fork
|
||||
related_records:
|
||||
- KB-PLUGIN-001
|
||||
- KB-INFRA-001
|
||||
---
|
||||
|
||||
# Contexto
|
||||
Renomear um plugin GLPI envolve muito mais do que trocar o nome do diretório.
|
||||
O plugin key permeia namespace PHP, hooks, tabelas de banco, URLs de ajax, classes JS/CSS
|
||||
e o registro na tabela `glpi_plugins`. Uma renomeação incompleta causa falhas silenciosas.
|
||||
|
||||
# Mapa completo de substituições (do mais específico para o mais geral)
|
||||
|
||||
Execute na seguinte ordem para evitar substituições duplas:
|
||||
|
||||
```
|
||||
GlpiPlugin\OldName → GlpiPlugin\NewName (namespace)
|
||||
PluginOldName → PluginNewName (prefixo de classe legado)
|
||||
PLUGIN_OLDNAME → PLUGIN_NEWNAME (constantes)
|
||||
plugin_oldname → plugin_newname (funções e hook keys)
|
||||
glpi_plugin_oldname → glpi_plugin_newname (tabelas de banco)
|
||||
QuestionTypeOldName → QuestionTypeNewName (classes src/)
|
||||
data-oldname- → data-newname- (atributos JS)
|
||||
oldname-group → newname-group (classes CSS)
|
||||
oldname_container → newname_container (classes CSS)
|
||||
@oldname/ → @newname/ (namespace Twig)
|
||||
"oldname" → "newname" (string literal entre aspas)
|
||||
OldName → NewName (PascalCase restante)
|
||||
oldname → newname (lowercase restante)
|
||||
"Old Display Name" → "New Display Name" (nome de exibição)
|
||||
"Old Author" → "New Author" (autor)
|
||||
```
|
||||
|
||||
Comando com perl (mais confiável que sed no macOS):
|
||||
```bash
|
||||
find ./plugins/oldname -type f ! -path "*/vendor/*" ! -name "*.png" \
|
||||
| xargs perl -i -pe 's/GlpiPlugin\\\\OldName/GlpiPlugin\\\\NewName/g; ...'
|
||||
```
|
||||
|
||||
# Arquivos a renomear (após substituir conteúdo)
|
||||
|
||||
```bash
|
||||
mv ajax/oldname.php ajax/newname.php
|
||||
mv ajax/oldname_data.php ajax/newname_data.php
|
||||
mv public/css/oldname.css public/css/newname.css
|
||||
mv public/js/oldname-form.js public/js/newname-form.js
|
||||
mv public/js/oldname.js.php public/js/newname.js.php
|
||||
mv oldname.xml newname.xml
|
||||
mv src/QuestionTypeOldName.php src/QuestionTypeNewName.php
|
||||
# ... demais arquivos src/
|
||||
mv plugins/oldname plugins/newname # por último
|
||||
```
|
||||
|
||||
# Pós-renomeação: atualizar o GLPI
|
||||
|
||||
```bash
|
||||
# Remover registro órfão do nome antigo (diretório não existe mais)
|
||||
mysql -u glpi -p glpi -e "DELETE FROM glpi_plugins WHERE directory='oldname';"
|
||||
|
||||
# Instalar e ativar com o novo key
|
||||
docker exec glpi-app php /var/www/glpi/bin/console plugin:install newname
|
||||
docker exec glpi-app php /var/www/glpi/bin/console plugin:activate newname
|
||||
docker exec glpi-app php /var/www/glpi/bin/console plugin:list
|
||||
```
|
||||
|
||||
# Verificação final
|
||||
```bash
|
||||
# Zero referências ao nome antigo (exceto menções históricas intencionais)
|
||||
grep -rn "oldname\|OldName\|OLDNAME" ./plugins/newname \
|
||||
--exclude-dir=vendor --exclude="*.png" | grep -v "vendor"
|
||||
|
||||
# Sintaxe PHP OK
|
||||
find ./plugins/newname -name "*.php" ! -path "*/vendor/*" \
|
||||
| xargs php -l | grep -v "No syntax errors"
|
||||
```
|
||||
|
||||
# Regra para agentes
|
||||
Ao renomear um plugin:
|
||||
1. Substituir conteúdo (ordem do específico para o geral)
|
||||
2. Renomear arquivos individuais
|
||||
3. Renomear o diretório
|
||||
4. Limpar registro órfão no banco
|
||||
5. Reinstalar via console
|
||||
|
||||
Nunca renomear o diretório antes de substituir o conteúdo — o container pode perder a referência.
|
||||
|
||||
# Classificação
|
||||
- Tipo: Procedimento operacional de refatoração.
|
||||
- Reutilização: aplicável a qualquer renomeação ou fork de plugin GLPI.
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
id: KB-PLUGIN-007
|
||||
title: GLPI 11 marca QuestionTypeItemDropdown como final
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- form-builder
|
||||
- question-type
|
||||
- fatal-error
|
||||
status: active
|
||||
severity: critical
|
||||
created_at: 2026-05-02
|
||||
updated_at: 2026-05-02
|
||||
applies_to:
|
||||
- GLPI 11.0.x
|
||||
---
|
||||
|
||||
# GLPI 11 marca QuestionTypeItemDropdown como final
|
||||
|
||||
## Sintoma
|
||||
|
||||
Ao instalar ou utilizar um plugin que estenda `QuestionTypeItemDropdown` em ambiente de produção do GLPI 11, ocorre o seguinte erro fatal que pode derrubar o sistema (Erro 500):
|
||||
|
||||
`Fatal Compile Error: Class GlpiPlugin\NomeDoPlugin\SuaClasse cannot extend final class Glpi\Form\QuestionType\QuestionTypeItemDropdown`
|
||||
|
||||
## Causa
|
||||
|
||||
Em versões recentes e estáveis do GLPI 11, a classe `Glpi\Form\QuestionType\QuestionTypeItemDropdown` (assim como outras nativas) recebeu a declaração `final`. O PHP impede que classes marcadas como finais sejam estendidas, gerando um erro de compilação assim que o arquivo é carregado pelo autoloader.
|
||||
|
||||
Isso geralmente ocorre em plugins que tentam reaproveitar a lógica do dropdown de itens padrão do GLPI apenas sobrescrevendo a renderização.
|
||||
|
||||
## Resolucao / Workaround
|
||||
|
||||
Não tente estender a classe final. Em vez disso:
|
||||
|
||||
1. **Estenda a classe base**
|
||||
Mude a herança da sua classe para estender a classe não-final imediatamente superior na hierarquia. Para o dropdown, use a classe base `QuestionTypeItem`.
|
||||
|
||||
```php
|
||||
// ANTES (Causa erro)
|
||||
class MinhaQuestao extends QuestionTypeItemDropdown
|
||||
|
||||
// DEPOIS (Funciona corretamente)
|
||||
use Glpi\Form\QuestionType\QuestionTypeItem;
|
||||
class MinhaQuestao extends QuestionTypeItem
|
||||
```
|
||||
|
||||
2. **Composição Manual (Duck Typing)**
|
||||
Re-implemente os métodos que você precisava herdar do `QuestionTypeItemDropdown`. Muitas vezes é necessário copiar configurações do `getExtraDataConfigClass`, repassar parâmetros de filtragem ou copiar a estrutura do template Twig nativo.
|
||||
|
||||
## Exemplo de Impacto
|
||||
|
||||
No plugin de categorização em cascata, a herança direta impedia a ativação em produção. Mudar para `QuestionTypeItem` exigiu também reconstruir a lógica que carrega e filtra as categorias respeitando configuração de interface (`is_helpdeskvisible`), tipos habilitados e profundidade da árvore.
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
id: KB-PLUGIN-008
|
||||
title: Interfaces de Validacao Customizada no Form Builder do GLPI 11
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- form-builder
|
||||
- validation
|
||||
- javascript
|
||||
status: active
|
||||
severity: medium
|
||||
created_at: 2026-05-02
|
||||
updated_at: 2026-05-02
|
||||
applies_to:
|
||||
- GLPI 11.0.x
|
||||
---
|
||||
|
||||
# Interfaces de Validacao Customizada no Form Builder do GLPI 11
|
||||
|
||||
## Contexto
|
||||
|
||||
Ao criar um novo tipo de questão (`QuestionType`) para o Form Builder do GLPI 11, o motor nativo utiliza validações genéricas. Por exemplo, se a questão for marcada como obrigatória e estiver vazia, o erro genérico exibido será "This field is mandatory".
|
||||
|
||||
## Como personalizar mensagens de erro
|
||||
|
||||
O GLPI fornece dois pontos de extensão na forma de interfaces que sua classe `QuestionType` pode implementar:
|
||||
|
||||
### 1. `CustomMandatoryMessageInterface`
|
||||
Usada para interceptar a falha da checagem de "campo obrigatório".
|
||||
Quando a resposta (após as devidas transformações) for considerada vazia, o GLPI chamará o método fornecido para exibir a mensagem.
|
||||
|
||||
```php
|
||||
use Glpi\Form\QuestionType\CustomMandatoryMessageInterface;
|
||||
|
||||
class MinhaQuestao extends AbstractQuestionType implements CustomMandatoryMessageInterface
|
||||
{
|
||||
public function getCustomMandatoryErrorMessage(): string
|
||||
{
|
||||
return __('Por favor, preencha todos os campos do widget.', 'meuplugin');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `QuestionTypeValidationInterface`
|
||||
Usada para validações complexas ou de domínio quando o campo **possui um valor preenchido**, mas pode ser inválido para as regras de negócio.
|
||||
|
||||
```php
|
||||
use Glpi\Form\QuestionType\QuestionTypeValidationInterface;
|
||||
use Glpi\Form\ValidationResult;
|
||||
|
||||
class MinhaQuestao extends AbstractQuestionType implements QuestionTypeValidationInterface
|
||||
{
|
||||
public function validateAnswer(Question $question, mixed $answer): ValidationResult
|
||||
{
|
||||
$result = new ValidationResult();
|
||||
|
||||
// Exemplo: campo não pode ter um valor que termine em 0
|
||||
if (is_numeric($answer) && $answer % 10 === 0) {
|
||||
$result->addError($question, __('O valor não pode terminar em zero.', 'meuplugin'));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## UI de Validação do Front-end
|
||||
Note que os erros retornados (via AJAX para `/Form/ValidateAnswers`) acionam o script `RendererController.js` do GLPI, que buscará no DOM os elementos correspondentes e aplicará as classes `is-invalid` nativas juntamente com o balão `.invalid-tooltip`. Não é recomendável forçar validações HTML5 (`setCustomValidity`) via JavaScript se estiver interagindo diretamente com o motor do Form Builder.
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
id: KB-PLUGIN-009
|
||||
title: Validacao mandatory ignora respostas que sao arrays nao-vazios com chaves em branco
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- form-builder
|
||||
- validation
|
||||
- answers-handler
|
||||
status: active
|
||||
severity: high
|
||||
created_at: 2026-05-02
|
||||
updated_at: 2026-05-02
|
||||
applies_to:
|
||||
- GLPI 11.0.x
|
||||
---
|
||||
|
||||
# Validacao mandatory ignora respostas que sao arrays nao-vazios com chaves em branco
|
||||
|
||||
## Sintoma
|
||||
Um campo obrigatório (mandatory) de um novo `QuestionType` não dispara nenhum erro de validação se o usuário o enviar vazio. Em vez disso, a página tenta enviar e falha, devolvendo um erro genérico do sistema (geralmente gerado na hora de gravar os destinos).
|
||||
|
||||
## Causa
|
||||
O mecanismo do `AnswersHandler` do GLPI verifica campos vazios da seguinte forma:
|
||||
```php
|
||||
if ($answer === null || (is_string($answer) && strip_tags($answer) === '') || (is_array($answer) && count($answer) === 0))
|
||||
```
|
||||
|
||||
Em widgets complexos que retornam uma estrutura via vários inputs (como `[items_id]` e `[itemtype]`), se o usuário não selecionar nada, a resposta "crua" (*raw*) pode chegar no backend como um array:
|
||||
```php
|
||||
$answer = [
|
||||
'itemtype' => 'ITILCategory',
|
||||
'items_id' => ''
|
||||
];
|
||||
```
|
||||
Este array possui 2 chaves, logo `count($answer) === 0` é `false`. O GLPI entende que a pergunta "foi respondida" e pula a checagem de obrigatoriedade.
|
||||
|
||||
## Solucao
|
||||
|
||||
A classe `QuestionType` deve implementar (ou sobrescrever) o método `transformConditionValueForComparisons` originado da `ConditionValueTransformerInterface`.
|
||||
|
||||
O objetivo é examinar a resposta *raw* precocemente e convertê-la ativamente em uma string vazia `''` caso os dados essenciais estejam ausentes.
|
||||
|
||||
```php
|
||||
use Override;
|
||||
|
||||
#[Override]
|
||||
public function transformConditionValueForComparisons(mixed $value, ?\Glpi\DBAL\JsonFieldInterface $question_config): string
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$items_id = $value['items_id'] ?? '';
|
||||
|
||||
// Trata chaves em branco ou nulas como resposta vazia global
|
||||
if ($items_id === '' || $items_id === null || (int)$items_id <= 0) {
|
||||
return ''; // string vazia dispara o check nativo do AnswersHandler!
|
||||
}
|
||||
}
|
||||
|
||||
// Passa adiante se tudo estiver ok
|
||||
return parent::transformConditionValueForComparisons($value, $question_config);
|
||||
}
|
||||
```
|
||||
|
||||
Ao retornar `''`, o `AnswersHandler` passará a reconhecer que o usuário pulou o campo, ativando corretamente a mensagem de erro (ou a sua `CustomMandatoryMessageInterface` caso definida).
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
---
|
||||
id: KB-PLUGIN-010
|
||||
title: Padrao correto para chamadas AJAX autenticadas em plugins GLPI 11
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- ajax
|
||||
- csrf
|
||||
- security
|
||||
- symfony
|
||||
- fetch
|
||||
- plugin
|
||||
status: active
|
||||
severity: critical
|
||||
created_at: 2026-05-03
|
||||
updated_at: 2026-05-03
|
||||
applies_to:
|
||||
- GLPI 11.0.x
|
||||
---
|
||||
|
||||
# Padrao correto para chamadas AJAX autenticadas em plugins GLPI 11
|
||||
|
||||
## Sintoma
|
||||
|
||||
Chamadas `fetch` POST feitas por um plugin retornam HTTP 403. O plugin está corretamente instalado e o usuário está autenticado com o direito necessário. O erro ocorre mesmo que `Session::checkCSRF()` não seja chamado no PHP do plugin.
|
||||
|
||||
## Causa
|
||||
|
||||
O GLPI 11 utiliza o kernel Symfony com um listener dedicado (`CheckCsrfListener`) que intercepta **todos os requests POST antes de o PHP do plugin executar**. Para requests AJAX (identificados pelo header `X-Requested-With: XMLHttpRequest`), ele exige o CSRF token no header **`X-Glpi-Csrf-Token`** — não no body da requisição.
|
||||
|
||||
Fonte: `/var/www/glpi/src/Glpi/Kernel/Listener/ControllerListener/CheckCsrfListener.php`
|
||||
|
||||
```php
|
||||
if ($request->isXmlHttpRequest()) {
|
||||
Session::checkCSRF(
|
||||
['_glpi_csrf_token' => $request->server->get('HTTP_X_GLPI_CSRF_TOKEN') ?? ''],
|
||||
preserve_token: true // token não é consumido — reutilizável em múltiplas chamadas
|
||||
);
|
||||
} else {
|
||||
Session::checkCSRF($request->request->all());
|
||||
}
|
||||
```
|
||||
|
||||
## Onde fica o token
|
||||
|
||||
O token é renderizado pelo layout do GLPI em uma meta tag:
|
||||
|
||||
```html
|
||||
<meta property="glpi:csrf_token" content="TOKEN_AQUI" />
|
||||
```
|
||||
|
||||
Com `preserve_token: true`, o mesmo token permanece válido para todas as chamadas AJAX da sessão — não é consumido.
|
||||
|
||||
## Solucao
|
||||
|
||||
### JavaScript (plugin)
|
||||
|
||||
```js
|
||||
function csrfToken() {
|
||||
var meta = document.querySelector('meta[property="glpi:csrf_token"]');
|
||||
return meta ? meta.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
fetch('/plugins/meu-plugin/ajax/endpoint.php', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin', // envia o cookie de sessão
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest', // identifica como AJAX para o kernel
|
||||
'X-Glpi-Csrf-Token': csrfToken() // token exigido pelo CheckCsrfListener
|
||||
},
|
||||
body: new URLSearchParams({ action: 'minha_action', key: 'valor' })
|
||||
});
|
||||
```
|
||||
|
||||
### PHP (endpoint do plugin)
|
||||
|
||||
**Não chamar** `Session::checkCSRF()` — o kernel já fez a verificação. Apenas verificar o direito do usuário:
|
||||
|
||||
```php
|
||||
<?php
|
||||
include('../../../inc/includes.php');
|
||||
|
||||
Session::checkRight('config', UPDATE); // suficiente — kernel já validou CSRF
|
||||
|
||||
// lógica do endpoint...
|
||||
```
|
||||
|
||||
## Comportamento esperado
|
||||
|
||||
Com o padrão acima:
|
||||
- Kernel valida CSRF via header antes do PHP rodar
|
||||
- PHP recebe a requisição autenticada normalmente
|
||||
- Token não é consumido (`preserve_token: true`), então múltiplos POSTs consecutivos funcionam sem gerar novo token
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
id: KB-PLUGIN-011
|
||||
title: "Procedimento de Release do Plugin Mindplace (LEGADO — GitHub)"
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- github
|
||||
- release
|
||||
- secrets
|
||||
- deprecated
|
||||
status: deprecated
|
||||
severity: medium
|
||||
created_at: 2024-05-10
|
||||
updated_at: 2026-05-21
|
||||
applies_to:
|
||||
- Plugin Mindplace (versão pré-Forgejo)
|
||||
supersedes: KB-PLUGIN-013
|
||||
---
|
||||
|
||||
> **⚠️ DEPRECATED** — O procedimento aqui descrito refere-se à fase em que o Mindplace usava o GitHub como backend de releases. Desde 2026-05, o backend é o **Forgejo da Mindtek** (`servicedesk.mindtek.com.br/git`) e o fluxo de release é automatizado pelo script `bin/mindplace-release.sh`.
|
||||
>
|
||||
> Para o procedimento atual, ver **[KB-PLUGIN-013](KB-PLUGIN-013-mindplace-plugin-publication-runbook.md)**.
|
||||
|
||||
# KB-PLUGIN-011 — Procedimento de Release do Plugin Mindplace (legado)
|
||||
|
||||
## Contexto
|
||||
|
||||
O plugin Mindplace usa um GitHub PAT (`secrets.php`) para autenticar chamadas à API do GitHub e aumentar o rate limit de 60 para 5.000 req/hora. Esse arquivo é **gitignored** — nunca entra no repositório público — mas precisa estar presente no ZIP distribuído aos clientes.
|
||||
|
||||
O ZIP gerado automaticamente pelo GitHub (Source code) **não inclui** arquivos gitignored. Por isso o ZIP de release deve ser criado manualmente.
|
||||
|
||||
## Arquivos sensíveis
|
||||
|
||||
| Arquivo | Git | ZIP cliente |
|
||||
|---|---|---|
|
||||
| `config/secrets.php` | gitignored ✗ | incluído ✓ |
|
||||
| `config/secrets.php.example` | commitado ✓ | excluído ✗ |
|
||||
|
||||
## Procedimento para gerar o ZIP de release
|
||||
|
||||
```bash
|
||||
cd docker/glpi/plugins/mindplace
|
||||
|
||||
zip -r mindplace-1.0.0.zip . \
|
||||
--exclude "*.git*" \
|
||||
--exclude "config/secrets.php.example"
|
||||
```
|
||||
|
||||
Substitua `1.0.0` pela versão da release.
|
||||
|
||||
## Upload no GitHub
|
||||
|
||||
1. Acesse o repositório `rodolphoolopes/mindplace` no GitHub
|
||||
2. Releases → Draft a new release
|
||||
3. Crie a tag da versão (ex: `v1.0.0`)
|
||||
4. Em **Assets**, faça upload do `mindplace-1.0.0.zip` gerado acima
|
||||
5. **Não usar** o ZIP automático "Source code" gerado pelo GitHub
|
||||
|
||||
## Estrutura do secrets.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
define('MINDPLACE_GITHUB_PAT', 'github_pat_...');
|
||||
```
|
||||
|
||||
O PAT deve ter permissões:
|
||||
- Repositório: `rodolphoolopes/mindplace`
|
||||
- Contents: Read-only
|
||||
- Metadata: Read-only (obrigatório pelo GitHub)
|
||||
|
||||
## Como o código lê o PAT
|
||||
|
||||
`PluginConfig::getGithubPat()` faz `include_once` do `secrets.php` se ele existir. Se o arquivo não existir, retorna string vazia e o plugin funciona sem autenticação (rate limit de 60 req/hora).
|
||||
|
||||
## Atenção
|
||||
|
||||
- Nunca commitar `secrets.php` — o GitHub faz varredura automática em repos públicos e **revoga o token imediatamente** se detectar
|
||||
- Ao renovar o PAT, atualizar o `secrets.php` local e regerar o ZIP de todas as versões ativas
|
||||
|
|
@ -0,0 +1,276 @@
|
|||
---
|
||||
id: KB-PLUGIN-012
|
||||
title: Estendendo o sistema de Tiles do Helpdesk no GLPI 11 — padrões e armadilhas
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- helpdesk
|
||||
- tiles
|
||||
- dom-manipulation
|
||||
- mutation-observer
|
||||
- twig
|
||||
- singleton
|
||||
- plugin
|
||||
status: active
|
||||
severity: high
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
applies_to:
|
||||
- GLPI 11.0.x
|
||||
---
|
||||
|
||||
# Estendendo o sistema de Tiles do Helpdesk no GLPI 11
|
||||
|
||||
Documento consolidado das armadilhas encontradas ao implementar o plugin `tilesections` (categorização visual de tiles na "Página Inicial do Helpdesk"). O sistema de tiles do GLPI 11 é nativamente plano (sem agrupamento) e não foi desenhado para extensão por plugins — qualquer modificação visual precisa contornar isso.
|
||||
|
||||
## 1. Estrutura DOM dos tiles no admin (Entidade > Página Inicial do Helpdesk)
|
||||
|
||||
Cada tile na grade de configuração é renderizado pela template `templates/pages/admin/helpdesk_home_config_tiles.html.twig` em **dois níveis aninhados**:
|
||||
|
||||
```html
|
||||
<section
|
||||
class="col-12 col-lg-6 col-xl-4 d-flex-soft pointer-events-none"
|
||||
data-glpi-draggable-item
|
||||
data-glpi-helpdesk-config-tile-container ← wrapper externo
|
||||
data-glpi-helpdesk-config-action-show-edit-form
|
||||
data-bs-toggle="offcanvas"
|
||||
>
|
||||
<div
|
||||
data-glpi-helpdesk-config-tile
|
||||
data-glpi-helpdesk-config-tile-id="123" ← ID está aqui
|
||||
data-glpi-helpdesk-config-tile-itemtype="..."
|
||||
class="card rounded my-2 cursor-pointer"
|
||||
>
|
||||
...
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Armadilha
|
||||
|
||||
Mover apenas o `<div>` interno (que tem o `data-glpi-helpdesk-config-tile-id`) **destrói o layout**:
|
||||
|
||||
- Perde as classes de coluna (`col-12 col-lg-6 col-xl-4`) → tile vira full-width
|
||||
- Perde os atributos de drag (`data-glpi-draggable-item`) → não é mais reordenável
|
||||
- Perde o trigger do offcanvas (`data-bs-toggle="offcanvas"`) → click pra editar não funciona
|
||||
|
||||
### Padrão correto
|
||||
|
||||
Selecionar o **outer container** com `[data-glpi-helpdesk-config-tile-container]` e ler os IDs do `<div>` interno:
|
||||
|
||||
```js
|
||||
const tileOuters = container.querySelectorAll('[data-glpi-helpdesk-config-tile-container]');
|
||||
tileOuters.forEach(outer => {
|
||||
const inner = outer.querySelector('[data-glpi-helpdesk-config-tile-id]');
|
||||
const itemtype = inner.getAttribute('data-glpi-helpdesk-config-tile-itemtype');
|
||||
const tileId = inner.getAttribute('data-glpi-helpdesk-config-tile-id');
|
||||
// mover `outer` (não `inner`)
|
||||
});
|
||||
```
|
||||
|
||||
## 2. TilesManager é singleton — `__construct` é privado
|
||||
|
||||
```php
|
||||
// ❌ ERRADO — Call to private __construct from scope ...
|
||||
$manager = new \Glpi\Helpdesk\Tile\TilesManager();
|
||||
|
||||
// ✅ CORRETO
|
||||
$manager = \Glpi\Helpdesk\Tile\TilesManager::getInstance();
|
||||
```
|
||||
|
||||
## 3. Como o GLPI obtém os tiles que serão renderizados no portal público
|
||||
|
||||
O `Glpi\Controller\Helpdesk\IndexController` (que renderiza o portal `/Helpdesk`) chama:
|
||||
|
||||
```php
|
||||
$manager->getVisibleTilesForSession(Session::getCurrentSessionInfo())
|
||||
```
|
||||
|
||||
Esse método:
|
||||
|
||||
1. Pega os tiles do **profile atual**; se vazio, sobe pelo entity tree
|
||||
2. Filtra por `$tile->isAvailable($session_info)` (permissões, configurações, etc.)
|
||||
3. Retorna `array<TileInterface&CommonDBTM>` na ordem que serão renderizados
|
||||
|
||||
**Use o mesmo método em endpoints AJAX do seu plugin** para garantir alinhamento perfeito com o que o usuário vê — inclusive quando o admin está personificando outro perfil.
|
||||
|
||||
## 4. Tiles do portal público NÃO têm `data-attribute` de identificação
|
||||
|
||||
O template `templates/pages/helpdesk/index.html.twig` renderiza apenas:
|
||||
|
||||
```html
|
||||
<a class="card mx-1 my-2 flex-grow-1" href="{{ tile.getTileUrl() }}">
|
||||
```
|
||||
|
||||
Não há `data-tile-id`, `data-itemtype`, etc. **Não dá pra fazer matching reverso por ID.**
|
||||
|
||||
### Armadilha: matching por URL é frágil
|
||||
|
||||
Usar `tile.getTileUrl()` como chave de matching falha em casos comuns:
|
||||
|
||||
- **URL vazia**: `ExternalPageTile` retorna `""` se o campo `url` estiver vazio. Múltiplos tiles com URL vazia colidem na mesma chave (`href::`) e o último processado "ganha", varrendo os outros.
|
||||
- **URLs duplicadas**: dois `ExternalPageTile` apontando pra mesma URL têm chave idêntica.
|
||||
- **Encoding**: query strings com `[]` (`criteria[0][field]=...`) podem ser escapadas diferentemente entre o servidor e o atributo HTML.
|
||||
|
||||
### Padrão correto: matching por índice de renderização
|
||||
|
||||
```php
|
||||
// servidor — endpoint AJAX
|
||||
$rendered = TilesManager::getInstance()
|
||||
->getVisibleTilesForSession(Session::getCurrentSessionInfo());
|
||||
|
||||
$tile_order = [];
|
||||
foreach ($rendered as $tile) {
|
||||
$tile_order[] = [
|
||||
'itemtype' => get_class($tile),
|
||||
'items_id' => (int) $tile->getDatabaseId(),
|
||||
'sections_id' => (int) ($mappings[get_class($tile) . '::' . $tile->getDatabaseId()] ?? 0),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// cliente — JS do portal
|
||||
const tileWrappers = tilesRow.querySelectorAll(':scope > div');
|
||||
tileWrappers.forEach((wrapper, idx) => {
|
||||
const tileInfo = tileOrder[idx]; // 1:1 garantido pela ordem
|
||||
if (!tileInfo) return;
|
||||
const sectionId = tileInfo.sections_id;
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
Funciona porque a sessão da chamada AJAX é a mesma sessão que renderizou a página — o `getVisibleTilesForSession` retorna a lista exata na ordem exata.
|
||||
|
||||
## 5. GLPI re-renderiza tiles via AJAX após delete/reorder
|
||||
|
||||
Quando o usuário deleta ou reordena um tile na tela de admin, o GLPI faz um AJAX e **substitui o conteúdo do container** `[data-glpi-helpdesk-config-tiles]` com a lista atualizada do servidor. Isso varre qualquer reorganização visual aplicada pelo plugin.
|
||||
|
||||
### Padrão correto: MutationObserver com guarda de re-entrada
|
||||
|
||||
```js
|
||||
let isReorganizing = false;
|
||||
|
||||
function watchGridRefreshes(tilesContainer) {
|
||||
new MutationObserver(() => {
|
||||
if (isReorganizing) return;
|
||||
|
||||
const hasTiles = tilesContainer.querySelectorAll('[data-glpi-helpdesk-config-tile-container]').length > 0;
|
||||
const hasSections = tilesContainer.querySelector('.minha-section-wrapper');
|
||||
if (!hasTiles || hasSections) return;
|
||||
|
||||
// Container voltou ao estado plano — re-aplicar reorganização
|
||||
queueMicrotask(async () => {
|
||||
await loadServerData();
|
||||
reorganize(tilesContainer); // setar isReorganizing dentro
|
||||
});
|
||||
}).observe(tilesContainer, { childList: true });
|
||||
}
|
||||
|
||||
function reorganize(tilesContainer) {
|
||||
isReorganizing = true;
|
||||
// ... mutations no DOM ...
|
||||
queueMicrotask(() => { isReorganizing = false; });
|
||||
}
|
||||
```
|
||||
|
||||
A guarda `isReorganizing` é essencial — sem ela, as próprias mutações do plugin disparam o observer em loop infinito.
|
||||
|
||||
## 6. Race condition no save de tile — não use polling
|
||||
|
||||
Quando o usuário salva um novo tile no offcanvas, o GLPI cria via AJAX e adiciona ao DOM **assíncronamente**. Polling com `setInterval` é frágil:
|
||||
|
||||
- "Último tile no DOM" não garante ser o recém-criado (ordenação muda)
|
||||
- Timeout duro (ex: 6s) pode estourar em conexões lentas
|
||||
|
||||
### Padrão correto: snapshot + MutationObserver com diff
|
||||
|
||||
```js
|
||||
function handleAddSave(tilesContainer, sectionId) {
|
||||
// Snapshot: tile keys antes do save
|
||||
const before = new Set();
|
||||
tilesContainer.querySelectorAll('[data-glpi-helpdesk-config-tile-id]').forEach(el => {
|
||||
const itemtype = el.getAttribute('data-glpi-helpdesk-config-tile-itemtype');
|
||||
const id = el.getAttribute('data-glpi-helpdesk-config-tile-id');
|
||||
before.add(`${itemtype}::${id}`);
|
||||
});
|
||||
|
||||
let resolved = false;
|
||||
const observer = new MutationObserver(() => {
|
||||
if (resolved) return;
|
||||
const tiles = tilesContainer.querySelectorAll('[data-glpi-helpdesk-config-tile-id]');
|
||||
for (const el of tiles) {
|
||||
const itemtype = el.getAttribute('data-glpi-helpdesk-config-tile-itemtype');
|
||||
const id = el.getAttribute('data-glpi-helpdesk-config-tile-id');
|
||||
const key = `${itemtype}::${id}`;
|
||||
if (!before.has(key)) {
|
||||
resolved = true;
|
||||
observer.disconnect();
|
||||
saveSectionMapping(itemtype, id, sectionId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(tilesContainer, { childList: true, subtree: true });
|
||||
|
||||
setTimeout(() => { if (!resolved) observer.disconnect(); }, 15000);
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Anti-flash: esconder container antes da reorganização
|
||||
|
||||
Se a reorganização visual roda dentro de um `setTimeout` (debounce), o usuário vê os tiles em estado "plano" antes de aparecerem categorizados. Solução: adicionar a classe de loading **imediatamente** ao encontrar o container, não dentro do callback do timeout:
|
||||
|
||||
```js
|
||||
function tryInit() {
|
||||
const tilesContainer = document.querySelector('[data-glpi-helpdesk-config-tiles]');
|
||||
if (!tilesContainer) return;
|
||||
|
||||
tilesContainer.classList.add('ts-loading'); // ← imediato
|
||||
|
||||
setTimeout(() => {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
init(tilesContainer); // remove 'ts-loading' no final
|
||||
}, 300);
|
||||
}
|
||||
```
|
||||
|
||||
Com CSS:
|
||||
```css
|
||||
[data-glpi-helpdesk-config-tiles].ts-loading {
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Templates de plugin têm namespace, não override direto
|
||||
|
||||
GLPI 11 carrega templates de plugin em namespace dedicado:
|
||||
|
||||
```php
|
||||
// Glpi\Application\View\TemplateRenderer
|
||||
$loader->addPath(Plugin::getPhpDir($plugin_key . '/templates'), $plugin_key);
|
||||
```
|
||||
|
||||
Isso registra `@plugin_key/path/to/template.html.twig`. **Não substitui** o template core no mesmo path.
|
||||
|
||||
Para "estender" templates core, opções são:
|
||||
|
||||
1. **Hooks**: usar `Hooks::ADD_JAVASCRIPT` / `Hooks::ADD_CSS` para injetar comportamento via JS/CSS
|
||||
2. **API server-side**: chamar APIs nativas do GLPI (`TilesManager::getInstance()`) em endpoints AJAX próprios
|
||||
3. **DOM manipulation**: aceitar a fragilidade e robustecer com observers (este KB)
|
||||
|
||||
Override direto de template core via plugin **não é suportado oficialmente**.
|
||||
|
||||
## Referências do core GLPI 11
|
||||
|
||||
| Caminho | Conteúdo |
|
||||
|---|---|
|
||||
| `src/Glpi/Helpdesk/Tile/TilesManager.php` | Singleton com `getInstance()` e `getVisibleTilesForSession()` |
|
||||
| `src/Glpi/Helpdesk/Tile/{FormTile,GlpiPageTile,ExternalPageTile}.php` | Implementações de tile |
|
||||
| `src/Glpi/Controller/Helpdesk/IndexController.php` | Renderização do portal público |
|
||||
| `templates/pages/admin/helpdesk_home_config_tiles.html.twig` | Template do admin grid |
|
||||
| `templates/pages/helpdesk/index.html.twig` | Template do portal público |
|
||||
| `src/Glpi/Application/View/TemplateRenderer.php` | Mecanismo de namespace de templates de plugin |
|
||||
|
|
@ -0,0 +1,451 @@
|
|||
---
|
||||
id: KB-PLUGIN-013
|
||||
title: Runbook — Publicação de plugin no Mindplace (procedimento padrão)
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- mindplace
|
||||
- runbook
|
||||
- publish
|
||||
- license
|
||||
- forgejo
|
||||
- release
|
||||
- kill-switch
|
||||
- workflow
|
||||
- automation
|
||||
- script
|
||||
status: active
|
||||
severity: high
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-21
|
||||
applies_to:
|
||||
- GLPI 11.0.x
|
||||
- Mindplace 1.0+
|
||||
- Forgejo (servicedesk.mindtek.com.br/git)
|
||||
---
|
||||
|
||||
# Runbook — Publicação de plugin no Mindplace
|
||||
|
||||
Procedimento padrão completo para publicar um novo plugin proprietário no Mindplace (marketplace privado da Mindtek). Cobre desde a integração do kill switch de licença no código até a entrega do serial ao cliente.
|
||||
|
||||
> **⚡ TL;DR — Publicação automatizada:** As Fases 2, 3 e 4 (Forgejo + ZIP + Release + plugins.json) estão **100% automatizadas** pelo script `bin/mindplace-release.sh`. Leia a seção **Publicação Automatizada** antes de executar os passos manuais.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
- [ ] Plugin está em estado funcional e testado localmente
|
||||
- [ ] Acesso de admin ao Forgejo (`servicedesk.mindtek.com.br/git`)
|
||||
- [ ] Acesso ao GLPI de produção da Mindtek (`servicedesk.mindtek.com.br`) com perfil que crie licenças de software
|
||||
- [ ] Plugin já tem `setup.php` válido com `plugin_version_<name>()` retornando metadata
|
||||
- [ ] `version` no `setup.php` segue SemVer (ex: `1.0.0`)
|
||||
- [ ] **Para publicação automatizada:** `FORGEJO_TOKEN` disponível (PAT com escopo `write:repository`)
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Publicação Automatizada (Fases 2–5)
|
||||
|
||||
As fases de publicação no Forgejo e catálogo são cobertas pelo script `bin/mindplace-release.sh` localizado na raiz do projeto GLPI11.
|
||||
|
||||
### Pré-requisito único: Personal Access Token (PAT)
|
||||
|
||||
Gere em: `https://servicedesk.mindtek.com.br/git/user/settings/applications`
|
||||
- Nome sugerido: `antigravity-publisher`
|
||||
- Escopo necessário: `write:repository`
|
||||
|
||||
Exporte como variável de ambiente (ou edite a variável `FORGEJO_TOKEN` diretamente no script):
|
||||
```bash
|
||||
export FORGEJO_TOKEN="seu_token_aqui"
|
||||
```
|
||||
|
||||
### Uso
|
||||
|
||||
```bash
|
||||
# Na raiz do projeto GLPI11:
|
||||
./bin/mindplace-release.sh ./docker/glpi/plugins/<nome-do-plugin>
|
||||
```
|
||||
|
||||
### O que o script faz automaticamente
|
||||
|
||||
| Fase | Ação |
|
||||
|---|---|
|
||||
| **1** | Cria repositório no Forgejo (se não existir) |
|
||||
| **2** | `git init` + commit + push para o Forgejo |
|
||||
| **3** | Gera ZIP limpo do plugin (exclui `.git`, `vendor`, `node_modules`, `*.DS_Store`) |
|
||||
| **4** | Cria tag `v{versão}` + Release no Forgejo + faz upload do ZIP como asset |
|
||||
| **5** | Atualiza `plugins.json` no repo `mindplace` com a entrada do novo plugin |
|
||||
|
||||
### Comportamento idempotente
|
||||
|
||||
O script é seguro para rodar múltiplas vezes:
|
||||
- ✅ Repositório já existe → pula criação
|
||||
- ✅ Release já existe → pula criação
|
||||
- ✅ Plugin já no `plugins.json` → pula atualização
|
||||
- ✅ Sempre faz push do código mais recente
|
||||
|
||||
### Fluxo de nova versão
|
||||
|
||||
```bash
|
||||
# 1. Bump da versão no setup.php
|
||||
# 'version' => '1.0.0' → '1.1.0'
|
||||
|
||||
# 2. Rodar o script — ele detecta a nova versão e cria nova Release
|
||||
./bin/mindplace-release.sh ./docker/glpi/plugins/meu-plugin
|
||||
```
|
||||
|
||||
### Notas importantes
|
||||
|
||||
- O script lê a versão diretamente da constante `PLUGIN_XXXVERSION` no `setup.php`
|
||||
- Arquivos de teste (`ajax/test*.php`, `front/test*.php`) devem estar no `.gitignore` do plugin para não serem publicados
|
||||
- O `.gitignore` padrão para plugins está documentado abaixo
|
||||
- O ZIP do asset pode acumular múltiplas versões na mesma Release se o script for rodado mais de uma vez sem bump de versão (não causa erro, apenas duplicata)
|
||||
|
||||
### .gitignore padrão para plugins
|
||||
|
||||
```gitignore
|
||||
# Dependências
|
||||
vendor/
|
||||
node_modules/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Build artifacts
|
||||
*.zip
|
||||
|
||||
# Arquivos de teste (não publicar)
|
||||
ajax/test*.php
|
||||
ajax/mcp_test.php
|
||||
front/test*.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Fase 1 — Kill switch de licença no código
|
||||
|
||||
Cada plugin do Mindplace **precisa** validar a licença antes de fazer qualquer coisa. O padrão do "Kill Switch" funciona em duas camadas:
|
||||
|
||||
1. `plugin_<name>_check_config()` impede ativação se licença inválida
|
||||
2. `plugin_init_<name>()` aborta silenciosamente se licença inválida (proteção em profundidade)
|
||||
|
||||
### Template do `setup.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Glpi\Plugin\Hooks;
|
||||
|
||||
function plugin_version_<name>(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Plugin Name',
|
||||
'version' => '1.0.0',
|
||||
'author' => 'Mindtek Tecnologia',
|
||||
'license' => 'GPLv2+',
|
||||
'requirements' => ['glpi' => ['min' => '11.0', 'max' => '12.0']],
|
||||
];
|
||||
}
|
||||
|
||||
function plugin_<name>_check_prerequisites(): bool
|
||||
{
|
||||
if (version_compare(GLPI_VERSION, '11.0', '<')) {
|
||||
echo 'This plugin requires GLPI >= 11.0';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrega a classe License do Mindplace caso o autoload ainda não tenha rodado.
|
||||
* Necessário porque o plugin pode inicializar antes do mindplace.
|
||||
*/
|
||||
function plugin_<name>_load_license(): void
|
||||
{
|
||||
if (class_exists('\GlpiPlugin\Mindplace\License')) {
|
||||
return;
|
||||
}
|
||||
foreach ([
|
||||
GLPI_ROOT . '/plugins/mindplace/src/License.php',
|
||||
GLPI_ROOT . '/marketplace/mindplace/src/License.php',
|
||||
] as $path) {
|
||||
if (file_exists($path)) {
|
||||
include_once $path;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function plugin_<name>_check_config($verbose = false): bool
|
||||
{
|
||||
plugin_<name>_load_license();
|
||||
|
||||
if (!class_exists('\GlpiPlugin\Mindplace\License') || !\GlpiPlugin\Mindplace\License::isValid()) {
|
||||
if ($verbose && !isCommandLine()) {
|
||||
echo "<div class='alert alert-danger'>Licença Mindtek inativa ou Mind Place ausente. O plugin foi desativado por segurança.</div>";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function plugin_init_<name>(): void
|
||||
{
|
||||
global $PLUGIN_HOOKS;
|
||||
|
||||
$PLUGIN_HOOKS['csrf_compliant']['<name>'] = true;
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// KILL SWITCH — abort silently if license invalid
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
plugin_<name>_load_license();
|
||||
|
||||
if (!class_exists('\GlpiPlugin\Mindplace\License') || !\GlpiPlugin\Mindplace\License::isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── A partir daqui, registrar todos os hooks/classes do plugin ───
|
||||
// $PLUGIN_HOOKS[Hooks::ADD_CSS]['<name>'] = [...];
|
||||
// Plugin::registerClass(...);
|
||||
}
|
||||
```
|
||||
|
||||
### Comportamento do Kill Switch
|
||||
|
||||
| Cenário | Resultado |
|
||||
|---|---|
|
||||
| Mindplace não instalado | `check_config` retorna `false` → plugin não pode ativar |
|
||||
| Mindplace instalado mas licença inválida/expirada | mesmo do anterior |
|
||||
| Plugin já estava ativo e licença expirou | `init` aborta sem registrar hooks → GLPI ignora o plugin como se não existisse |
|
||||
| Tudo OK | Plugin funciona normalmente |
|
||||
|
||||
### Checklist da Fase 1
|
||||
|
||||
- [ ] `plugin_<name>_load_license()` adicionada
|
||||
- [ ] `plugin_<name>_check_config()` valida licença
|
||||
- [ ] `plugin_init_<name>()` aborta se inválida
|
||||
- [ ] Testado: desativar mindplace → tentar ativar plugin → deve falhar com aviso
|
||||
- [ ] Testado: licença válida → plugin ativa e funciona
|
||||
|
||||
---
|
||||
|
||||
## Fase 2 — Publicação no Forgejo
|
||||
|
||||
Forgejo é onde o código-fonte e os releases ficam. URL base: `https://servicedesk.mindtek.com.br/git/`.
|
||||
|
||||
### 2.1 — Criar repositório
|
||||
|
||||
1. Acesse o Forgejo logado como admin (`rodolpho.lopes`)
|
||||
2. **+** → **New Repository**
|
||||
- Owner: `rodolpho.lopes`
|
||||
- Repository name: nome do plugin (ex: `tilesections`)
|
||||
- Visibility: **Public**
|
||||
- Initialize repository: ✅ (com README mínimo — releases exigem ao menos um commit)
|
||||
3. **Create Repository**
|
||||
|
||||
### 2.2 — Push do código-fonte
|
||||
|
||||
```bash
|
||||
cd /caminho/local/do/plugin
|
||||
|
||||
# Se ainda não é um repo git
|
||||
git init
|
||||
git branch -M main
|
||||
|
||||
# Adicionar Forgejo como remote
|
||||
git remote add forgejo https://servicedesk.mindtek.com.br/git/rodolpho.lopes/<plugin>.git
|
||||
|
||||
# Commit + push
|
||||
git add .
|
||||
git commit -m "Initial commit"
|
||||
git push forgejo main
|
||||
```
|
||||
|
||||
Vai pedir credenciais do Forgejo na primeira vez.
|
||||
|
||||
### 2.3 — Criar release com ZIP
|
||||
|
||||
1. No Forgejo, no repo do plugin: **Releases** → **New Release**
|
||||
2. **Tag name**: `v1.0.0` (deve bater com `version` em `setup.php`)
|
||||
3. **Title**: `v1.0.0`
|
||||
4. **Description**: changelog resumido
|
||||
5. Em **Attachments**, fazer upload do ZIP gerado:
|
||||
```bash
|
||||
cd /caminho/local/do/plugin
|
||||
zip -r <plugin>-1.0.0.zip . \
|
||||
--exclude "*.git*" \
|
||||
--exclude "node_modules/*" \
|
||||
--exclude "vendor/*" \
|
||||
--exclude "*.DS_Store"
|
||||
```
|
||||
6. **Publish Release**
|
||||
|
||||
### Checklist da Fase 2
|
||||
|
||||
- [ ] Repositório criado público no Forgejo
|
||||
- [ ] Código-fonte push para `main`
|
||||
- [ ] Logo `logo.png` na raiz do plugin (será exibido no Mindplace)
|
||||
- [ ] Tag `v<versão>` criada
|
||||
- [ ] ZIP do plugin anexado como asset da release
|
||||
- [ ] Versão do ZIP bate com `setup.php`
|
||||
|
||||
---
|
||||
|
||||
## Fase 3 — Cadastro no catálogo Mindplace
|
||||
|
||||
O Mindplace lê `plugins.json` do repositório `rodolpho.lopes/mindplace` no Forgejo. Cada plugin é uma entrada nesse array.
|
||||
|
||||
### 3.1 — Editar `plugins.json`
|
||||
|
||||
Acesse: `https://servicedesk.mindtek.com.br/git/rodolpho.lopes/mindplace`
|
||||
|
||||
Edite o arquivo `plugins.json` (ícone do lápis na visualização do arquivo) e adicione a nova entrada:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "<plugin>",
|
||||
"repo": "rodolpho.lopes/<plugin>",
|
||||
"name": "Nome Bonito do Plugin",
|
||||
"description": "Descrição curta de uma frase.",
|
||||
"authors": [{"name": "Rodolpho O Lopes"}],
|
||||
"license": "GPL-2.0-or-later",
|
||||
"logo_url": null,
|
||||
"homepage_url": "https://servicedesk.mindtek.com.br/git/rodolpho.lopes/<plugin>",
|
||||
"issues_url": "https://servicedesk.mindtek.com.br/git/rodolpho.lopes/<plugin>/issues",
|
||||
"note": 5
|
||||
}
|
||||
```
|
||||
|
||||
**Campos importantes:**
|
||||
- `key`: igual ao nome do diretório do plugin (sem espaços, lowercase)
|
||||
- `repo`: caminho `<owner>/<repo>` no Forgejo
|
||||
- `logo_url: null`: faz o Mindplace buscar `logo.png` da raiz do repo automaticamente
|
||||
|
||||
Commit direto pelo Forgejo (Commit Changes na interface).
|
||||
|
||||
### 3.2 — Validação
|
||||
|
||||
No GLPI do cliente (com mindplace instalado e licença ativa), abra a aba **Mind Place**. O novo plugin deve aparecer no grid em até 1 hora (cache TTL). Para forçar refresh imediato, clica no botão de refresh do Mindplace.
|
||||
|
||||
### Checklist da Fase 3
|
||||
|
||||
- [ ] Entrada adicionada em `plugins.json`
|
||||
- [ ] Commit feito
|
||||
- [ ] Plugin aparece no grid do Mindplace
|
||||
- [ ] Logo carrega corretamente (se `logo.png` existe na raiz do repo)
|
||||
- [ ] Botão **Download/Install** funciona
|
||||
|
||||
---
|
||||
|
||||
## Fase 4 — Cadastro de licença no GLPI de produção
|
||||
|
||||
Cada cliente recebe um serial único. O serial fica em `glpi_softwarelicenses` do GLPI da Mindtek e é validado pelo `mindtek_validador.php`.
|
||||
|
||||
### 4.1 — Criar entrada de licença
|
||||
|
||||
No GLPI de produção (`servicedesk.mindtek.com.br`):
|
||||
|
||||
1. **Ativos → Licenças de Software → +**
|
||||
2. Preencher:
|
||||
- **Nome**: `Licença <Plugin> — <Cliente>` (ex: `Licença Tilesections — Empresa Acme`)
|
||||
- **Software**: associar ao software do plugin (criar se não existir)
|
||||
- **Número de série**: gerar hash hex aleatório de 32 caracteres
|
||||
```bash
|
||||
openssl rand -hex 16 | tr 'a-z' 'A-Z'
|
||||
# exemplo: 7C9F1E3A4B5D6F8E2C0A1B3D4E5F6071
|
||||
```
|
||||
- **Tipo**: livre (sugestão: `Mindplace Plugin`)
|
||||
- **Validade**: opcional, conforme contrato
|
||||
3. Salvar
|
||||
|
||||
### 4.2 — Entregar o serial ao cliente
|
||||
|
||||
Mensagem padrão para o cliente:
|
||||
|
||||
> Olá,
|
||||
>
|
||||
> Sua licença do plugin **\<Plugin\>** está ativa. Use o serial abaixo para ativar no Mindplace do seu GLPI:
|
||||
>
|
||||
> `7C9F1E3A4B5D6F8E2C0A1B3D4E5F6071`
|
||||
>
|
||||
> **Como ativar:**
|
||||
> 1. No GLPI, acesse **Configurar → Plugins → Mind Place** (ícone de chave)
|
||||
> 2. Cole o serial no campo **API Token** e salve
|
||||
> 3. Aguarde o status mudar para "Ativo" (alguns segundos)
|
||||
> 4. Pronto, o plugin pode ser ativado normalmente
|
||||
|
||||
### Checklist da Fase 4
|
||||
|
||||
- [ ] Software cadastrado em `Ativos → Software` (se primeira vez)
|
||||
- [ ] Licença criada com serial único de 32 chars hex
|
||||
- [ ] Serial enviado ao cliente
|
||||
- [ ] Cliente confirmou ativação no Mindplace dele
|
||||
|
||||
---
|
||||
|
||||
## Fase 5 — Manutenção contínua
|
||||
|
||||
### Lançar nova versão do plugin
|
||||
|
||||
1. Atualizar `version` no `setup.php` (ex: `1.0.0` → `1.1.0`)
|
||||
2. Commit + push para `main` no Forgejo
|
||||
3. Criar nova release no Forgejo com tag `v1.1.0` + novo ZIP
|
||||
4. **Não** atualiza `plugins.json` — o Mindplace pega automaticamente a release mais recente
|
||||
5. Clientes verão o botão **Atualizar** no Mindplace dentro de 1 hora (cache TTL)
|
||||
|
||||
### Revogar licença de um cliente
|
||||
|
||||
1. No GLPI de produção: **Ativos → Licenças de Software** → encontrar o serial
|
||||
2. Marcar `is_deleted = 1` (mover para a lixeira) **OU** deletar permanentemente
|
||||
3. No próximo cron (1x por dia) ou na próxima vez que o cliente salvar a config do mindplace, `License::cronCheck()` vai retornar `expired`
|
||||
4. Plugin do cliente desativa automaticamente
|
||||
|
||||
### Verificar status de licença de um cliente
|
||||
|
||||
```sql
|
||||
-- No GLPI de produção
|
||||
SELECT id, name, serial, is_deleted, completename
|
||||
FROM glpi_softwarelicenses
|
||||
WHERE serial = 'SERIAL_DO_CLIENTE';
|
||||
```
|
||||
|
||||
Ou consultar o validador diretamente:
|
||||
```bash
|
||||
curl "https://servicedesk.mindtek.com.br/mindtek_validador.php?token=SERIAL_DO_CLIENTE"
|
||||
# {"status":"active"} ou {"status":"expired"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Referências cruzadas
|
||||
|
||||
- [KB-PLUGIN-011](KB-PLUGIN-011-mindplace-release-zip-procedure.md) — procedimento de ZIP via GitHub (legado, antes da migração para Forgejo)
|
||||
- [KB-PLUGIN-012](KB-PLUGIN-012-helpdesk-tiles-extension-patterns.md) — padrões para estender o sistema de tiles do Helpdesk
|
||||
|
||||
## Script de automação
|
||||
|
||||
| Arquivo | Localização | Função |
|
||||
|---|---|---|
|
||||
| `mindplace-release.sh` | `bin/mindplace-release.sh` na raiz do projeto GLPI11 | Script Bash que automatiza as Fases 2–5 completas via API do Forgejo |
|
||||
|
||||
## Arquivos críticos
|
||||
|
||||
| Arquivo | Localização | Função |
|
||||
|---|---|---|
|
||||
| `mindtek_validador.php` | `/var/www/html/glpi/public/` no servidor de produção | Endpoint que valida serials contra `glpi_softwarelicenses` |
|
||||
| `License.php` | `plugins/mindplace/src/` no GLPI do cliente | Cron + `isValid()` que outros plugins consultam |
|
||||
| `plugins.json` | repo `rodolpho.lopes/mindplace` no Forgejo | Catálogo do Mindplace |
|
||||
| `secrets.php` | NÃO USAR no setup atual com Forgejo (era para PAT do GitHub) | — |
|
||||
|
||||
## Notas importantes
|
||||
|
||||
- **Forgejo não tem rate limit** — não precisa de token de autenticação. Repositórios são públicos
|
||||
- **`License::isValid()` é zero-latency** — lê cache no banco, não bate em API. Pode ser chamado em cada page load sem custo
|
||||
- **Cron de validação roda 1x/dia** — se o cliente quer validar agora, salvar a config do mindplace força o check imediato
|
||||
- **Sem licença, sem prejuízo** — o kill switch é silencioso; o GLPI continua funcional sem o plugin, sem mensagens de erro
|
||||
124
records/plugin-dev/KB-PLUGIN-014-getfromdbbycrit-stale-fields.md
Normal file
124
records/plugin-dev/KB-PLUGIN-014-getfromdbbycrit-stale-fields.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
---
|
||||
id: KB-PLUGIN-014
|
||||
title: getFromDBByCrit() não limpa $fields em falha — reutilizar a mesma instância vaza estado entre iterações
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- commondbtm
|
||||
- getfromdbbycrit
|
||||
- bug
|
||||
- iteration
|
||||
- state-leak
|
||||
status: active
|
||||
severity: critical
|
||||
created_at: 2026-05-10
|
||||
updated_at: 2026-05-10
|
||||
applies_to:
|
||||
- GLPI 11.0.x
|
||||
---
|
||||
|
||||
# `getFromDBByCrit()` vaza `$fields` entre iterações quando reutilizado
|
||||
|
||||
## Sintoma
|
||||
|
||||
Loop que itera sobre uma lista de itens (ex: plugins, entidades, qualquer `CommonDBTM`) reutilizando a mesma instância da classe via `getFromDBByCrit()` — itens que **não existem** no banco aparecem misteriosamente com dados do **item anterior** que foi encontrado.
|
||||
|
||||
No caso real do Mindplace: dois plugins (`splititil` e `tilesections`). `splititil` estava instalado, `tilesections` não. Ao renderizar a lista, `tilesections` apareceu com o **estado e ID do `splititil`** — clicar "Habilitar" no `splititil` parecia habilitar o `tilesections` (porque o render reaproveitava os fields).
|
||||
|
||||
## Causa
|
||||
|
||||
`CommonDBTM::getFromDBByCrit()` retorna `false` quando não encontra o item, **mas não limpa `$this->fields`**. A próxima chamada a `$obj->isNewItem()` ou acesso a `$obj->fields['...']` retorna o estado da **iteração anterior**.
|
||||
|
||||
```php
|
||||
// CommonDBTM::getFromDBByCrit (resumido)
|
||||
public function getFromDBByCrit(array $crit) {
|
||||
$iter = $DB->request([
|
||||
'FROM' => $this::getTable(),
|
||||
'WHERE' => $crit,
|
||||
]);
|
||||
if (count($iter) == 1) {
|
||||
$row = $iter->current();
|
||||
return $this->getFromDB($row['id']); // sucesso: carrega fields
|
||||
}
|
||||
return false; // ← FALHA: fields NÃO são limpos!
|
||||
}
|
||||
```
|
||||
|
||||
## Código defeituoso
|
||||
|
||||
```php
|
||||
// ❌ ERRADO — vaza estado entre iterações
|
||||
$plugin_obj = new Plugin();
|
||||
foreach ($plugins as &$p) {
|
||||
$plugin_obj->getFromDBByCrit(['directory' => $p['key']]);
|
||||
if ($plugin_obj->isNewItem()) {
|
||||
// tilesections (não instalado) entra aqui só na PRIMEIRA iteração
|
||||
// se for o primeiro a falhar. Senão, herda fields do anterior.
|
||||
$p['state'] = Plugin::NOTINSTALLED;
|
||||
} else {
|
||||
$p['state'] = (int) $plugin_obj->fields['state']; // ← fields do plugin ERRADO
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Padrões corretos
|
||||
|
||||
### Opção 1 — Nova instância por iteração (mais defensivo)
|
||||
|
||||
```php
|
||||
foreach ($plugins as &$p) {
|
||||
$plugin_obj = new Plugin(); // ← fresh instance, fields zerados
|
||||
$found = $plugin_obj->getFromDBByCrit(['directory' => $p['key']]);
|
||||
if (!$found) {
|
||||
$p['state'] = Plugin::NOTINSTALLED;
|
||||
continue;
|
||||
}
|
||||
$p['state'] = (int) $plugin_obj->fields['state'];
|
||||
}
|
||||
```
|
||||
|
||||
### Opção 2 — Usar o valor de retorno (mais idiomático)
|
||||
|
||||
```php
|
||||
$plugin_obj = new Plugin();
|
||||
foreach ($plugins as &$p) {
|
||||
if (!$plugin_obj->getFromDBByCrit(['directory' => $p['key']])) {
|
||||
$p['state'] = Plugin::NOTINSTALLED;
|
||||
continue;
|
||||
}
|
||||
$p['state'] = (int) $plugin_obj->fields['state'];
|
||||
}
|
||||
```
|
||||
|
||||
A Opção 2 é mais idiomática, mas **só funciona** se você nunca acessar `$plugin_obj->fields` no branch de falha. Se houver qualquer chance de o código acessar `$plugin_obj` depois de uma falha, prefira a Opção 1.
|
||||
|
||||
## Como nunca usar `isNewItem()` após `getFromDBByCrit()`
|
||||
|
||||
`isNewItem()` retorna `$this->fields['id'] <= 0`. Como `getFromDBByCrit()` não limpa `fields`, **`isNewItem()` mente** após um miss. Use sempre o valor de retorno do próprio `getFromDBByCrit()`:
|
||||
|
||||
```php
|
||||
// ❌
|
||||
$obj->getFromDBByCrit($crit);
|
||||
if ($obj->isNewItem()) { ... } // não confiável!
|
||||
|
||||
// ✅
|
||||
if (!$obj->getFromDBByCrit($crit)) { ... }
|
||||
```
|
||||
|
||||
## Comportamento de métodos relacionados
|
||||
|
||||
| Método | Limpa `$fields` em falha? |
|
||||
|---|---|
|
||||
| `getFromDB($id)` | ✅ sim — `fields = []` |
|
||||
| `getFromDBByCrit($crit)` | ❌ não |
|
||||
| `getFromDBByQuery($query)` | ❌ não (em geral) |
|
||||
| `find($criteria)` | n/a — retorna array, não muda estado |
|
||||
|
||||
Quando em dúvida, **sempre instancie um objeto novo** por iteração. O overhead é desprezível e elimina toda uma classe de bugs sutis.
|
||||
|
||||
## Onde isso pegou na vida real
|
||||
|
||||
`docker/glpi/plugins/mindplace/src/MarketplaceView.php::showPage()` — bug surgiu quando o catálogo do Mindplace passou a ter mais de um plugin. O segundo plugin (não instalado) "herdava" o estado do primeiro (instalado), causando comportamento inexplicável: clicar Habilitar num plugin parecia ativar outro.
|
||||
|
||||
Corrigido em `mindplace v1.0.7` instanciando `new Plugin()` por iteração.
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
---
|
||||
id: KB-PLUGIN-015
|
||||
title: querySelector('.row') colide com elementos de outros plugins no Helpdesk portal
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi
|
||||
- glpi11
|
||||
- helpdesk
|
||||
- tilesections
|
||||
- dom
|
||||
- selectors
|
||||
- bootstrap
|
||||
- cross-plugin-interference
|
||||
status: active
|
||||
severity: high
|
||||
created_at: 2026-05-10
|
||||
updated_at: 2026-05-10
|
||||
applies_to:
|
||||
- GLPI 11.0.x
|
||||
---
|
||||
|
||||
# `querySelector('.row')` colide com outros plugins no Helpdesk portal
|
||||
|
||||
## Sintoma
|
||||
|
||||
Plugin funciona perfeitamente no ambiente de desenvolvimento mas falha **silenciosamente** em produção — não há erro no console, não há AJAX falhando, mas o DOM não é manipulado. A causa é difícil de descobrir porque o servidor retorna os dados corretos e o JS chega a rodar.
|
||||
|
||||
No caso real: `tilesections/public/js/helpdesk_home.js` em produção não reorganizava os tiles em categorias. Em dev funcionava. A diferença era a presença de **outro plugin** (`news_alert`) em produção que injetava um elemento com classe `row` antes dos tiles.
|
||||
|
||||
## Causa
|
||||
|
||||
A `container-xl` do portal do Helpdesk pode conter múltiplos elementos com class `row`:
|
||||
|
||||
```html
|
||||
<div class="container-xl">
|
||||
<table class="central">
|
||||
<tbody><tr><td>
|
||||
<!-- Plugin news_alert injeta isto via hook DISPLAY_CENTRAL -->
|
||||
<div class="plugin_news_alert-container row align-items-stretch"></div>
|
||||
</td></tr></tbody>
|
||||
</table>
|
||||
|
||||
<!-- A row dos tiles -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-6 col-md-4 d-flex">
|
||||
<a class="card" href="...">...</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Código defeituoso:
|
||||
|
||||
```js
|
||||
const container = tilesSection.querySelector('.container-xl');
|
||||
const tilesRow = container.querySelector('.row'); // ❌ pega o do news_alert
|
||||
```
|
||||
|
||||
`querySelector` faz busca em profundidade e retorna o **primeiro** match na árvore — incluindo elementos aninhados. O `plugin_news_alert-container` está dentro da `<table>` que vem **antes** da row dos tiles, então é encontrado primeiro.
|
||||
|
||||
Depois disso, `tilesRow.querySelectorAll(':scope > div')` retorna zero (o container do news plugin está vazio), e a função desiste sem fazer nada.
|
||||
|
||||
## Padrão correto
|
||||
|
||||
Usar `:scope > .row` para garantir que apenas **filhos diretos** sejam considerados:
|
||||
|
||||
```js
|
||||
const tilesRow = container.querySelector(':scope > .row'); // ✅ só filhos diretos
|
||||
```
|
||||
|
||||
`:scope` referencia o elemento de partida da query (no caso, `container`). `:scope > .row` significa "row que é filha direta de container".
|
||||
|
||||
## Lições
|
||||
|
||||
- **Nunca confie que sua produção tem a mesma estrutura DOM do dev**. Plugins de terceiros (news, dashboards, customizações antigas) injetam elementos em pontos imprevisíveis.
|
||||
- Para qualquer seletor que dependa de hierarquia, **prefira `:scope >` ou caminhos específicos** ao invés de buscas globais com classes Bootstrap genéricas como `.row`, `.card`, `.col-*`.
|
||||
- Quando funcionar local e não em produção, **não assuma diferenças de container/Docker**. Inspecione o DOM real do cliente. O usuário pode rodar no console:
|
||||
```js
|
||||
document.querySelector('.tiles-banner .container-xl').innerHTML.substring(0, 600)
|
||||
```
|
||||
e mandar o resultado.
|
||||
|
||||
## Hierarquia de especificidade recomendada
|
||||
|
||||
Para seletores de containers em plugins GLPI, em ordem de robustez:
|
||||
|
||||
1. **`[data-glpi-*]` atributos** — quando GLPI fornece (ex: `[data-glpi-helpdesk-config-tiles]`). Mais estável.
|
||||
2. **`:scope > .classe`** — quando só filhos diretos importam.
|
||||
3. **Caminho específico** — ex: `.tiles-banner > .container-xl > .row`.
|
||||
4. **`.row` solto** — **evitar**. Praticamente garante colisão com outros plugins.
|
||||
|
||||
## Onde isso pegou na vida real
|
||||
|
||||
`docker/glpi/plugins/tilesections/public/js/helpdesk_home.js::reorganizeTiles()` — corrigido em `tilesections v1.1.2`. O bug não era reproduzível em dev porque o ambiente Docker não tinha o plugin `news_alert` instalado.
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
---
|
||||
id: KB-PLUGIN-016
|
||||
title: "Bypass de CSRF e Autenticação Nativa (Bearer Token) para APIs Customizadas no GLPI 11"
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- glpi-11
|
||||
- csrf
|
||||
- api
|
||||
- jwt
|
||||
- oauth
|
||||
- auth
|
||||
- stateless
|
||||
status: active
|
||||
severity: high
|
||||
created_at: 2026-05-20
|
||||
updated_at: 2026-05-20
|
||||
applies_to:
|
||||
- GLPI 11+
|
||||
- Plugins com APIs customizadas
|
||||
---
|
||||
|
||||
# Bypass de CSRF e Autenticação Nativa para APIs Customizadas no GLPI 11
|
||||
|
||||
## Contexto
|
||||
No GLPI 11, a segurança do roteador principal e do Kernel do Symfony foi reforçada. A constante `GLPI_USE_CSRF_CHECK` passou a ser ignorada por motivos de segurança, e todas as rotas (incluindo as de compatibilidade "LegacyRoutes" mapeadas via `public/index.php`) sofrem interceptação do `CheckCsrfListener` se não forem explicitamente isentas.
|
||||
|
||||
Isso bloqueia requisições `POST` (como payloads JSON-RPC) feitas para endpoints customizados criados dentro de plugins (ex.: `ajax/mcp.php` ou `public/index.php`), retornando erro de "Ação não permitida" ou redirecionamento de sessão, impossibilitando a criação de APIs independentes dentro dos plugins usando a infraestrutura convencional sem modificações.
|
||||
|
||||
## Causa Raiz
|
||||
O `CheckCsrfListener` no Kernel do GLPI 11 bloqueia requisições `POST` sem token CSRF válido a menos que o endpoint seja registrado como um **recurso stateless** (sem estado). O GLPI Web Frontend requer que as requisições possuam estado (Session Cookies), bloqueando chamadas puras de API (ex: Server-to-Server via MCP) que dependem exclusivamente de Cabeçalhos de Autorização (`Authorization: Bearer <TOKEN>`).
|
||||
|
||||
## Resolução e Arquitetura Recomendada
|
||||
|
||||
A solução exige dois passos: isentar o endpoint customizado do controle de estado (e consequentemente do CSRF) e implementar a validação nativa do JWT dentro do escopo do endpoint, injetando o usuário autenticado na sessão corrente.
|
||||
|
||||
### 1. Registrar a Rota como Stateless
|
||||
No arquivo `setup.php` do seu plugin, dentro da função `plugin_init_seuplugin()`, registre o script PHP (ex: `ajax/mcp.php`) como uma rota stateless através do `SessionManager`. Essa regra será lida pelo `SessionManager::isResourceStateless()`:
|
||||
|
||||
```php
|
||||
function plugin_init_mcprotocol() {
|
||||
global $PLUGIN_HOOKS;
|
||||
$PLUGIN_HOOKS['csrf_compliant']['mcprotocol'] = true;
|
||||
|
||||
// Registra o endpoint da API como stateless para ignorar o CSRF e Session Checks no GLPI 11
|
||||
if (class_exists('\Glpi\Http\SessionManager') && method_exists('\Glpi\Http\SessionManager', 'registerPluginStatelessPath')) {
|
||||
\Glpi\Http\SessionManager::registerPluginStatelessPath('mcprotocol', '#ajax/mcp\.php#');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Validar JWT e Injetar na Sessão (Herança de Permissões)
|
||||
No seu arquivo de endpoint da API (`ajax/mcp.php`), carregue as dependências nativas (incluindo `inc/includes.php`). Valide o Bearer token JWT diretamente usando a chave pública nativa do GLPI (localizada em `GLPI_CONFIG_DIR . '/oauth.pub'`) e depois estabeleça as variáveis globais de sessão para garantir que todas as chamadas subjacentes das classes GLPI (ex: `Ticket`, `Project`) atuem em nome do usuário correto.
|
||||
|
||||
```php
|
||||
// ajax/mcp.php
|
||||
$AJAX_INCLUDE = 1;
|
||||
define('GLPI_ROOT', '../../..');
|
||||
|
||||
// Hack de bypass: força o método como GET antes do includes.php carregar
|
||||
// Para evitar possíveis verificações antecipadas em versões transicionais
|
||||
$realMethod = $_SERVER['REQUEST_METHOD'];
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
include (GLPI_ROOT . "/inc/includes.php");
|
||||
$_SERVER['REQUEST_METHOD'] = $realMethod;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// 1. Extração do Token
|
||||
$headers = getallheaders();
|
||||
$authHeader = $headers['Authorization'] ?? '';
|
||||
|
||||
if (empty($authHeader) || !preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
|
||||
echo json_encode(["error" => "Unauthorized - Bearer token missing"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$jwt = $matches[1];
|
||||
// ATENÇÃO: GLPI 11 exporta a chave pública no diretório principal de configs
|
||||
$publicKeyPath = GLPI_CONFIG_DIR . '/oauth.pub';
|
||||
|
||||
try {
|
||||
// 2. Validação Nativa
|
||||
$publicKey = file_get_contents($publicKeyPath);
|
||||
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
|
||||
$userId = $decoded->sub ?? null;
|
||||
|
||||
if (!$userId) {
|
||||
throw new Exception("Token missing 'sub' claim");
|
||||
}
|
||||
|
||||
// 3. Injeção de Sessão (Herança de LGPD / Permissões)
|
||||
$_SESSION['glpiID'] = $userId;
|
||||
$_SESSION['glpiname'] = "API User";
|
||||
// É recomendado carregar outras flags se for interagir com entidades complexas
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(["error" => "Invalid token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. Fluxo normal da API a partir daqui
|
||||
$payload = json_decode(file_get_contents('php://input'), true);
|
||||
echo json_encode(["status" => "success", "user_id" => $userId]);
|
||||
```
|
||||
|
||||
## Aprendizados Chave
|
||||
- O `scimmind` foi desenhado com o bypass baseado em `GLPI_USE_CSRF_CHECK`, o qual está obsoleto no GLPI 11.
|
||||
- A diretriz recomendada pela Teclib (GLPI 11) para pontos de acesso isolados é o uso do `SessionManager::registerPluginStatelessPath`.
|
||||
- Não adianta validar a requisição com sucesso e não definir o `$_SESSION['glpiID']`. O ecosistema do GLPI é intrinsicamente atrelado a `$_SESSION`, e falhar em instanciá-lo antes de acionar métodos da classe (como buscar itens de DB) levará a falhas de acesso e violação das entidades (`Entity`).
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
id: KB-PLUGIN-017
|
||||
title: "Bug do detector de wrapper directory no instalador Mindplace mutilava nomes de arquivos"
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- mindplace
|
||||
- marketplace
|
||||
- zip
|
||||
- extractor
|
||||
- install-bug
|
||||
status: active
|
||||
severity: high
|
||||
created_at: 2026-05-21
|
||||
updated_at: 2026-05-21
|
||||
applies_to:
|
||||
- Plugin Mindplace (ajax/marketplace.php)
|
||||
- Qualquer plugin distribuído via Mindplace
|
||||
---
|
||||
|
||||
# KB-PLUGIN-017 — Bug do detector de wrapper directory no instalador Mindplace
|
||||
|
||||
## Contexto
|
||||
|
||||
O extrator de plugins do Mindplace (`docker/glpi/plugins/mindplace/ajax/marketplace.php`) baixa o `.zip` da release do GitHub, detecta um "wrapper directory" no topo e remove esse prefixo dos demais entries antes de extrair para `marketplace/<plugin>/`.
|
||||
|
||||
Quando o ZIP de release de um plugin é gerado **sem** um wrapper directory consistente, a heurística antiga aceitava qualquer pasta top-level como wrapper e **mutilava** os paths de todos os outros entries.
|
||||
|
||||
## Sintoma
|
||||
|
||||
A instalação via Mindplace completa "com sucesso" mas o GLPI marca o plugin como **não-carregável** silenciosamente. Listando o diretório de instalação aparecem arquivos com nomes truncados:
|
||||
|
||||
```
|
||||
marketplace/mcprotocol/
|
||||
├── .php ← deveria ser hook.php
|
||||
├── .png ← deveria ser logo.png
|
||||
├── p.php ← deveria ser setup.php
|
||||
├── est.php ← deveria ser apirest.php
|
||||
├── ic/ ← deveria ser public/
|
||||
│ ├── index.php
|
||||
│ └── logo.png
|
||||
├── t/ ← deveria ser front/
|
||||
└── Boot.php ← deveria ser src/Boot.php
|
||||
```
|
||||
|
||||
O conteúdo dos arquivos está **íntegro** — apenas o nome do arquivo (caminho) foi mutilado.
|
||||
|
||||
## Causa Raiz
|
||||
|
||||
A detecção em `ajax/marketplace.php` (versão buggy):
|
||||
|
||||
```php
|
||||
$first = $zip->getNameIndex(0);
|
||||
$prefix = '';
|
||||
if ($first !== false && str_ends_with($first, '/') && substr_count(rtrim($first, '/'), '/') === 0) {
|
||||
$prefix = $first; // aceito como wrapper sem validar
|
||||
}
|
||||
```
|
||||
|
||||
A heurística aceitava como wrapper **qualquer pasta top-level que fosse o primeiro entry do ZIP**, mesmo que essa pasta não fosse realmente um wrapper (envelopando todos os demais).
|
||||
|
||||
Quando o ZIP do MCProtocol foi gerado pelo script `bin/mindplace-release.sh` (versão anterior, que fazia `cd "$PLUGIN_DIR" && zip -r "$ZIP_PATH" .` — sem wrapper directory), o primeiro entry era a pasta `inc/` (top-level legítima do plugin). O extrator então removia 4 chars (`inc/`) do começo de **todos** os outros entries:
|
||||
|
||||
| Entry no ZIP | substr(_, 4) | Resultado em disco |
|
||||
|---|---|---|
|
||||
| `hook.php` | `.php` | `.php` |
|
||||
| `logo.png` | `.png` | `.png` |
|
||||
| `setup.php` | `p.php` | `p.php` |
|
||||
| `apirest.php` | `est.php` | `est.php` |
|
||||
| `public/` | `ic/` | pasta `ic/` |
|
||||
| `public/index.php` | `ic/index.php` | dentro de `ic/` |
|
||||
| `front/` | `t/` | pasta `t/` |
|
||||
| `src/Boot.php` | `Boot.php` | top-level |
|
||||
| `inc/mcp.class.php` | `mcp.class.php` | top-level |
|
||||
|
||||
## Resolução (fix aplicado em maio/2026)
|
||||
|
||||
O detector agora **valida que o candidato a wrapper é prefixo de todos os entries** antes de aceitá-lo. Se algum entry não começar com o candidato, o prefix fica vazio e a extração preserva a estrutura original do ZIP.
|
||||
|
||||
```php
|
||||
// Detect common top-level wrapper directory.
|
||||
// The wrapper is only valid if EVERY entry starts with it; otherwise a
|
||||
// top-level folder like "inc/" would be mistaken for a wrapper and the
|
||||
// substr() below would mutilate the remaining entries' paths.
|
||||
$prefix = '';
|
||||
$count = $zip->count();
|
||||
$candidate = $count > 0 ? $zip->getNameIndex(0) : false;
|
||||
|
||||
if (
|
||||
$candidate !== false
|
||||
&& str_ends_with($candidate, '/')
|
||||
&& substr_count(rtrim($candidate, '/'), '/') === 0
|
||||
) {
|
||||
$allMatch = true;
|
||||
for ($i = 1; $i < $count; $i++) {
|
||||
$name = $zip->getNameIndex($i);
|
||||
if ($name === false || !str_starts_with($name, $candidate)) {
|
||||
$allMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($allMatch) {
|
||||
$prefix = $candidate;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Arquivo: `docker/glpi/plugins/mindplace/ajax/marketplace.php` (linhas ~91-114).
|
||||
|
||||
## Como diagnosticar uma instalação afetada
|
||||
|
||||
1. Listar `docker/glpi/marketplace/<plugin>/` — se aparecerem arquivos com nomes começando por `.` (`.php`, `.png`), arquivos com nomes incompletos (`p.php`, `est.php`), ou pastas com nomes esquisitos (`ic/`, `t/`), o bug está presente.
|
||||
2. Confirmar inspecionando o `.zip` original: `unzip -l <plugin>.zip | head` — se o primeiro entry **não** é um wrapper directory que contém todos os demais, o ZIP é vulnerável a esta versão do bug.
|
||||
|
||||
## Como evitar (em plugins novos)
|
||||
|
||||
Gerar releases com **wrapper directory consistente** — ver [[KB-PLUGIN-018]] para a workflow recomendada.
|
||||
|
||||
## Veja também
|
||||
|
||||
- [KB-PLUGIN-011](KB-PLUGIN-011-mindplace-release-zip-procedure.md) — Procedimento de release do Mindplace
|
||||
- [KB-PLUGIN-013](KB-PLUGIN-013-mindplace-plugin-publication-runbook.md) — Runbook de publicação de plugin no Mindplace
|
||||
- [KB-PLUGIN-018](KB-PLUGIN-018-plugin-release-zip-wrapper-convention.md) — Convenção de wrapper directory para ZIPs de release
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
---
|
||||
id: KB-PLUGIN-018
|
||||
title: "Convenção de wrapper directory na geração de ZIP de release de plugin (script mindplace-release.sh)"
|
||||
domain: plugin-dev
|
||||
tags:
|
||||
- mindplace
|
||||
- forgejo
|
||||
- release
|
||||
- zip
|
||||
- wrapper
|
||||
- mindplace-release-sh
|
||||
status: active
|
||||
severity: high
|
||||
created_at: 2026-05-21
|
||||
updated_at: 2026-05-21
|
||||
applies_to:
|
||||
- Qualquer plugin publicado via bin/mindplace-release.sh
|
||||
- Plugins distribuídos pelo catálogo Mindplace
|
||||
related:
|
||||
- KB-PLUGIN-013
|
||||
- KB-PLUGIN-017
|
||||
---
|
||||
|
||||
# KB-PLUGIN-018 — Convenção de wrapper directory para ZIPs de release
|
||||
|
||||
## Contexto
|
||||
|
||||
ZIPs de release de plugin GLPI distribuídos via Mindplace devem ter um **wrapper directory consistente** no topo (uma pasta com o nome do plugin envolvendo todos os arquivos). Sem isso, o extrator do Mindplace podia mutilar paths em versões anteriores ao fix descrito em [KB-PLUGIN-017](KB-PLUGIN-017-mindplace-zip-wrapper-detector-bug.md).
|
||||
|
||||
Mesmo após o fix do detector, manter o wrapper continua sendo a forma "correta" e mais robusta: dá ao extrator um prefix bem-definido para remover, e remove ambiguidade na inspeção do ZIP.
|
||||
|
||||
## Estrutura esperada
|
||||
|
||||
```
|
||||
mcprotocol-1.0.3.zip
|
||||
└── mcprotocol/ ← wrapper directory (nome = key do plugin = basename do diretório)
|
||||
├── setup.php
|
||||
├── hook.php
|
||||
├── ajax/
|
||||
│ └── mcp.php
|
||||
├── src/
|
||||
│ ├── Boot.php
|
||||
│ └── Server.php
|
||||
└── public/
|
||||
└── logo.png
|
||||
```
|
||||
|
||||
Verificação rápida:
|
||||
|
||||
```bash
|
||||
unzip -l <plugin>-<ver>.zip | head
|
||||
# Primeiro entry DEVE ser "<plugin>/"
|
||||
# Todos os demais entries DEVEM começar com "<plugin>/"
|
||||
```
|
||||
|
||||
## Fluxo oficial (automatizado)
|
||||
|
||||
Use o script `bin/mindplace-release.sh` documentado em [KB-PLUGIN-013](KB-PLUGIN-013-mindplace-plugin-publication-runbook.md). Desde 2026-05-21, a Fase 3 do script gera ZIPs com wrapper directory automaticamente — rodando `zip` do **diretório pai** do plugin:
|
||||
|
||||
```bash
|
||||
# Trecho relevante de bin/mindplace-release.sh (Fase 3)
|
||||
PARENT_DIR="$(dirname "$PLUGIN_DIR")"
|
||||
PLUGIN_BASENAME="$(basename "$PLUGIN_DIR")"
|
||||
cd "$PARENT_DIR"
|
||||
zip -r "$ZIP_PATH" "$PLUGIN_BASENAME" \
|
||||
--exclude "$PLUGIN_BASENAME/.git*" \
|
||||
--exclude "$PLUGIN_BASENAME/node_modules/*" \
|
||||
--exclude "$PLUGIN_BASENAME/vendor/*" \
|
||||
--exclude "*.DS_Store" \
|
||||
--exclude "*.zip" \
|
||||
-q
|
||||
|
||||
# Sanity check: aborta se wrapper não aparecer no topo
|
||||
FIRST_ENTRY=$(unzip -Z1 "$ZIP_PATH" | head -1)
|
||||
if [ "$FIRST_ENTRY" != "$PLUGIN_BASENAME/" ]; then
|
||||
err "ZIP gerado sem wrapper directory esperado..."
|
||||
fi
|
||||
```
|
||||
|
||||
O sanity check no fim falha rápido se algum dia o ZIP voltar a sair sem wrapper.
|
||||
|
||||
## Geração manual (fallback)
|
||||
|
||||
Se precisar gerar localmente fora do script:
|
||||
|
||||
```bash
|
||||
# A partir do diretório PAI da pasta do plugin
|
||||
cd docker/glpi/plugins
|
||||
zip -r mcprotocol-1.0.3.zip mcprotocol/ \
|
||||
-x "mcprotocol/.git*" \
|
||||
-x "*.zip" \
|
||||
-x "mcprotocol/node_modules/*" \
|
||||
-x "mcprotocol/vendor/*"
|
||||
```
|
||||
|
||||
**Não** fazer `cd plugin && zip -r ...zip .` — esse padrão produz ZIP sem wrapper e era o bug histórico do script.
|
||||
|
||||
## .gitignore padrão do plugin
|
||||
|
||||
Para que o ZIP não inclua arquivos sensíveis ou de teste, garantir `.gitignore` na raiz do plugin:
|
||||
|
||||
```gitignore
|
||||
# Dependências
|
||||
vendor/
|
||||
node_modules/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Build artifacts
|
||||
*.zip
|
||||
|
||||
# Arquivos de teste (não publicar)
|
||||
ajax/test*.php
|
||||
ajax/mcp_test.php
|
||||
front/test*.php
|
||||
```
|
||||
|
||||
O script `mindplace-release.sh` excludes `.git*` por padrão; o `.gitignore` em si pode ser commitado e publicado sem problema.
|
||||
|
||||
## Erros comuns
|
||||
|
||||
| Sintoma | Causa | Fix |
|
||||
|---|---|---|
|
||||
| Após instalar via Mindplace, arquivos com nomes mutilados (`.php`, `p.php`, pastas `ic/`, `t/`) | ZIP gerado sem wrapper + Mindplace com detector buggy | Atualizar Mindplace ([KB-PLUGIN-017]) **e** regerar ZIP com wrapper |
|
||||
| Após instalar, pasta `mcprotocol/mcprotocol/` aninhada | ZIP com wrapper mas extrator versão muito antiga, ou wrapper com nome diferente do `key` | Conferir que o basename do diretório bate com o `key` em plugins.json |
|
||||
| Release não aparece no Mindplace | Release sem asset `.zip` no Forgejo | Verificar que a Fase 4 do script subiu o asset; tem rate limit ou erro? |
|
||||
|
||||
## Veja também
|
||||
|
||||
- [KB-PLUGIN-011](KB-PLUGIN-011-mindplace-release-zip-procedure.md) — Procedimento de release **legado** via GitHub (deprecated)
|
||||
- [KB-PLUGIN-013](KB-PLUGIN-013-mindplace-plugin-publication-runbook.md) — Runbook completo de publicação no Mindplace via Forgejo
|
||||
- [KB-PLUGIN-017](KB-PLUGIN-017-mindplace-zip-wrapper-detector-bug.md) — Bug do detector de wrapper (motivação histórica)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# KB-PLUGIN-019: Arquitetura BFF (Backend For Frontend) e Segurança via API no GLPI 11 (Plugin MCProtocol)
|
||||
|
||||
**Domain:** plugin-dev
|
||||
**Tags:** glpi, glpi11, mcp, api, rbac, security, bff
|
||||
|
||||
## 1. Segurança e Escopo de Dados (RBAC)
|
||||
O uso obrigatório da API REST para listagens de dados (ao invés de consultas SQL diretas via `global $DB`) garante a aplicação das restrições de permissão nativas do GLPI.
|
||||
- **Risco do SQL Direto:** Ignora regras de Entidades, Perfis, e Direitos do GLPI (`getEntitiesRestrictCriteria()`). Pode causar vazamento de dados sensíveis (ex: chamados confidenciais) em clientes MCP.
|
||||
- **Vantagem da API:** O cliente MCP usa o próprio token OAuth vinculado à sessão de um usuário. O motor da API V2 obrigatoriamente aplica as restrições de Entidade e Perfil, e resolve relacionamentos (HATEOAS).
|
||||
|
||||
## 2. A Camada "Backend For Frontend" (BFF)
|
||||
Ferramentas MCP genéricas (ex: `glpi_get_items`) forçam o LLM a "adivinhar" rotas exatas (`/Assistance/Ticket`), consumindo tokens extras e elevando o risco de alucinações.
|
||||
- **Solução (View Semântica):** O plugin GLPI exporta ferramentas altamente especializadas (ex: `glpi_ticket_search_by_status`).
|
||||
- **Implementação:** O LLM envia parâmetros simples (`status: 4`), enquanto o código PHP da ferramenta empacota isso na sintaxe estrita da API do GLPI (ex: `['searchText' => ['status' => 4]]`), direcionando o request cURL internamente.
|
||||
|
||||
## 3. Roteamento e HTTP Loopback no cURL Interno
|
||||
Para que o plugin consiga consumir a própria API REST do GLPI sem passar pelo proxy/firewall de borda, o request cURL interno do plugin exige tratamentos:
|
||||
1. **Host Correto:** Deve usar o `$CFG_GLPI['url_base']` (ex: `127.0.0.1` associado ao virtual host) em vez de domains externos que causariam gargalo de NAT.
|
||||
2. **Redirecionamento:** `CURLOPT_FOLLOWLOCATION => true` deve estar ativo para prever ambientes onde Apache/Nginx force 301 de HTTP para HTTPS.
|
||||
3. **SSL Local:** `CURLOPT_SSL_VERIFYPEER => false` é obrigatório para não falhar caso os certificados locais/SNI não correspondam à interface loopback.
|
||||
|
||||
## 4. O Sistema de Buscas do GLPI (V2 API)
|
||||
Para realizar buscas exatas em colunas específicas (ex: "Apenas chamados com status X"), o parâmetro `searchText` genérico da API V2 tem limitações. Enviar `searchText=status=4` gera uma busca texto genérica. A solução correta via código PHP é enviar o `searchText` em formato array (`'searchText' => ['status' => 4]`), o que induz o GLPI a procurar a correspondência exata.
|
||||
33
records/plugin-dev/KB-PLUGIN-020-mcprotocol-bff-roadmap.md
Normal file
33
records/plugin-dev/KB-PLUGIN-020-mcprotocol-bff-roadmap.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# KB-PLUGIN-020: Roadmap de Evolução do MCProtocol (Views Semânticas API)
|
||||
|
||||
**Domain:** plugin-dev
|
||||
**Tags:** glpi, glpi11, mcp, api, roadmap, bff
|
||||
|
||||
Usando a ferramenta `glpi_ticket_search_by_status` como modelo de arquitetura "Backend For Frontend", as próximas atualizações do plugin focarão em mapear o restante do ecossistema de chamados e ativos do GLPI para ferramentas simples, seguras e com baixo custo de tokens.
|
||||
|
||||
## 1. Gestão de Chamados (Assistance/Ticket)
|
||||
- `glpi_ticket_get_pending_approvals`
|
||||
- **Objetivo:** Retornar os chamados aguardando aprovação do usuário atual.
|
||||
- **Uso de API:** Filtro por status=Aguardando Aprovação e amarrado à Entidade.
|
||||
- `glpi_ticket_assign_to_me`
|
||||
- **Objetivo:** Assumir a autoria técnica de um chamado com apenas o ID.
|
||||
- **Uso de API:** Aciona o PATCH nativo descobrindo o usuário da sessão.
|
||||
- `glpi_ticket_add_private_note`
|
||||
- **Objetivo:** Adicionar acompanhamento oculto do usuário comum.
|
||||
- **Uso de API:** POST com `is_private = 1`.
|
||||
|
||||
## 2. Gestão de Ativos (Assets)
|
||||
- `glpi_computer_get_unassigned`
|
||||
- **Objetivo:** Listar computadores em estoque (sem usuário/localização).
|
||||
- **Uso de API:** Busca em `/Assets/Computer` com filtro de status de estoque e sem vinculação.
|
||||
- `glpi_user_get_my_assets`
|
||||
- **Objetivo:** Retornar a lista de ativos que o usuário solicitante possui.
|
||||
- **Uso de API:** Busca computadores e periféricos vinculados ao ID da sessão.
|
||||
|
||||
## 3. Gestão de Projetos (Tools)
|
||||
- `glpi_project_get_open_tasks`
|
||||
- **Objetivo:** Listar tarefas de projeto do usuário.
|
||||
- **Uso de API:** Rota de projetos filtrando tarefas pendentes.
|
||||
|
||||
## Benefícios Contínuos
|
||||
O principal foco é: **Zero adivinhação de rotas pelo LLM, 100% de uso da engine RBAC nativa, JSON Limpo.**
|
||||
68
schemas/knowledge-record.schema.json
Normal file
68
schemas/knowledge-record.schema.json
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Knowledge Record Metadata",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"title",
|
||||
"domain",
|
||||
"tags",
|
||||
"status",
|
||||
"severity",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"applies_to"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^KB-[A-Z]+-[0-9]{3}$"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 8
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"enum": ["infrastructure", "security", "plugin-dev", "process", "ci-cd"]
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["active", "deprecated", "draft"]
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high", "critical"]
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"applies_to": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"related_records": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^KB-[A-Z]+-[0-9]{3}$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
Loading…
Reference in a new issue