- Bump DESK_MOLONI version to 3.0.1 across module - Normalize hooks to after_client_* and instantiate PerfexHooks safely - Fix OAuthController view path and API client class name - Add missing admin views for webhook config/logs; adjust view loading - Harden client portal routes and admin routes mapping - Make Dashboard/Logs/Queue tolerant to optional model methods - Align log details query with existing schema; avoid broken joins This makes the module operational in Perfex (admin + client), reduces 404s, and avoids fatal errors due to inconsistent tables/methods.
354 lines
10 KiB
PHP
354 lines
10 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Desk_moloni_model.php
|
|
*
|
|
* Base model for Desk-Moloni v3.0 integration
|
|
* Provides common functionality for all Desk-Moloni models
|
|
*
|
|
* @package DeskMoloni\Models
|
|
* @author Database Design Specialist
|
|
* @version 3.0
|
|
*/
|
|
|
|
defined('BASEPATH') or exit('No direct script access allowed');
|
|
|
|
class Desk_moloni_model extends App_Model
|
|
{
|
|
/**
|
|
* AES-256-GCM encryption key (should be stored securely in config)
|
|
*/
|
|
private $encryptionKey;
|
|
|
|
/**
|
|
* Table prefix for all Desk-Moloni tables (follows Perfex CRM convention)
|
|
*/
|
|
protected $tablePrefix = 'tbldeskmoloni_';
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
|
|
// Load encryption library
|
|
$this->load->library('encryption');
|
|
|
|
// Initialize encryption key (should be from secure config)
|
|
$this->encryptionKey = $this->getEncryptionKey();
|
|
|
|
// Load database
|
|
$this->load->database();
|
|
}
|
|
|
|
/**
|
|
* Get secure encryption key
|
|
*
|
|
* @return string
|
|
*/
|
|
private function getEncryptionKey()
|
|
{
|
|
// In production, this should come from secure configuration
|
|
// For now, using app key with salt
|
|
$appKey = get_option('encryption_key') ?: 'desk_moloni_default_key';
|
|
return hash('sha256', $appKey . 'desk_moloni_salt_v3', true);
|
|
}
|
|
|
|
/**
|
|
* Encrypt sensitive data using AES-256-GCM
|
|
*
|
|
* @param string $data Data to encrypt
|
|
* @return string Encrypted data with nonce
|
|
*/
|
|
protected function encryptData($data)
|
|
{
|
|
if (empty($data)) {
|
|
return $data;
|
|
}
|
|
|
|
try {
|
|
// Generate random nonce
|
|
$nonce = random_bytes(12); // 96-bit nonce for GCM
|
|
|
|
// Encrypt data
|
|
$encrypted = openssl_encrypt(
|
|
$data,
|
|
'aes-256-gcm',
|
|
$this->encryptionKey,
|
|
OPENSSL_RAW_DATA,
|
|
$nonce,
|
|
$tag
|
|
);
|
|
|
|
if ($encrypted === false) {
|
|
throw new Exception('Encryption failed');
|
|
}
|
|
|
|
// Combine nonce + tag + encrypted data and base64 encode
|
|
return base64_encode($nonce . $tag . $encrypted);
|
|
|
|
} catch (Exception $e) {
|
|
log_message('error', 'Desk-Moloni encryption error: ' . $e->getMessage());
|
|
throw new Exception('Failed to encrypt sensitive data');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypt sensitive data using AES-256-GCM
|
|
*
|
|
* @param string $encryptedData Encrypted data with nonce
|
|
* @return string Decrypted data
|
|
*/
|
|
protected function decryptData($encryptedData)
|
|
{
|
|
if (empty($encryptedData)) {
|
|
return $encryptedData;
|
|
}
|
|
|
|
try {
|
|
// Decode base64
|
|
$data = base64_decode($encryptedData);
|
|
|
|
if ($data === false || strlen($data) < 28) { // 12 + 16 + at least some data
|
|
throw new Exception('Invalid encrypted data format');
|
|
}
|
|
|
|
// Extract components
|
|
$nonce = substr($data, 0, 12);
|
|
$tag = substr($data, 12, 16);
|
|
$encrypted = substr($data, 28);
|
|
|
|
// Decrypt data
|
|
$decrypted = openssl_decrypt(
|
|
$encrypted,
|
|
'aes-256-gcm',
|
|
$this->encryptionKey,
|
|
OPENSSL_RAW_DATA,
|
|
$nonce,
|
|
$tag
|
|
);
|
|
|
|
if ($decrypted === false) {
|
|
throw new Exception('Decryption failed - data may be corrupted');
|
|
}
|
|
|
|
return $decrypted;
|
|
|
|
} catch (Exception $e) {
|
|
log_message('error', 'Desk-Moloni decryption error: ' . $e->getMessage());
|
|
throw new Exception('Failed to decrypt sensitive data');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate JSON data
|
|
*
|
|
* @param string $jsonString JSON string to validate
|
|
* @return bool True if valid JSON
|
|
*/
|
|
protected function validateJSON($jsonString)
|
|
{
|
|
if ($jsonString === null || $jsonString === '') {
|
|
return true; // NULL and empty strings are valid
|
|
}
|
|
|
|
json_decode($jsonString);
|
|
return json_last_error() === JSON_ERROR_NONE;
|
|
}
|
|
|
|
/**
|
|
* Validate ENUM values
|
|
*
|
|
* @param string $value Value to validate
|
|
* @param array $allowedValues Array of allowed ENUM values
|
|
* @return bool True if value is valid
|
|
*/
|
|
protected function validateEnum($value, $allowedValues)
|
|
{
|
|
return in_array($value, $allowedValues, true);
|
|
}
|
|
|
|
/**
|
|
* Get table name with prefix
|
|
*
|
|
* @param string $tableSuffix Table suffix (e.g., 'config', 'mapping')
|
|
* @return string Full table name
|
|
*/
|
|
protected function getTableName($tableSuffix)
|
|
{
|
|
return $this->tablePrefix . $tableSuffix;
|
|
}
|
|
|
|
/**
|
|
* Log database operations for audit trail
|
|
*
|
|
* @param string $operation Operation type (create, update, delete)
|
|
* @param string $table Table name
|
|
* @param array $data Operation data
|
|
* @param int|null $recordId Record ID if applicable
|
|
*/
|
|
protected function logDatabaseOperation($operation, $table, $data, $recordId = null)
|
|
{
|
|
try {
|
|
$logData = [
|
|
'operation' => $operation,
|
|
'table_name' => $table,
|
|
'record_id' => $recordId,
|
|
'data_snapshot' => json_encode($data),
|
|
'user_id' => get_staff_user_id(),
|
|
'ip_address' => $this->input->ip_address(),
|
|
'user_agent' => $this->input->user_agent(),
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
// Insert into audit log (if table exists)
|
|
if ($this->db->table_exists($this->getTableName('audit_log'))) {
|
|
$this->db->insert($this->getTableName('audit_log'), $logData);
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
// Don't fail the main operation if logging fails
|
|
log_message('error', 'Desk-Moloni audit log error: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate required fields
|
|
*
|
|
* @param array $data Data to validate
|
|
* @param array $requiredFields Required field names
|
|
* @return array Validation errors (empty if valid)
|
|
*/
|
|
protected function validateRequiredFields($data, $requiredFields)
|
|
{
|
|
$errors = [];
|
|
|
|
foreach ($requiredFields as $field) {
|
|
if (!isset($data[$field]) || $data[$field] === '' || $data[$field] === null) {
|
|
$errors[] = "Field '{$field}' is required";
|
|
}
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
/**
|
|
* Validate field lengths
|
|
*
|
|
* @param array $data Data to validate
|
|
* @param array $fieldLimits Field length limits ['field' => max_length]
|
|
* @return array Validation errors
|
|
*/
|
|
protected function validateFieldLengths($data, $fieldLimits)
|
|
{
|
|
$errors = [];
|
|
|
|
foreach ($fieldLimits as $field => $maxLength) {
|
|
if (isset($data[$field]) && strlen($data[$field]) > $maxLength) {
|
|
$errors[] = "Field '{$field}' exceeds maximum length of {$maxLength} characters";
|
|
}
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
/**
|
|
* Sanitize data for database insertion
|
|
*
|
|
* @param array $data Data to sanitize
|
|
* @return array Sanitized data
|
|
*/
|
|
protected function sanitizeData($data)
|
|
{
|
|
$sanitized = [];
|
|
|
|
foreach ($data as $key => $value) {
|
|
if (is_string($value)) {
|
|
// Trim whitespace and sanitize
|
|
$sanitized[$key] = trim($value);
|
|
} else {
|
|
$sanitized[$key] = $value;
|
|
}
|
|
}
|
|
|
|
return $sanitized;
|
|
}
|
|
|
|
/**
|
|
* Check if table exists
|
|
*
|
|
* @param string $tableName Table name to check
|
|
* @return bool True if table exists
|
|
*/
|
|
protected function tableExists($tableName)
|
|
{
|
|
return $this->db->table_exists($tableName);
|
|
}
|
|
|
|
/**
|
|
* Execute transaction with rollback on failure
|
|
*
|
|
* @param callable $callback Function to execute in transaction
|
|
* @return mixed Result of callback or false on failure
|
|
*/
|
|
protected function executeTransaction($callback)
|
|
{
|
|
$this->db->trans_begin();
|
|
|
|
try {
|
|
$result = $callback();
|
|
|
|
if ($this->db->trans_status() === false) {
|
|
throw new Exception('Transaction failed');
|
|
}
|
|
|
|
$this->db->trans_commit();
|
|
return $result;
|
|
|
|
} catch (Exception $e) {
|
|
$this->db->trans_rollback();
|
|
log_message('error', 'Desk-Moloni transaction error: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get human-readable timestamp
|
|
*
|
|
* @param string $timestamp Database timestamp
|
|
* @return string Formatted timestamp
|
|
*/
|
|
protected function formatTimestamp($timestamp)
|
|
{
|
|
if (empty($timestamp) || $timestamp === '0000-00-00 00:00:00') {
|
|
return null;
|
|
}
|
|
|
|
return date('Y-m-d H:i:s', strtotime($timestamp));
|
|
}
|
|
|
|
/**
|
|
* Check if current user has permission for operation
|
|
*
|
|
* @param string $permission Permission to check
|
|
* @return bool True if user has permission
|
|
*/
|
|
protected function hasPermission($permission)
|
|
{
|
|
// Check if user is admin or has specific permission
|
|
if (is_admin()) {
|
|
return true;
|
|
}
|
|
|
|
// Check module-specific permissions
|
|
return has_permission($permission, '', 'view') || has_permission($permission, '', 'create');
|
|
}
|
|
|
|
/**
|
|
* Get current user ID
|
|
*
|
|
* @return int|null User ID or null if not logged in
|
|
*/
|
|
protected function getCurrentUserId()
|
|
{
|
|
return get_staff_user_id();
|
|
}
|
|
} |