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>
250 lines
9.7 KiB
PHP
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;
|
|
}
|
|
}
|