- KB-PLUGIN-034: cifrar credenciais externas em itemtype via GLPIKey (padrão prepareInput + senha-vazia-mantem-atual). Validado no plugin sync. - KB-PLUGIN-028: seção sobre o mecanismo menu_toadd (seções válidas + estrutura). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
567 lines
18 KiB
Markdown
567 lines
18 KiB
Markdown
---
|
|
id: KB-PLUGIN-028
|
|
title: GLPI 11 — Plugin com menu próprio precisa registrar rights via Profile
|
|
domain: plugin-dev
|
|
tags:
|
|
- glpi11
|
|
- plugin
|
|
- profile
|
|
- rights
|
|
- menu
|
|
- sidebar
|
|
- gotcha
|
|
status: active
|
|
severity: high
|
|
created_at: 2026-06-01
|
|
updated_at: 2026-06-01
|
|
applies_to:
|
|
- glpi-11
|
|
- estimate-plugin
|
|
- webapplications-plugin
|
|
related_records:
|
|
- KB-PLUGIN-013
|
|
---
|
|
|
|
# GLPI 11 — Plugin com menu próprio precisa registrar rights via Profile
|
|
|
|
## Sintoma
|
|
|
|
Plugin instalado, ativo e habilitado no marketplace, classes carregando corretamente via autoload, **mas o menu não aparece** na sidebar do GLPI (nem o item dentro de Gerência / Helpdesk / Administração).
|
|
|
|
Não há erro no log — o menu é simplesmente omitido silenciosamente.
|
|
|
|
## Causa raiz
|
|
|
|
GLPI executa `Session::haveRight($rightname, READ)` antes de renderizar qualquer entrada de menu de um itemtype. Se o **right não existir** na tabela `glpi_profilerights` (jamais foi cadastrado pelo plugin), `haveRight()` retorna `0` e o menu é ocultado.
|
|
|
|
Verificação:
|
|
```sql
|
|
SELECT COUNT(*) FROM glpi_profilerights WHERE name LIKE '%plugin_meuplugin%';
|
|
-- retorna 0 → você está com o problema
|
|
```
|
|
|
|
## Solução padrão GLPI 11
|
|
|
|
Plugins que expõem itemtypes próprios DEVEM ter uma classe `Profile` que:
|
|
1. Estende `\Profile`
|
|
2. Expõe `getAllRights()` listando os rights
|
|
3. Implementa `initProfile()` para registrar os rights em `glpi_profilerights`
|
|
4. Implementa `createFirstAccess($profile_id)` para conceder full access ao perfil que instalou
|
|
|
|
### Esqueleto da classe `src/Profile.php`
|
|
|
|
```php
|
|
<?php
|
|
namespace GlpiPlugin\Meuplugin;
|
|
|
|
use CommonGLPI;
|
|
use DbUtils;
|
|
use ProfileRight;
|
|
|
|
class Profile extends \Profile
|
|
{
|
|
public static $rightname = "profile";
|
|
|
|
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
|
|
{
|
|
if ($item->getType() === 'Profile' && $item->getField('interface') === 'central') {
|
|
return self::createTabEntry(MeuItemtype::getTypeName(2));
|
|
}
|
|
return '';
|
|
}
|
|
|
|
public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0)
|
|
{
|
|
if ($item->getType() === 'Profile') {
|
|
self::addDefaultProfileInfos($item->getID(), [
|
|
'plugin_meuplugin_x' => 0,
|
|
]);
|
|
(new self())->showForm($item->getID());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static function getAllRights($all = false): array
|
|
{
|
|
return [
|
|
[
|
|
'itemtype' => MeuItemtype::class,
|
|
'label' => MeuItemtype::getTypeName(2),
|
|
'field' => 'plugin_meuplugin_x',
|
|
],
|
|
];
|
|
}
|
|
|
|
public static function initProfile(): void
|
|
{
|
|
$dbu = new DbUtils();
|
|
foreach ((new self())->getAllRights(true) as $data) {
|
|
if ($dbu->countElementsInTable('glpi_profilerights', ['name' => $data['field']]) === 0) {
|
|
ProfileRight::addProfileRights([$data['field']]);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static function createFirstAccess($profiles_id): void
|
|
{
|
|
self::addDefaultProfileInfos($profiles_id, [
|
|
'plugin_meuplugin_x' => READ + CREATE + UPDATE + DELETE + PURGE,
|
|
], true);
|
|
}
|
|
|
|
public static function addDefaultProfileInfos($profiles_id, array $rights, bool $drop_existing = false): void
|
|
{
|
|
$dbu = new DbUtils();
|
|
$profileRight = new ProfileRight();
|
|
foreach ($rights as $name => $value) {
|
|
$exists = $dbu->countElementsInTable('glpi_profilerights', [
|
|
'profiles_id' => $profiles_id, 'name' => $name,
|
|
]) > 0;
|
|
if ($exists && $drop_existing) {
|
|
$profileRight->deleteByCriteria(['profiles_id' => $profiles_id, 'name' => $name]);
|
|
$exists = false;
|
|
}
|
|
if (!$exists) {
|
|
$profileRight->add([
|
|
'profiles_id' => $profiles_id,
|
|
'name' => $name,
|
|
'rights' => $value,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Registro em `setup.php`
|
|
|
|
```php
|
|
$PLUGIN_HOOKS['change_profile']['meuplugin'] = [
|
|
'GlpiPlugin\\Meuplugin\\Profile', 'initProfile'
|
|
];
|
|
|
|
Plugin::registerClass('GlpiPlugin\\Meuplugin\\Profile', [
|
|
'addtabon' => ['Profile']
|
|
]);
|
|
```
|
|
|
|
### Chamada no `hook.php` (install)
|
|
|
|
```php
|
|
function plugin_meuplugin_install(): bool
|
|
{
|
|
// ... criação de tabelas ...
|
|
|
|
\GlpiPlugin\Meuplugin\Profile::initProfile();
|
|
if (isset($_SESSION['glpiactiveprofile']['id'])) {
|
|
\GlpiPlugin\Meuplugin\Profile::createFirstAccess(
|
|
(int) $_SESSION['glpiactiveprofile']['id']
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
```
|
|
|
|
## Mecanismo do menu (menu_toadd) — como o itemtype entra na sidebar
|
|
|
|
Registrar o right faz o menu *poder* aparecer; para ele *de fato* aparecer, o
|
|
itemtype precisa ser injetado numa seção do menu via o hook `menu_toadd` (lido em
|
|
`Html::generateMenuSession()` / `Html.php`):
|
|
|
|
```php
|
|
// setup.php, dentro de plugin_init_<key>()
|
|
Plugin::registerClass('GlpiPlugin\\Meuplugin\\MeuItemtype');
|
|
$PLUGIN_HOOKS['menu_toadd']['meuplugin'] = [
|
|
'config' => 'GlpiPlugin\\Meuplugin\\MeuItemtype',
|
|
// ou vários: 'config' => ['Classe1', 'Classe2']
|
|
];
|
|
```
|
|
|
|
Seções válidas (chave do array): **`assets`, `helpdesk`, `management`, `tools`,
|
|
`admin`, `config`** (Setup), `plugins`. Itemtype administrativo de configuração
|
|
→ `config`; ver taxonomia em [KB-PLUGIN-029].
|
|
|
|
O `getMenuContent()` herdado de `CommonGLPI`/`CommonDBTM` já monta a entrada
|
|
(título, ícone, links search/add) a partir de `getTypeName()`, `getIcon()`,
|
|
`getSearchURL()` e `canCreate()` — **gated por `canView()`** (que depende do right).
|
|
Não precisa sobrescrever `getMenuContent()` para um CRUD simples.
|
|
|
|
## Gotcha — Instalação via CLI
|
|
|
|
`bin/console glpi:plugin:install -f <plugin>` roda **sem sessão GLPI**, então `$_SESSION['glpiactiveprofile']['id']` é `null` e `createFirstAccess()` não é executado. Resultado: rights são registrados na tabela mas com valor `0` (sem permissão real).
|
|
|
|
### Mitigação A — Web-first install
|
|
|
|
Recomendado: ativar o plugin pela primeira vez **via interface web** logado como Super-Admin. A sessão está ativa e o `createFirstAccess()` concede full access automaticamente.
|
|
|
|
### Mitigação B — Forçar via SQL após CLI install
|
|
|
|
Quando install foi via CLI e você precisa destravar rapidamente:
|
|
|
|
```sql
|
|
UPDATE glpi_profilerights
|
|
SET rights = 31 -- READ+CREATE+UPDATE+DELETE+PURGE
|
|
WHERE name IN ('plugin_meuplugin_x', 'plugin_meuplugin_y')
|
|
AND profiles_id IN (SELECT id FROM glpi_profiles WHERE interface='central');
|
|
```
|
|
|
|
Depois faça logout/login no GLPI pra a sessão recarregar os rights.
|
|
|
|
### Mitigação C — install.php que concede pra todos os admins
|
|
|
|
Pode-se estender o `install` pra dar full access automaticamente a todos os profiles com `interface='central'`:
|
|
|
|
```php
|
|
foreach ($DB->request(['FROM' => 'glpi_profiles', 'WHERE' => ['interface' => 'central']]) as $p) {
|
|
\GlpiPlugin\Meuplugin\Profile::createFirstAccess((int) $p['id']);
|
|
}
|
|
```
|
|
|
|
Mas isso pode ser intrusivo — perfis de Observer/Read-Only ganhariam create/delete por padrão. Avaliar caso a caso.
|
|
|
|
## Conflito de nomes — `Profile` colide com dropdown
|
|
|
|
Se o plugin já tem uma classe `Profile` pra outro conceito (ex: catálogo de perfis de executor, perfis de licenciamento, etc.), há colisão com a classe `Profile` exigida pra rights.
|
|
|
|
### Solução
|
|
|
|
Renomear a classe que NÃO é a de rights pra algo semanticamente mais claro. Exemplos do plugin Estimate:
|
|
- `Profile` (executor) → `ExecutorProfile`
|
|
- Tabela mantém `glpi_plugin_estimate_profiles` via override `getTable()`
|
|
|
|
```php
|
|
class ExecutorProfile extends CommonDropdown
|
|
{
|
|
public static function getTable($classname = null)
|
|
{
|
|
return 'glpi_plugin_estimate_profiles';
|
|
}
|
|
}
|
|
```
|
|
|
|
A classe `Profile` (extends `\Profile`) **sempre** fica reservada pra gestão de rights.
|
|
|
|
## Checklist de validação
|
|
|
|
Antes de jogar a culpa no menu/cache, confira:
|
|
|
|
```sql
|
|
-- 1. Right existe?
|
|
SELECT name, COUNT(*) c FROM glpi_profilerights
|
|
WHERE name LIKE '%plugin_meuplugin%' GROUP BY name;
|
|
|
|
-- 2. Profile atual tem o right ativo (>0)?
|
|
SELECT pr.name, pr.rights
|
|
FROM glpi_profilerights pr
|
|
JOIN glpi_profiles p ON p.id = pr.profiles_id
|
|
WHERE p.id = ? AND pr.name LIKE '%plugin_meuplugin%';
|
|
```
|
|
|
|
Se ambos retornam valores válidos (>0), o menu **vai aparecer** após logout/login (sessão precisa recarregar o cache de rights).
|
|
|
|
## Lições
|
|
|
|
1. **Menu silenciosamente oculto é sintoma clássico de right ausente.**
|
|
2. CLI install é parcial — sempre validar com web install ou SQL update.
|
|
3. Classe `Profile` (extends `\Profile`) é **convenção rígida** do GLPI — qualquer conceito de "perfil" no domínio do plugin precisa de outro nome.
|
|
4. Logout/login é necessário pra a sessão recarregar rights (não basta refresh).
|
|
|
|
## Bônus — Páginas `front/` no GLPI 11 NÃO usam `include('inc/includes.php')`
|
|
|
|
No GLPI 9/10, todo plugin começava com:
|
|
```php
|
|
include('../../../inc/includes.php');
|
|
```
|
|
|
|
No **GLPI 11 (Symfony)**, isso quebra porque:
|
|
|
|
1. O marketplace pode estar em `/var/glpi/marketplace/<plugin>/` (fora do tree do GLPI core que está em `/var/www/glpi/`)
|
|
2. `dirname(__DIR__, 3)` ou `../../../` resolve para path errado
|
|
3. O `LegacyFileLoadController` já bootou GLPI/autoload antes de invocar o arquivo
|
|
|
|
### Padrão correto GLPI 11
|
|
|
|
Começar direto sem include:
|
|
|
|
```php
|
|
<?php
|
|
Session::checkLoginUser();
|
|
|
|
$class = \GlpiPlugin\Meuplugin\Item::class;
|
|
|
|
Html::header(
|
|
\GlpiPlugin\Meuplugin\Item::getTypeName(2),
|
|
$_SERVER['PHP_SELF'],
|
|
'management',
|
|
$class
|
|
);
|
|
|
|
Search::show($class);
|
|
|
|
Html::footer();
|
|
```
|
|
|
|
Plugins de referência: `webapplications`, `splititil` (ambos no marketplace do GLPI 11).
|
|
|
|
### Sintoma quando inclui errado
|
|
|
|
```
|
|
include(): Failed opening '../../../inc/includes.php' for inclusion
|
|
(include_path='.:/usr/local/lib/php')
|
|
at <plugin>/front/<page>.php line 7
|
|
```
|
|
|
|
### Gotcha — Funções SQL agregadas no query builder do GLPI
|
|
|
|
`$DB->request()` é um query builder que **escapa tudo como nome de coluna** por padrão. Passar `'COUNT(*) AS cnt'` como string em `SELECT` gera SQL inválido:
|
|
|
|
```
|
|
MySQL query error: Unknown column 'COUNT(*)' in 'SELECT'
|
|
```
|
|
|
|
Porque a query final fica:
|
|
```sql
|
|
SELECT `plugin_estimate_states_id`, `COUNT(*)` AS `cnt` FROM ...
|
|
^^^^^^^^^^^^ escapado como coluna
|
|
```
|
|
|
|
**Solução:** usar `Glpi\DBAL\QueryExpression` pra funções SQL:
|
|
|
|
```php
|
|
use Glpi\DBAL\QueryExpression;
|
|
|
|
$DB->request([
|
|
'SELECT' => [
|
|
'plugin_estimate_states_id',
|
|
new QueryExpression('COUNT(*) AS ' . $DB->quoteName('cnt')),
|
|
],
|
|
'FROM' => self::getTable(),
|
|
'WHERE' => ['is_deleted' => 0],
|
|
'GROUPBY' => 'plugin_estimate_states_id',
|
|
]);
|
|
```
|
|
|
|
Vale pra `COUNT(*)`, `SUM()`, `AVG()`, `MAX()`, `MIN()`, `IF()`, `CASE WHEN`, etc. Sempre quotar nomes de coluna referenciados com `$DB->quoteName(...)`.
|
|
|
|
### Gotcha — `name` duplicado em templates Twig customizados
|
|
|
|
Quando você cria um template Twig pro form do itemtype e estende `generic_show_form.html.twig`, o GLPI **já renderiza automaticamente** os campos padrão (`name`, `entities_id`, datas). Adicionar `fields.textField('name', ...)` no bloco `more_fields` causa **duplicação**.
|
|
|
|
```twig
|
|
{# ERRADO — duplica o campo Nome #}
|
|
{% block more_fields %}
|
|
{{ fields.textField('name', item.fields['name'], __('Name')) }}
|
|
...
|
|
{% endblock %}
|
|
|
|
{# CERTO — só campos adicionais #}
|
|
{% block more_fields %}
|
|
{# 'name' renderizado pelo generic_show_form #}
|
|
{{ fields.dropdownField('Client', 'plugin_client_id', ...) }}
|
|
...
|
|
{% endblock %}
|
|
```
|
|
|
|
Outros campos auto-renderizados (não duplicar): `id`, `name`, `entities_id`, `is_recursive`, `date_creation`, `date_mod`.
|
|
|
|
### Internacionalização de plugin (gettext .po/.mo)
|
|
|
|
Plugins GLPI usam **gettext** com domínio por plugin. Cada `__('String', 'estimate')` busca em `<plugin_dir>/locales/<lang>.mo`.
|
|
|
|
#### Estrutura
|
|
|
|
```
|
|
estimate/
|
|
└── locales/
|
|
├── pt_BR.po ← fonte editável (UTF-8)
|
|
└── pt_BR.mo ← binário compilado (consumido pelo GLPI)
|
|
```
|
|
|
|
Sem prefixo de domínio no filename — convenção é só o código de idioma (`<lang>_<COUNTRY>.po/.mo`).
|
|
|
|
#### Workflow
|
|
|
|
```bash
|
|
# 1. Instalar gettext (pacote completo, não só -base)
|
|
apt-get install -y gettext # provê msgfmt
|
|
|
|
# 2. Editar locales/pt_BR.po (formato gettext padrão)
|
|
# 3. Compilar
|
|
cd marketplace/<plugin>/locales
|
|
msgfmt pt_BR.po -o pt_BR.mo
|
|
|
|
# 4. Limpar cache do GLPI
|
|
docker exec <container> find /var/glpi/files/_cache -name "locales" -type d -exec rm -rf {} +
|
|
```
|
|
|
|
#### Formato `.po`
|
|
|
|
```
|
|
msgid ""
|
|
msgstr ""
|
|
"Language: pt_BR\n"
|
|
"Content-Type: text/plain; charset=UTF-8\n"
|
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
|
|
# Termo singular
|
|
msgid "Estimate"
|
|
msgstr "Estimativa"
|
|
|
|
# Termo plural (suporta nplurals)
|
|
msgid "Item"
|
|
msgid_plural "Items"
|
|
msgstr[0] "Item"
|
|
msgstr[1] "Itens"
|
|
|
|
# Termo com placeholder
|
|
msgid "Add %s"
|
|
msgstr "Adicionar %s"
|
|
```
|
|
|
|
#### Carregamento automático
|
|
|
|
O GLPI 11 chama `Plugin::loadLang('<plugin>')` no boot — não precisa código adicional. Basta o `.mo` estar em `locales/<lang>.mo` e o usuário ter `glpilanguage` setado.
|
|
|
|
#### Validar tradução via console
|
|
|
|
```bash
|
|
docker exec <container> php -r "
|
|
chdir('/var/www/glpi');
|
|
require 'vendor/autoload.php';
|
|
\$k = new Glpi\Kernel\Kernel('production', false);
|
|
\$k->boot();
|
|
\$_SESSION['glpilanguage'] = 'pt_BR';
|
|
\Session::loadLanguage();
|
|
\Plugin::loadLang('<plugin>');
|
|
echo __('Total hours', '<plugin>').PHP_EOL;
|
|
"
|
|
```
|
|
|
|
#### Termos que GLPI core já traduz (não precisa repetir no plugin)
|
|
|
|
Estes são traduzidos pelo `.mo` do GLPI core. Use `__('Name')` (sem 2º arg) e herda:
|
|
|
|
`Name`, `Description`, `Status`, `Category`, `Color`, `Date`, `Hours`, `Quantity`,
|
|
`Currency`, `Document`, `Documents`, `Notes`, `Historical`, `Profile`, `User`,
|
|
`Group`, `Entity`, `Add`, `Save`, `Delete`, `Cancel`, e a maioria dos verbos/labels comuns.
|
|
|
|
### Constantes úteis disponíveis no front (já definidas)
|
|
|
|
| Constante | Valor típico no container | Uso |
|
|
|---|---|---|
|
|
| `GLPI_ROOT` | `/var/www/glpi` | Path do GLPI core |
|
|
| `GLPI_MARKETPLACE_DIR` | `/var/glpi/marketplace` | Onde plugins de marketplace ficam (pode diferir de `GLPI_ROOT`) |
|
|
| `GLPI_CONFIG_DIR` | `/var/glpi/config` | Config + chaves OAuth |
|
|
| `GLPI_PLUGIN_DOC_DIR` | `/var/glpi/files/_plugins` | Storage de arquivos por plugin |
|
|
|
|
## Bônus 2 — Sidebar de tabs no form do itemtype
|
|
|
|
GLPI exibe um menu lateral de tabs em cada itemtype (Documento, Itens associados, Notas, Histórico, etc.). Para um plugin replicar isso:
|
|
|
|
### Pai (Estimate.php — itemtype principal) — implementa `defineTabs()`
|
|
|
|
```php
|
|
public function defineTabs($options = [])
|
|
{
|
|
$ong = [];
|
|
$this->addDefaultFormTab($ong); // form principal (campos do item)
|
|
$this->addStandardTab(EstimateItem::class, $ong, $options); // tab "Itens" (filho custom)
|
|
$this->addStandardTab('Document_Item', $ong, $options); // anexos nativos
|
|
$this->addStandardTab('Notepad', $ong, $options); // notas nativas
|
|
$this->addStandardTab('Log', $ong, $options); // histórico nativo
|
|
return $ong;
|
|
}
|
|
```
|
|
|
|
### Filho (EstimateItem.php — entidade que aparece como tab no pai)
|
|
|
|
```php
|
|
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
|
|
{
|
|
if ($item instanceof Estimate) {
|
|
$count = (new DbUtils())->countElementsInTable(self::getTable(), [
|
|
self::$items_id => $item->getID(),
|
|
]);
|
|
return self::createTabEntry(self::getTypeName(2), $count);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0)
|
|
{
|
|
if ($item instanceof Estimate) {
|
|
self::showForEstimate($item); // sua função de renderização
|
|
}
|
|
return true;
|
|
}
|
|
```
|
|
|
|
### Registro em `setup.php`
|
|
|
|
```php
|
|
Plugin::registerClass('GlpiPlugin\\Estimate\\EstimateItem', [
|
|
'addtabon' => ['GlpiPlugin\\Estimate\\Estimate']
|
|
]);
|
|
```
|
|
|
|
`addtabon` informa ao GLPI que esse itemtype deve aparecer como tab nos itemtypes listados.
|
|
|
|
### Tabs nativos que "saem de graça"
|
|
|
|
| Tab nativo | Class GLPI | O que faz |
|
|
|---|---|---|
|
|
| Documentos | `Document_Item` | Anexar arquivos ao itemtype |
|
|
| Notas | `Notepad` | Notas privadas do usuário |
|
|
| Histórico | `Log` | Audit log automático de mudanças |
|
|
| Reservas | `Reservation` | Reservar item por período (CIs) |
|
|
| Itens associados | `KnowbaseItem_Item` ou `Item_Devices` | Itens relacionados |
|
|
|
|
### Gotcha — `count_on_tabs`
|
|
|
|
Mostrar contador na label da tab depende de `$_SESSION['glpishow_count_on_tabs']` (config GLPI). Sempre testar com `?? true`:
|
|
|
|
```php
|
|
if ($_SESSION['glpishow_count_on_tabs'] ?? true) {
|
|
$count = ...;
|
|
}
|
|
```
|
|
|
|
### Gotcha — Tabs nativos exigem rights
|
|
|
|
`Document_Item`, `Notepad`, `Log` etc. fazem check de `canView()` no usuário corrente. No CLI (`bin/console`) a sessão não existe → `addStandardTab` retorna sem adicionar. Validar tabs sempre no **browser logado**, não no console.
|
|
|
|
### Linkar itemtype a Ticket / Project (rastreabilidade bidirecional)
|
|
|
|
Para que seu plugin apareça nos dropdowns "tipo de item" ao **adicionar associação** num Ticket ou Project, precisa adicionar ao `$CFG_GLPI` em `plugin_init_<plugin>()`:
|
|
|
|
```php
|
|
global $CFG_GLPI;
|
|
$CFG_GLPI['ticket_types'][] = 'GlpiPlugin\\Meuplugin\\MeuItemtype';
|
|
$CFG_GLPI['project_asset_types'][] = 'GlpiPlugin\\Meuplugin\\MeuItemtype';
|
|
```
|
|
|
|
E pra o tab **inverso** (Meu Itemtype aparecer como tab em Ticket/Project), usar `addtabon`:
|
|
|
|
```php
|
|
Plugin::registerClass('GlpiPlugin\\Meuplugin\\MeuItemtype', [
|
|
'addtabon' => ['Ticket', 'Project']
|
|
]);
|
|
```
|
|
|
|
E no `defineTabs()` do seu itemtype, adicionar os tabs nativos:
|
|
|
|
```php
|
|
$this->addStandardTab('Item_Ticket', $ong, $options);
|
|
$this->addStandardTab('Item_Project', $ong, $options);
|
|
```
|
|
|
|
Arrays úteis em `$CFG_GLPI`:
|
|
|
|
| Array | Para que serve |
|
|
|---|---|
|
|
| `ticket_types` | Item pode ser associado a Ticket via `Item_Ticket` |
|
|
| `project_asset_types` | Item pode ser associado a Project via `Item_Project` |
|
|
| `asset_types` | Item aparece como "ativo" em listagens genéricas |
|
|
| `link_types` | Item pode ser link de relacionamento via `Link_Itemtype` |
|
|
| `document_types` | Item pode receber Documents anexados via `Document_Item` |
|
|
| `state_types` | Item pode ter `states_id` (estado do ativo) |
|