isPluginItemType() rejeita '\GlpiPlugin\...' e registerClass falha silenciosamente — tab addtabon some sem erro em log (visto no butterfly). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.3 KiB
| id | title | domain | tags | status | severity | created_at | updated_at | applies_to | related_records | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| KB-PLUGIN-038 | GLPI 11 — Assets de plugin: Monaco no lugar do CodeMirror, fim do jQuery UI, add_css/add_javascript e estáticos só em public/ | plugin-dev |
|
active | high | 2026-07-03 | 2026-07-03 |
|
|
GLPI 11 — Assets de plugin: Monaco, fim do jQuery UI, add_css e public/
Aprendizados do porte do plugin butterfly (2.1.0, GLPI 10 → 11.0.8, 2026-07-03).
Quatro armadilhas independentes que atingem qualquer plugin vindo do GLPI 9/10.
1. CodeMirror foi removido — o editor de código do core é o Monaco
public/lib/ do GLPI 11 não tem mais CodeMirror; tem monaco-editor/monaco.js.
Qualquer CodeMirror.fromTextArea(...) de plugin quebra com ReferenceError
silencioso no console (o form continua funcionando como textarea puro).
Carregar a lib
monaco é chave válida do mecanismo $CFG_GLPI['javascript'] (testado em
Html.php ~linha 1178, in_array('monaco', $jslibs)):
// setup.php, em plugin_init_<key>()
$CFG_GLPI['javascript']['config']['config'][] = 'monaco';
Padrão de uso (o mesmo do core em templates/pages/admin/entity/custom_ui.html.twig)
echo '<div id="meu_editor" style="height: 300px"></div>';
echo Html::scriptBlock('
$(function() {
window.GLPI.Monaco.createEditor("meu_editor", "scss", ' . json_encode($valor, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) . ', []).then(() => {
$("#meu_editor").closest("form").on("formdata", (e) => {
const editors = window.monaco.editor.getEditors().filter(
(ed) => ed._domElement.id === "meu_editor"
);
if (editors.length) {
e.originalEvent.formData.delete("meu_campo");
e.originalEvent.formData.append("meu_campo", editors[0].getValue());
}
});
});
});
');
Pontos-chave:
- O Monaco não usa textarea: é um
<div>container + sync no submit via eventoformdata(por isso o form precisa ser um<form>nativo). - Manter um
<textarea class="d-none" name="meu_campo">com o valor original como fallback: se a lib não carregar, o valor antigo é preservado no POST (o handlerformdatafazdelete+appendquando o editor existe). - Em Twig, preferir a macro
fields_macros.html.twigdo core que já embute esse padrão.
2. jQuery UI não existe mais
$(...).tabs(), .dialog(), .sortable() etc. quebram com
TypeError: ... is not a function. Plugins antigos costumam ter tabs jQuery UI
na tela de config. Substituir por tabs do Tabler/Bootstrap ou remover.
(jQuery em si continua disponível.)
3. add_css / add_javascript: valor é interpolado UMA vez e ganha ?v= do core
Glpi\Application\View\Extension\PluginExtension monta
"/plugins/{$plugin}/{$file}" e acrescenta 'version' => Plugin::getPluginFilesVersion($plugin)
(vira ?v=<hash> na URL). Consequências:
- Truques de objeto com
__toStringpara injetar query string (padrão "file_exists chama primeiro, concatenação chama depois" do GLPI 9/10) não funcionam mais — o cast acontece uma única vez. Usar string simples. - Cache busting por mudança de config deve ser resolvido pelo próprio endpoint
(ex.: redirect para
?_=<timestamp>) ou aceitando o?v=por versão. - Continua valendo KB-PLUGIN-004: a chave do array é o plugin key exato.
4. Estáticos de plugin: só o que está em public/ é servido
RequestRouterTrait (core) só expõe recursos de plugin que estejam no
diretório public/ do plugin, acessados sem o segmento /public na URL:
plugins/meuplugin/public/pics/logo.png → GET /plugins/meuplugin/pics/logo.png
Arquivos fora de public/ (ex.: pics/, css/ na raiz do plugin) retornam
404 — mesmo que .htaccess os liberasse no Apache do GLPI 10. Scripts PHP
legados (front/*.php) continuam roteados pelo LegacyFileLoadController
(com as firewall strategies registradas no setup.php).
Migração típica: git mv pics public/pics e trocar referências relativas
(../plugins/...) por $CFG_GLPI['root_doc'] . '/plugins/<key>/...'.
Validação padrão
# lib presente?
ls /var/www/glpi/public/lib | grep -i monaco # existe
ls /var/www/glpi/public/lib | grep -i codemirror # NÃO existe
# estático servido?
curl -s -o /dev/null -w "%{http_code}" http://localhost/plugins/<key>/pics/x.png
5. Plugin::registerClass NUNCA com barra invertida inicial
Plugin::registerClass('\GlpiPlugin\Meuplugin\Config', ...) falha
silenciosamente (retorna false): a primeira coisa que ele faz é
isPluginItemType($itemtype), cuja regex não aceita \ inicial.
Sintoma observado no butterfly: tab de config registrada via addtabon
simplesmente não aparece em Configuração > Geral, sem nenhum erro em log.
// ERRADO — isPluginItemType() retorna false, registerClass vira no-op
Plugin::registerClass('\GlpiPlugin\Butterfly\Config', ['addtabon' => ['Config']]);
// CERTO — ::class nunca inclui a barra inicial
Plugin::registerClass(\GlpiPlugin\Butterfly\Config::class, ['addtabon' => ['Config']]);
O mesmo vale para o valor de glpi_tab/forcetab em URLs: a chave da tab
gerada pelo core é <itemtype-sem-barra>$<n> (ex.:
GlpiPlugin\Butterfly\Config$1), então o link de config_page deve usar
rawurlencode('GlpiPlugin\Butterfly\Config') . '$1'.
Diagnóstico rápido (kernel bootado):
var_dump(isPluginItemType('\GlpiPlugin\Meuplugin\Config')); // false = problema
// e inspecionar CommonGLPI::$othertabs via Reflection para ver o que registrou
Regra para agentes
Ao portar plugin GLPI 9/10 → 11, fazer grep por: CodeMirror, .tabs(,
.dialog(, new .*FileVersion|__toString, '../plugins/,
registerClass('\\ (barra inicial), e conferir se todo
asset estático referenciado por URL está dentro de public/. Cada acerto
desses é quebra silenciosa (só aparece no console do browser ou como 404).
Classificação
- Tipo: gotchas de porte GLPI 10 → 11 (front-end/assets).
- Reutilização: obrigatória em qualquer porte de plugin com UI.