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