Redmine/inc/config.class.php
Gemini 48600b9692 feat(2.0): dicionário "de → para" por operação + variáveis de autor
Feedback de UX: o admin precisa saber O QUE o Redmine espera, não só os
valores. Cada card do editor ganha a tabela colapsável "Campos que o
Redmine espera" (campos padrão da operação + custom fields do ambiente,
com obrigatoriedade e valores possíveis) — clicar num campo adiciona a
linha com o caminho pronto. A paleta vira explicitamente o "DE" (variáveis
do GLPI) e ganha chamado.tecnico e tarefa.autor (texto; autoria real segue
automática via impersonation). Sync passa a cachear custom_fields de todos
os tipos (issue/time_entry/project).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:00:19 -03:00

250 lines
9.7 KiB
PHP

<?php
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
class PluginRedmineConfig extends CommonDBTM {
/**
* Define the tab name
* @param int $nb
* @return string
*/
static function getTypeName($nb = 0) {
return 'Redmine Integration';
}
/**
* Encrypt the API key before persisting it.
* An empty key on update means "keep the current one".
* @param array $input
* @return array
*/
function prepareInputForUpdate($input) {
if (array_key_exists('api_key', $input)) {
if ($input['api_key'] === '' || $input['api_key'] === null) {
unset($input['api_key']); // keep the stored key
} else {
$input['api_key'] = (new \GLPIKey())->encrypt((string) $input['api_key']);
}
}
// The status map is posted as status_map[<glpi_status>] = <redmine_status_id>.
if (isset($input['status_map']) && is_array($input['status_map'])) {
$input['status_map'] = json_encode($input['status_map']);
}
// Default watchers come from a multiselect (array of Redmine user ids).
if (isset($input['default_watcher_ids'])) {
$ids = is_array($input['default_watcher_ids']) ? $input['default_watcher_ids'] : [];
$input['default_watcher_ids'] = json_encode(array_values(array_filter(array_map('intval', $ids))));
}
return $input;
}
/**
* Return the (single) config row.
* @return array
*/
public static function getConfigRow() {
global $DB;
$it = $DB->request(['FROM' => 'glpi_plugin_redmine_configs', 'WHERE' => ['id' => 1]]);
return count($it) > 0 ? $it->current() : [];
}
/**
* Resolve the Redmine user for a GLPI user.
* Priority: manual override (usermap) -> e-mail auto-match against the cache.
* @param int $glpi_users_id
* @return array|null Redmine user cache row [redmine_id, login, mail, name] or null
*/
public static function resolveRedmineUser($glpi_users_id) {
global $DB;
$glpi_users_id = (int) $glpi_users_id;
if ($glpi_users_id <= 0) {
return null;
}
// 1) Manual override
$ov = $DB->request([
'FROM' => 'glpi_plugin_redmine_usermap',
'WHERE' => ['users_id' => $glpi_users_id]
]);
if (count($ov) > 0) {
$rid = (int) $ov->current()['redmine_user_id'];
$ru = $DB->request(['FROM' => 'glpi_plugin_redmine_users', 'WHERE' => ['redmine_id' => $rid]]);
if (count($ru) > 0) {
return $ru->current();
}
}
// 2) E-mail auto-match (cache local)
$email = \UserEmail::getDefaultForUser($glpi_users_id);
if (!empty($email)) {
$ru = $DB->request(['FROM' => 'glpi_plugin_redmine_users', 'WHERE' => ['mail' => $email]]);
if (count($ru) > 0) {
return $ru->current();
}
// 3) Self-healing: cache pode estar vazio/desatualizado — busca ao
// vivo no Redmine e grava no cache para as próximas resoluções.
$live = PluginRedmineRedmineapi::findUserByEmail($email);
if ($live && !empty($live['login'])) {
$row = [
'redmine_id' => (int) $live['id'],
'login' => $live['login'],
'mail' => $live['mail'] ?? $email,
'name' => trim(($live['firstname'] ?? '') . ' ' . ($live['lastname'] ?? '')),
];
$DB->delete('glpi_plugin_redmine_users', ['redmine_id' => $row['redmine_id']]);
$DB->insert('glpi_plugin_redmine_users', $row);
Toolbox::logInFile('redmine', "Cache de usuários atualizado ao vivo para '{$email}' -> {$row['login']}.\n");
return $row;
}
}
return null;
}
/**
* @return int default Redmine role id used for auto-membership
*/
public static function getDefaultRoleId() {
return (int) (self::getConfigRow()['default_role_id'] ?? 0);
}
/**
* @return int[] configured default watcher Redmine user ids
*/
public static function getDefaultWatcherIds() {
$raw = self::getConfigRow()['default_watcher_ids'] ?? '';
if (empty($raw)) {
return [];
}
$ids = json_decode($raw, true);
return is_array($ids) ? array_values(array_map('intval', $ids)) : [];
}
/**
* Map a GLPI ticket status to the configured Redmine status id.
* @param int $glpi_status
* @return int|null
*/
public static function mapTicketStatus($glpi_status) {
$row = self::getConfigRow();
if (empty($row['status_map'])) {
return null;
}
$map = json_decode($row['status_map'], true);
if (!is_array($map) || empty($map[$glpi_status])) {
return null;
}
return (int) $map[$glpi_status];
}
/**
* Build a {id: name} options map from a Redmine metadata list.
* @param array $list
* @return array
*/
private static function toOptions($list) {
$opt = [];
foreach (($list ?? []) as $el) {
if (isset($el['id'], $el['name'])) {
$opt[$el['id']] = $el['name'];
}
}
return $opt;
}
/**
* Show config form
* @param int $ID
* @param array $options
* @return boolean
*/
function showForm($ID, array $options = []) {
global $DB;
// Fetch config
$config = [];
$iterator = $DB->request([
'FROM' => 'glpi_plugin_redmine_configs',
'WHERE' => ['id' => 1]
]);
if (count($iterator) > 0) {
$config = $iterator->current();
}
$metadata = !empty($config['metadata_cache']) ? json_decode($config['metadata_cache'], true) : [];
$status_map = !empty($config['status_map']) ? json_decode($config['status_map'], true) : [];
$watcher_ids = self::getDefaultWatcherIds();
// Redmine users (from cache) -> "Nome (login)" options
$redmine_user_options = [];
foreach ($DB->request(['FROM' => 'glpi_plugin_redmine_users', 'ORDER' => 'name']) as $u) {
$redmine_user_options[$u['redmine_id']] = trim($u['name']) . ' (' . $u['login'] . ')';
}
// Existing manual overrides (GLPI user -> Redmine user) with display names
$usermap = [];
foreach ($DB->request(['FROM' => 'glpi_plugin_redmine_usermap']) as $row) {
$usermap[] = [
'id' => $row['id'],
'users_id' => $row['users_id'],
'glpi_name' => getUserName($row['users_id']),
'redmine_name' => $redmine_user_options[$row['redmine_user_id']] ?? ('#' . $row['redmine_user_id']),
];
}
// Templates de payload (motor 2.0) + paleta de variáveis
$tpl_labels = [
'create_issue' => __('Criar tarefa (issue)', 'redmine'),
'log_time' => __('Lançar tempo', 'redmine'),
'update_status' => __('Atualizar status', 'redmine'),
'create_project' => __('Criar projeto', 'redmine'),
];
$templates = [];
foreach (PluginRedmineTemplateengine::OPERATIONS as $op) {
$row = PluginRedmineTemplateengine::getRow($op);
$templates[$op] = [
'label' => $tpl_labels[$op] ?? $op,
'template' => (string) ($row['template'] ?? ''),
'is_active' => (int) ($row['is_active'] ?? 0),
];
}
// Dicionário "PARA" por operação: campos que o Redmine espera/aceita
$field_dicts = [];
foreach (PluginRedmineTemplateengine::OPERATIONS as $op) {
$field_dicts[$op] = PluginRedmineTemplateengine::fieldDictionary($op);
}
\Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/config_form.html.twig', [
'templates' => $templates,
'palette_vars' => PluginRedmineTemplateengine::variableCatalog(),
'field_dicts' => $field_dicts,
'action_url' => Toolbox::getItemTypeFormURL(__CLASS__),
'csrf_token_value' => Session::getNewCSRFToken(),
'redmine_url' => $config['redmine_url'] ?? '',
'has_key' => !empty($config['api_key']),
'has_metadata' => !empty($metadata['trackers']) || !empty($metadata['statuses']),
'tracker_options' => self::toOptions($metadata['trackers'] ?? []),
'priority_options' => self::toOptions($metadata['priorities'] ?? []),
'activity_options' => self::toOptions($metadata['activities'] ?? []),
'status_options' => self::toOptions($metadata['statuses'] ?? []),
'role_options' => self::toOptions($metadata['roles'] ?? []),
'redmine_user_options' => $redmine_user_options,
'default_tracker_id' => (int) ($config['default_tracker_id'] ?? 0),
'default_priority_id' => (int) ($config['default_priority_id'] ?? 0),
'default_activity_id' => (int) ($config['default_activity_id'] ?? 0),
'default_role_id' => (int) ($config['default_role_id'] ?? 0),
'default_watcher_ids' => $watcher_ids,
'status_map' => is_array($status_map) ? $status_map : [],
'glpi_ticket_statuses' => \Ticket::getAllStatusArray(),
'usermap' => $usermap,
'user_cache_count' => count($redmine_user_options),
]);
return true;
}
}