/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ '3.0.0', 'api_base_url' => 'https://api.moloni.pt/v1/', 'api_timeout' => '30', 'sync_enabled' => '1', 'sync_interval_minutes' => '15', 'max_retry_attempts' => '3', 'log_retention_days' => '365', 'queue_batch_size' => '50', 'encryption_algorithm' => 'AES-256-GCM' ]; /** * Configuration Model Constructor * * Initializes the configuration model with proper table naming, * encryption setup, and default configuration initialization. * * @since 3.0.0 * @author Descomplicar® * @throws Exception If table initialization fails or database connection issues */ public function __construct() { parent::__construct(); $this->table = $this->getTableName('config'); $this->initializeDefaults(); } /** * Retrieve configuration value by key with automatic decryption * * Fetches configuration value from database with automatic decryption * for sensitive keys. Returns default value if key doesn't exist. * * @param string $key Configuration key to retrieve * @param mixed $default Default value returned if key is not found * @return mixed Configuration value (decrypted if encrypted) or default value * @throws Exception When database query fails or decryption errors * @since 3.0.0 * @author Descomplicar® */ public function get($key, $default = null) { try { // Validate key if (empty($key)) { return $default; } $query = $this->db->where('setting_key', $key)->get($this->table); if ($query->num_rows() === 0) { return $default; } $row = $query->row(); // Decrypt if encrypted if ($row->encrypted == 1) { return $this->decryptData($row->setting_value); } return $row->setting_value; } catch (Exception $e) { log_message('error', 'Desk-Moloni config get error: ' . $e->getMessage()); return $default; } } /** * Store configuration value with automatic encryption for sensitive keys * * Saves configuration value to database with automatic encryption detection * for sensitive keys, comprehensive validation, and secure storage. * * @param string $key Configuration key (must be non-empty, alphanumeric with underscores) * @param mixed $value Configuration value to store * @param bool $forceEncryption Force encryption regardless of automatic detection * @return bool True on successful save, false on failure * @throws InvalidArgumentException When key validation fails or invalid parameters * @throws Exception When database operations fail or encryption errors * @since 3.0.0 * @author Descomplicar® */ public function set($key, $value, $forceEncryption = false) { try { // Validate key if (empty($key)) { throw new InvalidArgumentException('Configuration key cannot be empty'); } // Validate input $validationErrors = $this->validateConfigData(['setting_key' => $key, 'setting_value' => $value]); if (!empty($validationErrors)) { throw new Exception('Validation failed: ' . implode(', ', $validationErrors)); } // Determine if value should be encrypted $shouldEncrypt = $forceEncryption || $this->isSensitiveKey($key); // Prepare data $data = [ 'setting_key' => $key, 'setting_value' => $shouldEncrypt ? $this->encryptData($value) : $value, 'encrypted' => $shouldEncrypt ? 1 : 0, 'updated_at' => date('Y-m-d H:i:s') ]; // Check if key exists $existing = $this->db->where('setting_key', $key)->get($this->table); if ($existing->num_rows() > 0) { // Update existing $result = $this->db->where('setting_key', $key)->update($this->table, $data); $this->logDatabaseOperation('update', $this->table, $data, $existing->row()->id); } else { // Insert new $data['created_at'] = date('Y-m-d H:i:s'); $result = $this->db->insert($this->table, $data); $this->logDatabaseOperation('create', $this->table, $data, $this->db->insert_id()); } return $result; } catch (InvalidArgumentException $e) { throw $e; // Re-throw validation exceptions } catch (Exception $e) { log_message('error', 'Desk-Moloni config set error: ' . $e->getMessage()); return false; } } /** * Store configuration value with forced encryption * * Convenience method for storing configuration values with mandatory encryption, * regardless of key type. Used for storing sensitive data securely. * * @param string $key Configuration key to store * @param mixed $value Configuration value to encrypt and store * @return bool True on successful encrypted storage, false on failure * @throws InvalidArgumentException When key validation fails * @throws Exception When encryption or database operations fail * @since 3.0.0 * @author Descomplicar® */ public function set_encrypted($key, $value) { return $this->set($key, $value, true); } /** * Get encrypted configuration value (decrypted) * * @param string $key Configuration key * @param mixed $default Default value if key not found * @return mixed Decrypted configuration value */ public function get_encrypted($key, $default = null) { return $this->get($key, $default); } /** * Store OAuth access token with expiration tracking * * Securely stores OAuth access token with encrypted storage and * expiration timestamp for automatic token refresh management. * * @param string $token OAuth access token to store securely * @param int $expires_at Unix timestamp when token expires * @return bool True on successful storage of both token and expiration, false on failure * @throws Exception When token encryption fails or database operations error * @since 3.0.0 * @author Descomplicar® */ public function set_oauth_token($token, $expires_at) { try { $success = true; $success &= $this->set('oauth_access_token', $token, true); $success &= $this->set('oauth_token_expires_at', date('Y-m-d H:i:s', $expires_at)); return $success; } catch (Exception $e) { log_message('error', 'Desk-Moloni OAuth token set error: ' . $e->getMessage()); return false; } } /** * Get OAuth token with metadata * * @return array Token data array */ public function get_oauth_token() { try { $token = $this->get('oauth_access_token'); $expires_at = $this->get('oauth_token_expires_at'); if (empty($token)) { return []; } return [ 'token' => $token, 'expires_at' => $expires_at ? strtotime($expires_at) : null, 'created_at' => time(), 'type' => 'oauth_token' ]; } catch (Exception $e) { log_message('error', 'Desk-Moloni OAuth token get error: ' . $e->getMessage()); return []; } } /** * Validate OAuth token existence and expiration status * * Checks if OAuth access token exists and is not expired, with a * 5-minute buffer to prevent token expiration during API calls. * * @return bool True if token exists and is valid (not expired), false otherwise * @throws Exception When token validation process fails or database errors occur * @since 3.0.0 * @author Descomplicar® */ public function is_oauth_token_valid() { try { $accessToken = $this->get('oauth_access_token'); $expiresAt = $this->get('oauth_token_expires_at'); if (empty($accessToken) || empty($expiresAt)) { return false; } // Check if token is expired (with 5-minute buffer) $expirationTime = strtotime($expiresAt) - 300; // 5 minutes buffer return time() < $expirationTime; } catch (Exception $e) { log_message('error', 'Desk-Moloni OAuth validation error: ' . $e->getMessage()); return false; } } /** * Delete configuration key * * @param string $key Configuration key * @return bool Success status */ public function delete($key) { try { if (empty($key)) { return false; } $existing = $this->db->where('setting_key', $key)->get($this->table); if ($existing->num_rows() === 0) { return true; // Already doesn't exist } $result = $this->db->where('setting_key', $key)->delete($this->table); $this->logDatabaseOperation('delete', $this->table, ['setting_key' => $key], $existing->row()->id); return $result; } catch (Exception $e) { log_message('error', 'Desk-Moloni config delete error: ' . $e->getMessage()); return false; } } /** * Retrieve all configuration values with optional encryption handling * * Fetches complete configuration dataset with optional decryption of sensitive values, * includes default configuration values for missing keys. * * @param bool $includeEncrypted Whether to decrypt and include encrypted values (default: true) * @return array Complete configuration array with all keys and values, * encrypted values are decrypted if $includeEncrypted is true * @throws Exception When database query fails or decryption errors occur * @since 3.0.0 * @author Descomplicar® */ public function get_all($includeEncrypted = true) { try { $query = $this->db->get($this->table); $config = []; foreach ($query->result() as $row) { if ($row->encrypted == 1 && $includeEncrypted) { $config[$row->setting_key] = $this->decryptData($row->setting_value); } elseif ($row->encrypted == 0) { $config[$row->setting_key] = $row->setting_value; } // Skip encrypted values if includeEncrypted is false } // Add default values for missing keys foreach ($this->defaultConfig as $key => $defaultValue) { if (!isset($config[$key])) { $config[$key] = $defaultValue; } } return $config; } catch (Exception $e) { log_message('error', 'Desk-Moloni config get_all error: ' . $e->getMessage()); return $this->defaultConfig; } } /** * Set multiple configuration values in batch * * @param array $config_batch Array of key => value pairs * @return bool Success status */ public function set_batch($config_batch) { try { if (!is_array($config_batch) || empty($config_batch)) { return false; } $success = true; // Use transaction for batch operations return $this->executeTransaction(function() use ($config_batch, &$success) { foreach ($config_batch as $key => $value) { if (!$this->set($key, $value)) { $success = false; throw new Exception("Failed to set config key: {$key}"); } } return $success; }); } catch (Exception $e) { log_message('error', 'Desk-Moloni config set_batch error: ' . $e->getMessage()); return false; } } /** * Get configuration keys by pattern * * @param string $pattern LIKE pattern for key matching * @param bool $includeEncrypted Whether to decrypt encrypted values * @return array Matching configuration */ public function getByPattern($pattern, $includeEncrypted = true) { try { $query = $this->db->like('setting_key', $pattern)->get($this->table); $config = []; foreach ($query->result() as $row) { if ($row->encrypted == 1 && $includeEncrypted) { $config[$row->setting_key] = $this->decryptData($row->setting_value); } elseif ($row->encrypted == 0) { $config[$row->setting_key] = $row->setting_value; } } return $config; } catch (Exception $e) { log_message('error', 'Desk-Moloni config getByPattern error: ' . $e->getMessage()); return []; } } /** * Get OAuth configuration * * @return array OAuth configuration */ public function getOAuthConfig() { $oauthKeys = [ 'oauth_client_id', 'oauth_client_secret', 'oauth_access_token', 'oauth_refresh_token', 'oauth_token_expires_at' ]; $config = []; foreach ($oauthKeys as $key) { $config[$key] = $this->get($key); } return $config; } /** * Set OAuth tokens * * @param array $tokens OAuth token data * @return bool Success status */ public function setOAuthTokens($tokens) { try { $requiredTokens = ['access_token', 'refresh_token', 'expires_in']; foreach ($requiredTokens as $required) { if (!isset($tokens[$required])) { throw new Exception("Missing required OAuth token: {$required}"); } } // Calculate expiration timestamp $expiresAt = date('Y-m-d H:i:s', time() + (int)$tokens['expires_in']); $success = true; $success &= $this->set('oauth_access_token', $tokens['access_token'], true); $success &= $this->set('oauth_refresh_token', $tokens['refresh_token'], true); $success &= $this->set('oauth_token_expires_at', $expiresAt); return $success; } catch (Exception $e) { log_message('error', 'Desk-Moloni OAuth tokens error: ' . $e->getMessage()); return false; } } /** * Clear all OAuth tokens (for logout/revoke) * * @return bool Success status */ public function clearOAuthTokens() { $success = true; $oauthKeys = ['oauth_access_token', 'oauth_refresh_token', 'oauth_token_expires_at']; foreach ($oauthKeys as $key) { $success &= $this->delete($key); } return $success; } /** * Get API configuration * * @return array API configuration */ public function getAPIConfig() { return [ 'base_url' => $this->get('api_base_url', 'https://api.moloni.pt/v1/'), 'timeout' => (int)$this->get('api_timeout', 30), 'company_id' => $this->get('moloni_company_id'), 'access_token' => $this->get('oauth_access_token') ]; } /** * Check if configuration key is sensitive and should be encrypted * * @param string $key Configuration key * @return bool True if key is sensitive */ private function isSensitiveKey($key) { return in_array($key, $this->sensitiveKeys) || strpos($key, 'password') !== false || strpos($key, 'secret') !== false || strpos($key, 'token') !== false || strpos($key, 'key') !== false; } /** * Validate configuration data * * @param array $data Configuration data to validate * @return array Validation errors */ private function validateConfigData($data) { $errors = []; // Required fields $requiredFields = ['setting_key']; $errors = array_merge($errors, $this->validateRequiredFields($data, $requiredFields)); // Field length limits $fieldLimits = [ 'setting_key' => 255 ]; $errors = array_merge($errors, $this->validateFieldLengths($data, $fieldLimits)); // Key format validation if (isset($data['setting_key'])) { if (!preg_match('/^[a-z0-9_]+$/', $data['setting_key'])) { $errors[] = 'Setting key must contain only lowercase letters, numbers, and underscores'; } } return $errors; } /** * Initialize default configuration values in database * * Sets up default configuration values for module operation, * only creates values that don't already exist in database. * * @return bool True if all default values were successfully initialized, false on any failure * @throws Exception When database operations fail or default value validation errors * @since 3.0.0 * @author Descomplicar® */ public function initializeDefaults() { $success = true; foreach ($this->defaultConfig as $key => $value) { // Only set if not already exists if ($this->get($key) === null) { $success &= $this->set($key, $value); } } return $success; } /** * Export configuration for backup (excluding sensitive data) * * @return array Non-sensitive configuration data */ public function exportConfig() { try { $allConfig = $this->get_all(false); // Don't include encrypted values // Remove sensitive keys entirely from export foreach ($this->sensitiveKeys as $sensitiveKey) { unset($allConfig[$sensitiveKey]); } return $allConfig; } catch (Exception $e) { log_message('error', 'Desk-Moloni config export error: ' . $e->getMessage()); return []; } } /** * Get configuration value with caching * * @param string $key Configuration key * @param mixed $default Default value if key not found * @return mixed Configuration value */ public function get_cached($key, $default = null) { $cache_key = 'config_' . $key; // Check if cached and not expired if (isset(self::$config_cache[$cache_key])) { $cached = self::$config_cache[$cache_key]; if ((time() - $cached['timestamp']) < $this->cache_ttl) { desk_moloni_log('debug', "Config cache hit for key: $key", [], 'cache'); return $cached['value']; } else { // Cache expired, remove it unset(self::$config_cache[$cache_key]); desk_moloni_log('debug', "Config cache expired for key: $key", [], 'cache'); } } // Cache miss, get from database $value = $this->get($key, $default); // Cache the result self::$config_cache[$cache_key] = [ 'value' => $value, 'timestamp' => time() ]; desk_moloni_log('debug', "Config cached for key: $key", ['ttl' => $this->cache_ttl], 'cache'); return $value; } /** * Set configuration value and invalidate cache * * @param string $key Configuration key * @param mixed $value Configuration value * @param bool $forceEncryption Force encryption even for non-sensitive keys * @return bool Success status */ public function set_cached($key, $value, $forceEncryption = false) { $result = $this->set($key, $value, $forceEncryption); if ($result) { // Invalidate cache for this key $cache_key = 'config_' . $key; unset(self::$config_cache[$cache_key]); desk_moloni_log('debug', "Config cache invalidated for key: $key", [], 'cache'); } return $result; } /** * Clear all configuration cache */ public function clear_cache() { $count = count(self::$config_cache); self::$config_cache = []; desk_moloni_log('info', "Configuration cache cleared", ['cached_items' => $count], 'cache'); } /** * Get cache statistics * * @return array Cache statistics */ public function get_cache_stats() { $stats = [ 'cached_items' => count(self::$config_cache), 'cache_ttl' => $this->cache_ttl, 'items' => [] ]; foreach (self::$config_cache as $key => $data) { $age = time() - $data['timestamp']; $stats['items'][] = [ 'key' => $key, 'age_seconds' => $age, 'expires_in' => $this->cache_ttl - $age ]; } return $stats; } }