JezK
Edit File: zoho-auth.php
<?php error_reporting(E_ALL); ini_set('display_errors', 1); // Manually include wp-config.php to ensure credentials are accessible require_once('/home/authentica.com/public_html/wp-config.php'); // Ensure this file is accessed within WordPress if (!defined('ABSPATH')) { exit; // Prevent direct access } // Security headers for any JSON output from this file function _za_header_json() { header('Content-Type: application/json'); header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); header('Referrer-Policy: no-referrer'); } // Fetch credentials from wp-config.php $client_id = defined('ZOHO_CLIENT_ID') ? ZOHO_CLIENT_ID : null; $client_secret = defined('ZOHO_CLIENT_SECRET') ? ZOHO_CLIENT_SECRET : null; $refresh_token = defined('ZOHO_REFRESH_TOKEN') ? ZOHO_REFRESH_TOKEN : null; $domain = defined('ZOHO_DOMAIN') ? ZOHO_DOMAIN : 'https://accounts.zoho.com'; // Validate that all required credentials are available if (!$client_id || !$client_secret || !$refresh_token) { _za_header_json(); echo json_encode(['error' => 'Missing Zoho API credentials.']); exit; } /** * Normalize any provided domain to a proper Zoho ACCOUNTS domain. * If someone sets ZOHO_DOMAIN to a zohoapis host, we map it to the corresponding accounts host. */ function za_normalize_accounts_domain($domain) { if (!$domain) return 'https://accounts.zoho.com'; $d = strtolower(trim($domain)); // ensure scheme if (strpos($d, 'http') !== 0) { $d = 'https://' . $d; } // strip trailing slash $d = rtrim($d, '/'); // If pointing to zohoapis*, map to the matching accounts host by region if (strpos($d, 'zohoapis') !== false) { if (strpos($d, '.eu') !== false) return 'https://accounts.zoho.eu'; if (strpos($d, '.in') !== false) return 'https://accounts.zoho.in'; if (strpos($d, '.com.cn') !== false) return 'https://accounts.zoho.com.cn'; if (strpos($d, '.com.au') !== false) return 'https://accounts.zoho.com.au'; return 'https://accounts.zoho.com'; } // If not an accounts.zoho.* host, default safely to US if (strpos($d, 'accounts.zoho.') === false) { return 'https://accounts.zoho.com'; } return $d; } $accounts_domain = za_normalize_accounts_domain($domain); /** * Token cache strategy * - Prefer WordPress transients if available * - Fallback to /tmp file cache (with lock) */ define('ZA_TOKEN_TRANSIENT_KEY', 'zoho_access_token_cache_v1'); define('ZA_TOKEN_FILE', '/tmp/zoho_access_token_cache.json'); define('ZA_TOKEN_LOCK_FILE', '/tmp/zoho_access_token_cache.lock'); define('ZA_TOKEN_SAFETY_MARGIN_SEC', 120); // refresh 2 minutes early function za_can_use_wp_transients() { return function_exists('get_transient') && function_exists('set_transient'); } function za_cache_read() { if (za_can_use_wp_transients()) { $data = get_transient(ZA_TOKEN_TRANSIENT_KEY); if (is_array($data) && isset($data['access_token'], $data['expires_at'])) { return $data; } } if (is_readable(ZA_TOKEN_FILE)) { $json = file_get_contents(ZA_TOKEN_FILE); if ($json !== false) { $data = json_decode($json, true); if (is_array($data) && isset($data['access_token'], $data['expires_at'])) { return $data; } } } return null; } function za_cache_write($access_token, $expires_in) { $now = time(); $expires_in = max(60, (int)$expires_in); $expires_at = $now + $expires_in; $data = [ 'access_token' => $access_token, 'expires_at' => $expires_at, 'written_at' => $now, 'source' => 'refresh' ]; if (za_can_use_wp_transients()) { $ttl = max(30, $expires_in - ZA_TOKEN_SAFETY_MARGIN_SEC); set_transient(ZA_TOKEN_TRANSIENT_KEY, $data, $ttl); } $tmpFile = ZA_TOKEN_FILE . '.tmp'; @file_put_contents($tmpFile, json_encode($data, JSON_UNESCAPED_SLASHES)); @chmod($tmpFile, 0600); @rename($tmpFile, ZA_TOKEN_FILE); return $data; } function za_is_token_valid($data) { if (!is_array($data) || empty($data['access_token']) || empty($data['expires_at'])) return false; return (time() + ZA_TOKEN_SAFETY_MARGIN_SEC) < (int)$data['expires_at']; } /** * Refresh the token from Zoho OAuth server * Returns: ['http_code'=>int,'error'=>string,'response'=>array,'response_raw_excerpt'=>string?] */ function za_do_refresh_request($client_id, $client_secret, $refresh_token, $accounts_domain) { $token_url = rtrim($accounts_domain, '/') . '/oauth/v2/token'; $params = [ "refresh_token" => $refresh_token, "client_id" => $client_id, "client_secret" => $client_secret, "grant_type" => "refresh_token" ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $token_url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Content-Type: application/x-www-form-urlencoded" ]); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error_msg = curl_error($ch); curl_close($ch); $decoded = json_decode($response, true); $raw_excerpt = ''; if (!is_array($decoded)) { // Keep a tiny excerpt for debugging (no secrets included) $raw_excerpt = substr((string)$response, 0, 300); $decoded = []; } return [ "http_code" => $http_code, "error" => $error_msg, "response" => $decoded, "response_raw_excerpt" => $raw_excerpt ]; } /** Lock helpers */ function za_acquire_lock() { $fh = @fopen(ZA_TOKEN_LOCK_FILE, 'c'); if (!$fh) return null; if (@flock($fh, LOCK_EX)) return $fh; @fclose($fh); return null; } function za_release_lock($fh) { if ($fh) { @flock($fh, LOCK_UN); @fclose($fh); } } /** * Public: get_zoho_access_token() * Returns: ['http_code'=>int,'error'=>string,'response'=>['access_token'=>...,'expires_at'=>...,'source'=>...]] */ function get_zoho_access_token() { global $client_id, $client_secret, $refresh_token, $accounts_domain; // 1) Use cached token if still valid $cached = za_cache_read(); if (za_is_token_valid($cached)) { return [ "http_code" => 200, "error" => "", "response" => [ "access_token" => $cached['access_token'], "expires_at" => $cached['expires_at'], "source" => "cache" ] ]; } // 2) Acquire lock to perform a single refresh across concurrent requests $lock = za_acquire_lock(); if (!$lock) { // Could not lock; re-check cache in case another process refreshed it $cached2 = za_cache_read(); if (za_is_token_valid($cached2)) { return [ "http_code" => 200, "error" => "", "response" => [ "access_token" => $cached2['access_token'], "expires_at" => $cached2['expires_at'], "source" => "cache" ] ]; } // Proceed without lock as last resort } // 3) Double-check cache under lock $cached3 = za_cache_read(); if (za_is_token_valid($cached3)) { za_release_lock($lock); return [ "http_code" => 200, "error" => "", "response" => [ "access_token" => $cached3['access_token'], "expires_at" => $cached3['expires_at'], "source" => "cache" ] ]; } // 4) Do the refresh $refresh = za_do_refresh_request($client_id, $client_secret, $refresh_token, $accounts_domain); if ($refresh['http_code'] !== 200 || empty($refresh['response']['access_token'])) { // Pass through Zoho's error and a small excerpt if body wasn't JSON return $refresh; } // 5) Persist and return the new token $access_token = $refresh['response']['access_token']; $expires_in = !empty($refresh['response']['expires_in']) ? (int)$refresh['response']['expires_in'] : 3600; $stored = za_cache_write($access_token, $expires_in); za_release_lock($lock); return [ "http_code" => 200, "error" => "", "response" => [ "access_token" => $stored['access_token'], "expires_at" => $stored['expires_at'], "source" => "refresh" ] ]; } // Disabled for security - do not expose token publicly if ($_SERVER['REQUEST_METHOD'] === 'GET') { _za_header_json(); http_response_code(403); echo json_encode(['error' => 'Access denied']); exit; }