false, 'message' => 'Missing id_token']); exit; } // Decode JWT payload (middle segment, base64url) $parts = explode('.', $idToken); if (count($parts) !== 3) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Malformed token']); exit; } $payloadJson = base64_decode(strtr($parts[1], '-_', '+/') . str_repeat('=', (4 - strlen($parts[1]) % 4) % 4)); $payload = json_decode($payloadJson, true); if (!$payload) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Cannot decode token payload']); exit; } // Validate claims $iss = $payload['iss'] ?? ''; $aud = $payload['aud'] ?? ''; $exp = $payload['exp'] ?? 0; if (strpos($iss, AZURE_TENANT_ID) === false) { http_response_code(401); echo json_encode(['success' => false, 'message' => 'Invalid issuer']); exit; } if ($aud !== AZURE_CLIENT_ID) { http_response_code(401); echo json_encode(['success' => false, 'message' => 'Invalid audience']); exit; } if (time() > $exp) { http_response_code(401); echo json_encode(['success' => false, 'message' => 'Token expired']); exit; } $email = $payload['preferred_username'] ?? $payload['email'] ?? ''; if (empty($email)) { http_response_code(401); echo json_encode(['success' => false, 'message' => 'No email claim in token']); exit; } session_regenerate_id(true); $_SESSION['user_email'] = strtolower(trim($email)); echo json_encode(['success' => true]); exit; } // --------------------------------------------------------------- // AUTH GUARD — all other actions require a valid session // --------------------------------------------------------------- if (empty($_SESSION['user_email'])) { http_response_code(401); echo json_encode(['success' => false, 'message' => 'Unauthorised']); exit; } $CURRENT_USER = $_SESSION['user_email']; $dataFile = getUserDataFile($CURRENT_USER); $logFile = 'activity.log'; // --------------------------------------------------------------- // LOGOUT (server-side session destroy; client handles MSAL) // --------------------------------------------------------------- if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'logout') { $_SESSION = []; session_destroy(); echo json_encode(['success' => true]); exit; } // Helper to log activity function logActivity($message, $type = 'INFO') { global $logFile, $CURRENT_USER; $timestamp = date('Y-m-d H:i:s'); $logEntry = "[$timestamp] [$type] [$CURRENT_USER] $message\n"; file_put_contents($logFile, $logEntry, FILE_APPEND); } // Helper to read data function getData() { global $dataFile; if (!file_exists($dataFile)) return []; $content = file_get_contents($dataFile); return json_decode($content, true) ?? []; } // Helper to save data function saveData($data) { global $dataFile; file_put_contents($dataFile, json_encode($data, JSON_PRETTY_PRINT)); } // Helper to generate filename from a row function generateFilename($item) { $omgid = trim($item['OMGID'] ?? ''); $domain = trim($item['Domain'] ?? ''); $subteam = trim($item['Subteam'] ?? ''); $brand = trim($item['Brand'] ?? ''); $event = trim($item['Event'] ?? ''); $initiative = trim($item['Initiative'] ?? ''); $yy = trim($item['YY'] ?? ''); $seq = trim($item['Sequence'] ?? ''); $assetName = trim($item['AssetName'] ?? ''); $version = trim($item['Version'] ?? ''); if ($domain === 'EVNT') { $parts = ['EVNT']; if ($event) $parts[] = $event; if ($yy) $parts[] = $yy; if ($seq) $parts[] = $seq; $middle = implode('-', $parts); } else { $parts = []; if ($domain) $parts[] = $domain; if ($subteam) $parts[] = $subteam; if ($brand) $parts[] = $brand; if ($initiative) $parts[] = $initiative; if ($yy) $parts[] = $yy; if ($seq) $parts[] = $seq; $middle = implode('-', $parts); } $suffix = ''; if ($assetName) $suffix .= '_' . $assetName; if ($version) $suffix .= '_v' . $version; if (empty($omgid) && empty($middle)) return ''; return ($omgid ? $omgid . ' - ' : '') . $middle . $suffix; } if ($_SERVER['REQUEST_METHOD'] === 'GET') { if ($action === 'load') { echo json_encode(getData()); exit; } // Sheet Management Endpoints if ($action === 'list_sheets') { $sheets = getUserSheets($CURRENT_USER); echo json_encode(['success' => true, 'sheets' => $sheets]); exit; } if ($action === 'load_sheet') { $sheetId = $_GET['id'] ?? ''; $data = loadSheetData($CURRENT_USER, $sheetId); if ($data !== null) { echo json_encode(['success' => true, 'data' => $data]); } else { echo json_encode(['success' => false, 'message' => 'Sheet not found']); } exit; } // Load Campaign if ($action === 'load_campaign') { $name = $_GET['name'] ?? ''; $file = __DIR__ . '/campaigns/' . preg_replace('/[^a-z0-9_-]/i', '', $name) . '.json'; if (file_exists($file)) { $data = json_decode(file_get_contents($file), true); echo json_encode(['success' => true, 'data' => $data]); } else { echo json_encode(['success' => false, 'message' => 'Campaign not found']); } exit; } } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $input = json_decode(file_get_contents('php://input'), true); if ($action === 'save') { if (isset($input['data']) && is_array($input['data'])) { saveData($input['data']); echo json_encode(['success' => true]); } else { echo json_encode(['success' => false, 'message' => 'Invalid data format']); } exit; } // Save Campaign if ($action === 'save_campaign') { $name = preg_replace('/[^a-z0-9_-]/i', '', $input['name'] ?? 'untitled'); $data = $input['data'] ?? []; $file = __DIR__ . '/campaigns/' . $name . '.json'; if (!is_dir(__DIR__ . '/campaigns/')) { mkdir(__DIR__ . '/campaigns/', 0755, true); } file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); echo json_encode(['success' => true]); exit; } // Sheet Management POST Endpoints if ($action === 'save_sheet') { $name = $input['name'] ?? ''; $data = getData(); $sheet = createSheet($CURRENT_USER, $name, $data); echo json_encode(['success' => true, 'sheet' => $sheet]); exit; } if ($action === 'duplicate_sheet') { $sheetId = $input['id'] ?? ''; $sheet = duplicateSheet($CURRENT_USER, $sheetId); if ($sheet) { echo json_encode(['success' => true, 'sheet' => $sheet]); } else { echo json_encode(['success' => false, 'message' => 'Failed to duplicate sheet']); } exit; } if ($action === 'delete_sheet') { $sheetId = $input['id'] ?? ''; deleteSheet($CURRENT_USER, $sheetId); echo json_encode(['success' => true]); exit; } if ($action === 'rename_sheet') { $sheetId = $input['id'] ?? ''; $newName = $input['name'] ?? ''; $success = renameSheet($CURRENT_USER, $sheetId, $newName); echo json_encode(['success' => $success]); exit; } if ($action === 'update_sheet') { $data = $input['data'] ?? []; saveData($data); echo json_encode(['success' => true]); exit; } if ($action === 'command') { $data = getData() ?? []; $command = trim($input['command']); logActivity("Command received: $command", 'COMMAND'); $commandLower = strtolower($command); // Pre-processing: Convert number words to digits $numberMap = [ 'one' => '1', 'two' => '2', 'three' => '3', 'four' => '4', 'five' => '5', 'six' => '6', 'seven' => '7', 'eight' => '8', 'nine' => '9', 'ten' => '10', 'eleven' => '11', 'twelve' => '12', 'twenty' => '20', 'thirty' => '30' ]; foreach ($numberMap as $word => $digit) { $commandLower = preg_replace('/\b' . $word . '\b/', $digit, $commandLower); } if (isset($GEMINI_API_KEY) && $GEMINI_API_KEY !== 'YOUR_API_KEY_HERE') { $currentDate = date('Y-m-d'); $currentYear2Digit = date('y'); $dataContext = json_encode($data); $yoloMode = isset($input['yolo_mode']) && $input['yolo_mode'] ? 'TRUE' : 'FALSE'; $conversationHistory = isset($input['history']) ? $input['history'] : ''; $campaignName = isset($input['campaign_name']) ? trim($input['campaign_name']) : ''; $globalContext = ''; if (!empty($campaignName)) { $globalContext .= "\nGLOBAL CAMPAIGN NAME: $campaignName"; } $prompt = " You are an intelligent assistant managing a JOB NAMING TOOL for Dow Jones. Current Date: $currentDate YOLO MODE: $yoloMode $globalContext CONVERSATION HISTORY: $conversationHistory CURRENT DATA (Context for your actions): $dataContext === DOW JONES JOB NAMING CONVENTION === FORMAT: [OMGID] - [Domain]-[Subteam]-[Brand]-[Initiative]-[YY]-[Sequence]_[AssetName]_v[Version] EVENT FORMAT: [OMGID] - EVNT-[EventAbbrev]-[YY]-[Sequence]_[AssetName]_v[Version] EXAMPLES: - 000000 - PMKT-ACQ-WSJ-BIC-26-01_MetaBanner_v1 - 000001 - PMKT-ENGRT-WSJ-BIC-26-01_Email_v1 - 000002 - EVNT-GFF-26-02_Agenda_v3 === VALID VALUES === DOMAIN options: PMKT (Performance Marketing), BRND (Brand), EVNT (Event), B2B SUBTEAM options (used when Domain is NOT EVNT): - ACQ (Acquisition), B2B, CMKT (Content Marketing), ENGRT (Engagement/Retention), ENT (Enterprise) BRAND options (used when Domain is NOT EVNT): - WSJ (Wall Street Journal), WSJ+ (Wall Street Journal+), BAR (Barron's), MW (MarketWatch) - DF (Dragonfly), DJE (Dow Jones Energy), FAC (Factiva), FE (Free Expression) - GRI (Global Risk Insights), NWS (Newswires), OA (Oxford Analytica), RSK (Risk), RSKC (Risk Center) - DJRJ (Risk Journal), R&C (Risk & Compliance), WECR (World ECR) EVENT options (used ONLY when Domain is EVNT, replaces Subteam+Brand): - GH (Global Horizons), DJRJS (Risk Journal Summit), WECR (World ECR) - FEOE (Free Expressions Opinion Event), FOH (Future of Health), GFF (Global Food Forum) - JH (Journal House), TL (TechLive), TLQ (TechLive Qatar), TLCYB (TechLive Cyber), FOE (The Future of Everything) - WSJIL (WSJ Invest Live), BODC (Board of Directors Council) - CCOC (CCO Council), CEOC (CEO Council), CFOC (CFO Council), CMOC (CMO Council) - CPOC (CPO Council), TECC (Technology Council), WSJLI (WSJ Leadership Institute) OMGID: 6-digit numeric string (e.g., 000000, 000001) YY: 2-digit year (e.g., 26 for 2026). Default to current year: $currentYear2Digit Sequence: 2-digit sequence number (e.g., 01, 02, 03) Initiative: Free text abbreviation (e.g., BIC, DEMO, LAUNCH) AssetName: Descriptive name with no spaces (e.g., MetaBanner, Email, Agenda, LinkedInAd) Version: Integer version number (e.g., 1, 2, 3) === DATA SCHEMA === Each item has these fields: - OMGID (String, 6 digits with leading zeros) - Domain (String, one of: PMKT, BRND, EVNT, B2B) - Subteam (String, one of: ACQ, B2B, CMKT, ENGRT, ENT) - leave empty for EVNT domain - Brand (String, one of the brand abbreviations above) - leave empty for EVNT domain - Event (String, one of the event abbreviations above) - ONLY used for EVNT domain, leave empty otherwise - Initiative (String, free text) - YY (String, 2 digits) - Sequence (String, 2 digits with leading zero) - AssetName (String, no spaces, PascalCase) - Version (String, integer) - Filename (Auto-generated, READ-ONLY - DO NOT create this field) === SUPPORTED OPERATIONS === 1. 'create': Create new job name entries Output: { \"operation\": \"create\", \"items\": [ { \"OMGID\": \"000000\", \"Domain\": \"PMKT\", \"Subteam\": \"ACQ\", \"Brand\": \"WSJ\", \"Event\": \"\", \"Initiative\": \"BIC\", \"YY\": \"26\", \"Sequence\": \"01\", \"AssetName\": \"MetaBanner\", \"Version\": \"1\" } ] } 2. 'update': Update existing entries by OMGID Output: { \"operation\": \"update\", \"target_ids\": [\"000000\"], \"values\": { \"Version\": \"2\" } } 3. 'batch_update': Update multiple items with DIFFERENT values Output: { \"operation\": \"batch_update\", \"updates\": [ { \"OMGID\": \"000000\", \"values\": { \"AssetName\": \"LinkedInAd\" } } ] } 4. 'question': Ask for clarification (ONLY if YOLO MODE is FALSE) Output: { \"operation\": \"question\", \"text\": \"What domain is this for?\" } === CRITICAL RULES === 1. **MULTIPLE ITEMS**: When user says 'create 5 jobs', create 5 separate items in the array. Auto-increment Sequence (01, 02, 03...) and OMGID for each. 2. **EVENT DOMAIN**: When Domain is EVNT, do NOT populate Subteam or Brand. Use the Event field instead. The user might say the full event name - map it to the abbreviation. 3. **NON-EVENT DOMAINS**: When Domain is PMKT, BRND, or B2B, populate Subteam and Brand. Leave Event empty. 4. **OMGID AUTO-INCREMENT**: If user doesn't specify OMGID, look at existing data and auto-increment from the highest existing OMGID. If no data exists, start at 000000. 5. **SEQUENCE AUTO-INCREMENT**: Within a batch of items sharing the same Domain+Subteam+Brand+Initiative+YY (or Domain+Event+YY for events), auto-increment sequence starting from 01. 6. **ASSET NAME**: Should be PascalCase with no spaces (e.g., MetaBanner, LinkedInAd, EmailHeader). If user gives a name with spaces, convert it. 7. **NAME RESOLUTION**: When user says full names, map to abbreviations: - 'Performance Marketing' -> PMKT, 'Acquisition' -> ACQ, 'Wall Street Journal' -> WSJ - 'Global Food Forum' -> GFF, 'Tech Live' -> TECL, etc. 8. **YOLO MODE**: If TRUE, NEVER ask questions. Guess missing info: - Missing Domain -> PMKT - Missing Subteam -> ACQ - Missing Brand -> WSJ - Missing Initiative -> GEN - Missing YY -> $currentYear2Digit - Missing AssetName -> Asset - Missing Version -> 1 9. **CLARIFICATION RECOVERY**: User's input may be answering a previous question. Combine with conversation history and EXECUTE. 10. **CONTEXT**: Use CURRENT DATA to resolve references like 'change all version numbers to 2' or 'update the GFF events'. CRITICAL: Respond with ONLY valid JSON. No explanations, no markdown, no conversational text. Your response must be a single JSON object starting with { and ending with }. User Command: \"$command\" "; // Call Gemini API $url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=" . $GEMINI_API_KEY; $dataPayload = [ "contents" => [ [ "parts" => [ ["text" => $prompt] ] ] ] ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($dataPayload)); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $response = curl_exec($ch); if (curl_errno($ch)) { echo json_encode(['success' => false, 'message' => 'Curl error: ' . curl_error($ch)]); exit; } curl_close($ch); $responseObj = json_decode($response, true); if (isset($responseObj['error'])) { $errorMsg = $responseObj['error']['message'] ?? 'Unknown Error'; logActivity("Gemini API Error: $errorMsg", 'ERROR'); echo json_encode([ 'success' => false, 'message' => "Gemini API Error: " . $errorMsg, 'debug_raw' => $response ]); exit; } $llmText = $responseObj['candidates'][0]['content']['parts'][0]['text'] ?? ''; if (empty($llmText)) { echo json_encode([ 'success' => false, 'message' => "AI returned an empty response.", 'debug_raw' => $response ]); exit; } // Robust JSON Extraction $start = strpos($llmText, '{'); $end = strrpos($llmText, '}'); if ($start !== false && $end !== false) { $responseText = substr($llmText, $start, $end - $start + 1); } else { $responseText = $llmText; } $llmAction = json_decode($responseText, true); $debugInfo = [ 'debug_llm' => $llmText, 'debug_extracted' => $responseText, 'debug_json_error' => json_last_error_msg() ]; if (json_last_error() !== JSON_ERROR_NONE || !$llmAction) { echo json_encode(array_merge([ 'success' => false, 'message' => "Invalid JSON from AI", ], $debugInfo)); exit; } // Execute Action if ($llmAction['operation'] === 'create') { $newItems = $llmAction['items']; $count = 0; foreach ($newItems as $item) { if (isset($item['AssetName'])) { $item['AssetName'] = str_replace(' ', '', $item['AssetName']); } $item['Filename'] = generateFilename($item); $data[] = $item; $count++; } saveData($data); logActivity("Created $count job names via AI", 'SUCCESS'); echo json_encode(array_merge(['success' => true, 'message' => "Created $count job names.", 'count' => $count], $debugInfo)); } elseif ($llmAction['operation'] === 'update') { $updates = $llmAction['values']; $targetIds = $llmAction['target_ids'] ?? []; $count = 0; foreach ($data as &$row) { $match = false; if (!empty($targetIds)) { if (in_array($row['OMGID'] ?? '', $targetIds)) { $match = true; } } else { $match = true; } if ($match) { foreach ($updates as $key => $val) { $row[$key] = $val; } $row['Filename'] = generateFilename($row); $count++; } } saveData($data); logActivity("Updated $count items via AI", 'SUCCESS'); echo json_encode(array_merge(['success' => true, 'message' => "Updated $count items.", 'count' => $count], $debugInfo)); } elseif ($llmAction['operation'] === 'batch_update') { $updates = $llmAction['updates']; $count = 0; foreach ($updates as $update) { $id = $update['OMGID']; $values = $update['values']; foreach ($data as &$row) { if (($row['OMGID'] ?? '') === $id) { foreach ($values as $key => $val) { $row[$key] = $val; } $row['Filename'] = generateFilename($row); $count++; break; } } } saveData($data); logActivity("Batch updated $count items via AI", 'SUCCESS'); echo json_encode(array_merge(['success' => true, 'message' => "Batch updated $count items.", 'count' => $count], $debugInfo)); } elseif ($llmAction['operation'] === 'question') { logActivity("AI asked question: " . $llmAction['text'], 'QUESTION'); echo json_encode(array_merge(['success' => true, 'question' => $llmAction['text']], $debugInfo)); } else { echo json_encode(array_merge(['success' => false, 'message' => 'Unknown operation: ' . $llmAction['operation']], $debugInfo)); } exit; } } } ?>