Lint / PHP (phpcs PSR-12) (push) Successful in 31s
Lint / JS (eslint) (push) Successful in 13s
Lint / PHP requirements (version + extensions) (push) Successful in 39s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 2m1s
Lint / Deploy (push) Successful in 6s
The setValue()/is_required/select-options half of this issue was already fixed incidentally by #47's new api/ticket_custom_fields.php endpoint. The remaining gap: createDefinition()/updateDefinition() never validated field_type against the six values the schema's enum() actually allows (text/textarea/select/checkbox/date/number), so a malformed type could be stored via the admin API and break whatever UI renders it later. Added an ALLOWED_FIELD_TYPES allowlist check at the top of both methods, returning the same ['success' => false, 'error' => ...] shape they already use for a DB failure — api/custom_fields.php already propagates that shape correctly with no changes needed there. Verified against real MariaDB: an invalid field_type is rejected on both create and update, while a valid one still succeeds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv
263 lines
7.6 KiB
PHP
263 lines
7.6 KiB
PHP
<?php
|
|
|
|
/**
|
|
* CustomFieldModel - Manages custom field definitions and values
|
|
*/
|
|
|
|
class CustomFieldModel
|
|
{
|
|
private $conn;
|
|
|
|
// Must match custom_field_definitions.field_type's enum() in the schema.
|
|
private const ALLOWED_FIELD_TYPES = ['text', 'textarea', 'select', 'checkbox', 'date', 'number'];
|
|
|
|
public function __construct($conn)
|
|
{
|
|
$this->conn = $conn;
|
|
}
|
|
|
|
// ========================================
|
|
// Field Definitions
|
|
// ========================================
|
|
|
|
/**
|
|
* Get all field definitions
|
|
*/
|
|
public function getAllDefinitions($category = null, $activeOnly = true)
|
|
{
|
|
$sql = "SELECT * FROM custom_field_definitions WHERE 1=1";
|
|
$params = [];
|
|
$types = '';
|
|
|
|
if ($activeOnly) {
|
|
$sql .= " AND is_active = 1";
|
|
}
|
|
|
|
if ($category !== null) {
|
|
$sql .= " AND (category = ? OR category IS NULL)";
|
|
$params[] = $category;
|
|
$types .= 's';
|
|
}
|
|
|
|
$sql .= " ORDER BY display_order ASC, field_id ASC";
|
|
|
|
if (!empty($params)) {
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param($types, ...$params);
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
} else {
|
|
$result = $this->conn->query($sql);
|
|
}
|
|
|
|
$fields = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
if ($row['field_options']) {
|
|
$row['field_options'] = json_decode($row['field_options'], true);
|
|
}
|
|
$fields[] = $row;
|
|
}
|
|
|
|
if (isset($stmt)) {
|
|
$stmt->close();
|
|
}
|
|
|
|
return $fields;
|
|
}
|
|
|
|
/**
|
|
* Get a single field definition
|
|
*/
|
|
public function getDefinition($fieldId)
|
|
{
|
|
$sql = "SELECT * FROM custom_field_definitions WHERE field_id = ?";
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param('i', $fieldId);
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
$row = $result->fetch_assoc();
|
|
$stmt->close();
|
|
|
|
if ($row && $row['field_options']) {
|
|
$row['field_options'] = json_decode($row['field_options'], true);
|
|
}
|
|
|
|
return $row;
|
|
}
|
|
|
|
/**
|
|
* Create a new field definition
|
|
*/
|
|
public function createDefinition($data)
|
|
{
|
|
if (!in_array($data['field_type'] ?? '', self::ALLOWED_FIELD_TYPES, true)) {
|
|
return ['success' => false, 'error' => 'Invalid field_type'];
|
|
}
|
|
|
|
$options = null;
|
|
if (isset($data['field_options']) && !empty($data['field_options'])) {
|
|
$options = json_encode($data['field_options']);
|
|
}
|
|
|
|
$sql = "INSERT INTO custom_field_definitions
|
|
(field_name, field_label, field_type, field_options, category, is_required, display_order, is_active)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
|
|
|
$isRequired = $data['is_required'] ?? 0;
|
|
$displayOrder = $data['display_order'] ?? 0;
|
|
$isActive = $data['is_active'] ?? 1;
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param(
|
|
'sssssiii',
|
|
$data['field_name'],
|
|
$data['field_label'],
|
|
$data['field_type'],
|
|
$options,
|
|
$data['category'],
|
|
$isRequired,
|
|
$displayOrder,
|
|
$isActive
|
|
);
|
|
|
|
if ($stmt->execute()) {
|
|
$id = $this->conn->insert_id;
|
|
$stmt->close();
|
|
return ['success' => true, 'field_id' => $id];
|
|
}
|
|
|
|
$error = $stmt->error;
|
|
$stmt->close();
|
|
return ['success' => false, 'error' => $error];
|
|
}
|
|
|
|
/**
|
|
* Update a field definition
|
|
*/
|
|
public function updateDefinition($fieldId, $data)
|
|
{
|
|
if (!in_array($data['field_type'] ?? '', self::ALLOWED_FIELD_TYPES, true)) {
|
|
return ['success' => false, 'error' => 'Invalid field_type'];
|
|
}
|
|
|
|
$options = null;
|
|
if (isset($data['field_options']) && !empty($data['field_options'])) {
|
|
$options = json_encode($data['field_options']);
|
|
}
|
|
|
|
$sql = "UPDATE custom_field_definitions SET
|
|
field_name = ?, field_label = ?, field_type = ?, field_options = ?,
|
|
category = ?, is_required = ?, display_order = ?, is_active = ?
|
|
WHERE field_id = ?";
|
|
|
|
$isRequired = $data['is_required'] ?? 0;
|
|
$displayOrder = $data['display_order'] ?? 0;
|
|
$isActive = $data['is_active'] ?? 1;
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param(
|
|
'sssssiiii',
|
|
$data['field_name'],
|
|
$data['field_label'],
|
|
$data['field_type'],
|
|
$options,
|
|
$data['category'],
|
|
$isRequired,
|
|
$displayOrder,
|
|
$isActive,
|
|
$fieldId
|
|
);
|
|
|
|
$success = $stmt->execute();
|
|
$stmt->close();
|
|
return ['success' => $success];
|
|
}
|
|
|
|
/**
|
|
* Delete a field definition
|
|
*/
|
|
public function deleteDefinition($fieldId)
|
|
{
|
|
// This will cascade delete all values due to FK constraint
|
|
$sql = "DELETE FROM custom_field_definitions WHERE field_id = ?";
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param('i', $fieldId);
|
|
$success = $stmt->execute();
|
|
$stmt->close();
|
|
return ['success' => $success];
|
|
}
|
|
|
|
// ========================================
|
|
// Field Values
|
|
// ========================================
|
|
|
|
/**
|
|
* Get all field values for a ticket
|
|
*/
|
|
public function getValuesForTicket($ticketId)
|
|
{
|
|
$sql = "SELECT cfv.*, cfd.field_name, cfd.field_label, cfd.field_type, cfd.field_options
|
|
FROM custom_field_values cfv
|
|
JOIN custom_field_definitions cfd ON cfv.field_id = cfd.field_id
|
|
WHERE cfv.ticket_id = ?
|
|
ORDER BY cfd.display_order ASC";
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param('s', $ticketId);
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
|
|
$values = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
if ($row['field_options']) {
|
|
$row['field_options'] = json_decode($row['field_options'], true);
|
|
}
|
|
$values[$row['field_name']] = $row;
|
|
}
|
|
|
|
$stmt->close();
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Set a field value for a ticket (insert or update)
|
|
*/
|
|
public function setValue($ticketId, $fieldId, $value)
|
|
{
|
|
$sql = "INSERT INTO custom_field_values (ticket_id, field_id, field_value)
|
|
VALUES (?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE field_value = VALUES(field_value), updated_at = CURRENT_TIMESTAMP";
|
|
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param('sis', $ticketId, $fieldId, $value);
|
|
$success = $stmt->execute();
|
|
$stmt->close();
|
|
return ['success' => $success];
|
|
}
|
|
|
|
/**
|
|
* Set multiple field values for a ticket
|
|
*/
|
|
public function setValues($ticketId, $values)
|
|
{
|
|
$results = [];
|
|
foreach ($values as $fieldId => $value) {
|
|
$results[$fieldId] = $this->setValue($ticketId, $fieldId, $value);
|
|
}
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Delete all field values for a ticket
|
|
*/
|
|
public function deleteValuesForTicket($ticketId)
|
|
{
|
|
$sql = "DELETE FROM custom_field_values WHERE ticket_id = ?";
|
|
$stmt = $this->conn->prepare($sql);
|
|
$stmt->bind_param('s', $ticketId);
|
|
$success = $stmt->execute();
|
|
$stmt->close();
|
|
return ['success' => $success];
|
|
}
|
|
}
|