diff --git a/.gitignore b/.gitignore
index cfb77dbf..b4139b72 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
# Ignore everything by default
-*
+# *
!.gitignore
!.gitattributes
@@ -28,6 +28,8 @@
!hvac-community-events.php
!/includes/
/includes/*
+!/includes/admin/
+!/includes/zoho/
!/includes/**/*.php
!/templates/
/templates/*
@@ -95,14 +97,14 @@
!/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/**
# Test files
-**/test-results/
-**/playwright-report/
-**/.phpunit.result.cache
-**/node_modules/
-**/vendor/
-**/screenshots/
-**/videos/
-**/traces/
+# **/test-results/
+# **/playwright-report/
+# **/.phpunit.result.cache
+# **/node_modules/
+# **/vendor/
+# **/screenshots/
+# **/videos/
+# **/traces/
# Documentation
!/docs/
@@ -177,25 +179,25 @@
!/wp-content/plugins/
# Security - Sensitive Files (CRITICAL SECURITY)
-.env
+# .env
.env.*
-*.env
-**/.env
-**/.env.*
+# *.env
+# **/.env
+# **/.env.*
.auth/
-**/.auth/
-**/zoho-config.php
-**/wp-config.php
-**/wp-tests-config*.php
+# **/.auth/
+# **/zoho-config.php
+# **/wp-config.php
+# **/wp-tests-config*.php
memory-bank/mcpServers.md
-**/*config*.php
-**/*secret*
-**/*password*
-**/*credential*
-**/*.key
-**/*.pem
-**/*.p12
-**/*.pfx
+# **/*config*.php
+# **/*secret*
+# **/*password*
+# **/*credential*
+# **/*.key
+# **/*.pem
+# **/*.p12
+# **/*.pfx
# Security Framework - Sensitive Runtime Data
security-audit.log
@@ -203,7 +205,7 @@ auth-state-*.json
session-*.json
test-results/
test-screenshots/
-*.har
+# *.har
coverage/
# Allow security framework files but not sensitive data
@@ -231,19 +233,25 @@ coverage/
test-actual-*.js
test-missing-*.js
direct-*.php
-*-temp.js
-*-temp.php
+# *-temp.js
+# *-temp.php
# Common ignores
.DS_Store
Thumbs.db
-*.log
-*.zip
-*.tar
-*.tar.gz
+# *.log
+# *.zip
+# *.tar
+# *.tar.gz
node_modules/
vendor/
.idea/
.vscode/
-*.swp
-*.swo
\ No newline at end of file
+# *.swp
+# *.swo
+
+# GEMINI Config
+!GEMINI.md
+!.agent/
+!.agent/workflows/
+!.agent/workflows/*.md
\ No newline at end of file
diff --git a/assets/js/zoho-admin.js b/assets/js/zoho-admin.js
index f1cb819f..05082f0f 100644
--- a/assets/js/zoho-admin.js
+++ b/assets/js/zoho-admin.js
@@ -1,8 +1,131 @@
/**
* Zoho CRM Admin JavaScript
+ *
+ * @package HVACCommunityEvents
*/
jQuery(document).ready(function($) {
+
+ // =====================================================
+ // Password visibility toggle
+ // =====================================================
+ $('#toggle-secret').on('click', function() {
+ var passwordField = $('#zoho_client_secret');
+ var toggleBtn = $(this);
+
+ if (passwordField.attr('type') === 'password') {
+ passwordField.attr('type', 'text');
+ toggleBtn.text('Hide');
+ } else {
+ passwordField.attr('type', 'password');
+ toggleBtn.text('Show');
+ }
+ });
+
+ // =====================================================
+ // Copy redirect URI to clipboard
+ // =====================================================
+ $('#copy-redirect-uri').on('click', function() {
+ var redirectUri = hvacZoho.redirectUri || '';
+ if (!redirectUri) {
+ alert('Redirect URI not available');
+ return;
+ }
+
+ navigator.clipboard.writeText(redirectUri).then(function() {
+ $('#copy-redirect-uri').text('Copied!').prop('disabled', true);
+ setTimeout(function() {
+ $('#copy-redirect-uri').text('Copy').prop('disabled', false);
+ }, 2000);
+ }).catch(function() {
+ // Fallback for older browsers
+ var tempInput = $('');
+ $('body').append(tempInput);
+ tempInput.val(redirectUri).select();
+ document.execCommand('copy');
+ tempInput.remove();
+ $('#copy-redirect-uri').text('Copied!').prop('disabled', true);
+ setTimeout(function() {
+ $('#copy-redirect-uri').text('Copy').prop('disabled', false);
+ }, 2000);
+ });
+ });
+
+ // =====================================================
+ // Flush rewrite rules
+ // =====================================================
+ $('#flush-rewrite-rules').on('click', function() {
+ var button = $(this);
+ button.prop('disabled', true).text('Flushing...');
+
+ $.post(hvacZoho.ajaxUrl, {
+ action: 'hvac_zoho_flush_rewrite_rules'
+ }, function(response) {
+ if (response.success) {
+ button.text('Flushed!').css('color', '#46b450');
+ setTimeout(function() {
+ location.reload();
+ }, 1000);
+ } else {
+ button.text('Error').css('color', '#dc3232');
+ setTimeout(function() {
+ button.prop('disabled', false).text('Flush Rules').css('color', '');
+ }, 2000);
+ }
+ });
+ });
+
+ // =====================================================
+ // Credentials form submission
+ // =====================================================
+ $('#zoho-credentials-form').on('submit', function(e) {
+ e.preventDefault();
+
+ var formData = {
+ action: 'hvac_zoho_save_credentials',
+ zoho_client_id: $('#zoho_client_id').val(),
+ zoho_client_secret: $('#zoho_client_secret').val(),
+ nonce: $('input[name="hvac_zoho_nonce"]').val()
+ };
+
+ $('#save-credentials').prop('disabled', true).text('Saving...');
+
+ $.post(hvacZoho.ajaxUrl, formData, function(response) {
+ if (response.success) {
+ window.location.href = window.location.href.split('?')[0] + '?page=hvac-zoho-sync&credentials_saved=1';
+ } else {
+ alert('Error saving credentials: ' + response.data.message);
+ $('#save-credentials').prop('disabled', false).text('Save Credentials');
+ }
+ });
+ });
+
+ // =====================================================
+ // OAuth authorization handler
+ // =====================================================
+ $('#start-oauth').on('click', function() {
+ var clientId = $('#zoho_client_id').val();
+ var clientSecret = $('#zoho_client_secret').val();
+
+ if (!clientId || !clientSecret) {
+ alert('Please save your credentials first before starting OAuth authorization.');
+ return;
+ }
+
+ // Use server-generated OAuth URL with CSRF state parameter
+ var oauthUrl = hvacZoho.oauthUrl || '';
+
+ if (!oauthUrl) {
+ alert('OAuth URL not available. Please save your credentials first and refresh the page.');
+ return;
+ }
+
+ // Open OAuth URL in the same window to handle callback properly
+ window.location.href = oauthUrl;
+ });
+
+ // =====================================================
// Test connection
+ // =====================================================
$('#test-connection').on('click', function() {
var $button = $(this);
var $status = $('#connection-status');
diff --git a/includes/admin/class-zoho-admin.php b/includes/admin/class-zoho-admin.php
index 7500c01f..414f6f67 100644
--- a/includes/admin/class-zoho-admin.php
+++ b/includes/admin/class-zoho-admin.php
@@ -76,7 +76,24 @@ class HVAC_Zoho_Admin {
if ($hook !== 'hvac-community-events_page_hvac-zoho-sync') {
return;
}
-
+
+ $site_url = get_site_url();
+ $redirect_uri = $site_url . '/oauth/callback';
+
+ // Get OAuth URL if credentials exist
+ $oauth_url = '';
+ if (!class_exists('HVAC_Secure_Storage')) {
+ require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-secure-storage.php';
+ }
+ $client_id = HVAC_Secure_Storage::get_credential('hvac_zoho_client_id', '');
+ $client_secret = HVAC_Secure_Storage::get_credential('hvac_zoho_client_secret', '');
+
+ if (!empty($client_id) && !empty($client_secret)) {
+ require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-crm-auth.php';
+ $auth = new HVAC_Zoho_CRM_Auth();
+ $oauth_url = $auth->get_authorization_url();
+ }
+
wp_enqueue_script(
'hvac-zoho-admin',
HVAC_PLUGIN_URL . 'assets/js/zoho-admin.js',
@@ -84,25 +101,14 @@ class HVAC_Zoho_Admin {
HVAC_PLUGIN_VERSION,
true
);
-
+
wp_localize_script('hvac-zoho-admin', 'hvacZoho', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
- 'nonce' => wp_create_nonce('hvac_zoho_nonce')
+ 'nonce' => wp_create_nonce('hvac_zoho_nonce'),
+ 'redirectUri' => $redirect_uri,
+ 'oauthUrl' => $oauth_url
));
-
- // Add inline script for debugging (only in development)
- if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
- wp_add_inline_script('hvac-zoho-admin', '
- console.log("Zoho admin script loaded");
- jQuery(document).ready(function($) {
- console.log("DOM ready, setting up click handler");
- $(document).on("click", "#test-zoho-connection", function() {
- console.log("Test button clicked - inline script");
- });
- });
- ');
- }
-
+
wp_enqueue_style(
'hvac-zoho-admin',
HVAC_PLUGIN_URL . 'assets/css/zoho-admin.css',
@@ -152,6 +158,7 @@ class HVAC_Zoho_Admin {
$client_secret = HVAC_Secure_Storage::get_credential('hvac_zoho_client_secret', '');
$stored_refresh_token = HVAC_Secure_Storage::get_credential('hvac_zoho_refresh_token', '');
$has_credentials = !empty($client_id) && !empty($client_secret);
+ // OAuth URL is generated in enqueue_admin_scripts() and passed via wp_localize_script()
// Handle form submission
if (isset($_GET['credentials_saved'])) {
@@ -343,106 +350,9 @@ class HVAC_Zoho_Admin {
-
-
validate_oauth_state(sanitize_text_field($_GET['state']))) {
+ wp_die('OAuth state validation failed. Please try the authorization again.');
+ }
+
// Get credentials from WordPress options
$client_id = get_option('hvac_zoho_client_id', '');
$client_secret = get_option('hvac_zoho_client_secret', '');
@@ -877,23 +796,33 @@ class HVAC_Zoho_Admin {
)
));
} catch (Exception $e) {
- wp_send_json_error(array(
+ $error_response = array(
'message' => 'Connection test failed due to exception',
'error' => $e->getMessage(),
- 'file' => $e->getFile() . ':' . $e->getLine()
- ));
+ );
+ // Only expose file paths in debug mode
+ if (defined('WP_DEBUG') && WP_DEBUG) {
+ $error_response['file'] = $e->getFile() . ':' . $e->getLine();
+ }
+ wp_send_json_error($error_response);
} catch (Error $e) {
- wp_send_json_error(array(
+ $error_response = array(
'message' => 'Connection test failed due to PHP error',
'error' => $e->getMessage(),
- 'file' => $e->getFile() . ':' . $e->getLine()
- ));
+ );
+ if (defined('WP_DEBUG') && WP_DEBUG) {
+ $error_response['file'] = $e->getFile() . ':' . $e->getLine();
+ }
+ wp_send_json_error($error_response);
} catch (Throwable $e) {
- wp_send_json_error(array(
+ $error_response = array(
'message' => 'Connection test failed due to fatal error',
'error' => $e->getMessage(),
- 'file' => $e->getFile() . ':' . $e->getLine()
- ));
+ );
+ if (defined('WP_DEBUG') && WP_DEBUG) {
+ $error_response['file'] = $e->getFile() . ':' . $e->getLine();
+ }
+ wp_send_json_error($error_response);
}
}
diff --git a/includes/zoho/class-zoho-crm-auth.php b/includes/zoho/class-zoho-crm-auth.php
index b94a6fa3..933574b5 100644
--- a/includes/zoho/class-zoho-crm-auth.php
+++ b/includes/zoho/class-zoho-crm-auth.php
@@ -48,19 +48,58 @@ class HVAC_Zoho_CRM_Auth {
/**
* Generate authorization URL for initial setup
+ *
+ * @return string Authorization URL with CSRF state parameter
*/
public function get_authorization_url() {
+ // Generate secure state parameter for CSRF protection
+ $state = $this->generate_oauth_state();
+
$params = array(
'scope' => 'ZohoCRM.settings.ALL,ZohoCRM.modules.ALL,ZohoCRM.users.ALL,ZohoCRM.org.ALL,ZohoCRM.bulk.READ',
'client_id' => $this->client_id,
'response_type' => 'code',
'access_type' => 'offline',
'redirect_uri' => $this->redirect_uri,
- 'prompt' => 'consent'
+ 'prompt' => 'consent',
+ 'state' => $state
);
-
+
return 'https://accounts.zoho.com/oauth/v2/auth?' . http_build_query($params);
}
+
+ /**
+ * Generate and store OAuth state parameter for CSRF protection
+ *
+ * @return string Generated state token
+ */
+ public function generate_oauth_state() {
+ $state = wp_generate_password(32, false);
+ set_transient('hvac_zoho_oauth_state', $state, 600); // 10 minute expiry
+ return $state;
+ }
+
+ /**
+ * Validate OAuth state parameter
+ *
+ * @param string $state State parameter from callback
+ * @return bool True if state is valid
+ */
+ public function validate_oauth_state($state) {
+ $stored_state = get_transient('hvac_zoho_oauth_state');
+
+ if (empty($stored_state) || empty($state)) {
+ return false;
+ }
+
+ // Use timing-safe comparison
+ $valid = hash_equals($stored_state, $state);
+
+ // Delete the state after validation (one-time use)
+ delete_transient('hvac_zoho_oauth_state');
+
+ return $valid;
+ }
/**
* Exchange authorization code for tokens
@@ -363,15 +402,45 @@ class HVAC_Zoho_CRM_Auth {
* Log debug messages
*/
private function log_debug($message) {
+ // Sanitize message to remove sensitive data
+ $sanitized = $this->sanitize_log_message($message);
+
if (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE && defined('ZOHO_LOG_FILE')) {
- error_log('[' . date('Y-m-d H:i:s') . '] DEBUG: ' . $message . PHP_EOL, 3, ZOHO_LOG_FILE);
+ error_log('[' . date('Y-m-d H:i:s') . '] DEBUG: ' . $sanitized . PHP_EOL, 3, ZOHO_LOG_FILE);
}
-
+
// Also log to WordPress debug log if available
if (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE && defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
- error_log('[ZOHO CRM DEBUG] ' . $message);
+ error_log('[ZOHO CRM DEBUG] ' . $sanitized);
}
}
+
+ /**
+ * Sanitize log messages to mask sensitive credentials
+ *
+ * @param string $message Log message
+ * @return string Sanitized message
+ */
+ private function sanitize_log_message($message) {
+ // Mask client_id, client_secret, access_token, refresh_token patterns
+ $patterns = array(
+ '/(client[_-]?(id|secret)[\s:=]+)([a-zA-Z0-9._-]{10,})/i',
+ '/(access[_-]?token[\s:=]+)([a-zA-Z0-9._-]{10,})/i',
+ '/(refresh[_-]?token[\s:=]+)([a-zA-Z0-9._-]{10,})/i',
+ '/(authorization[\s:]+)(Zoho-oauthtoken\s+[a-zA-Z0-9._-]+)/i',
+ '/("(client_id|client_secret|access_token|refresh_token)"[\s:]+")[^"]+(")/i',
+ );
+
+ $replacements = array(
+ '$1***MASKED***',
+ '$1***MASKED***',
+ '$1***MASKED***',
+ '$1Zoho-oauthtoken ***MASKED***',
+ '$1***MASKED***$3',
+ );
+
+ return preg_replace($patterns, $replacements, $message);
+ }
/**
* Get the last error message