#!/usr/bin/perl

#                                      Copyright 2025 WebPros International, LLC
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.

package Cpanel::Server::Handlers::OpenIdConnect::CometIntercept;

use cPstrict;

=head1 NAME

Cpanel::Server::Handlers::OpenIdConnect::CometIntercept - Intercept Comet WebPros callbacks

=head1 DESCRIPTION

This module wraps the standard OpenIdConnect callback handler to detect and
handle Comet backup plugin authentication flows differently. When a callback
is detected as being for Comet (via the 'purpose' field in state), it serves
a JavaScript page that communicates with the parent window instead of creating
a server-side session.

=cut

use Cpanel::Server ();

sub serve_callback_page {
    my ($handler) = @_;    # This is the OpenIdConnect handler object

    # Serve a simple JavaScript page that extracts code/state from URL
    # and posts them to the parent window - no server-side processing needed

    my $html       = _generate_callback_page();
    my $server_obj = $handler->get_server_obj();
    my $socket     = $server_obj->connection()->get_socket();

    # Send HTTP response directly to socket with aggressive cache-busting headers
    my $content_length = length($html);
    my $etag           = time() . "-" . $$;    # Unique ETag based on time and PID

    print {$socket} "HTTP/1.1 200 OK\r\n";
    print {$socket} "Content-Type: text/html; charset=utf-8\r\n";
    print {$socket} "Content-Length: $content_length\r\n";
    print {$socket} "Cache-Control: no-cache, no-store, must-revalidate, max-age=0, private\r\n";
    print {$socket} "Pragma: no-cache\r\n";
    print {$socket} "Expires: 0\r\n";
    print {$socket} "ETag: \"$etag\"\r\n";
    print {$socket} "Vary: *\r\n";
    print {$socket} "X-Content-Type-Options: nosniff\r\n";
    print {$socket} "\r\n";
    print {$socket} $html;

    # Tell cpsrvd we've sent headers
    $server_obj->sent_headers_to_socket();

    return 1;
}

sub _generate_callback_page {

    # Generate HTML page that extracts code/state from URL via JavaScript
    # No server-side parameters needed - everything happens client-side

    return <<'HTML';
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Expires" content="0">
    <title>WebPros Authentication - Processing...</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            background: #f5f5f5;
        }
        .processing {
            text-align: center;
            background: white;
            padding: 2rem;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            max-width: 500px;
        }
        .spinner {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #3498db;
            border-radius: 50%;
            width: 40px;
            height: 40px;
            animation: spin 1s linear infinite;
            margin: 0 auto 1rem;
        }
        \@keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        .message {
            color: #666;
            margin-top: 1rem;
        }
        .error {
            color: #d32f2f;
        }
        .debug {
            margin-top: 1rem;
            padding: 1rem;
            background: #f0f0f0;
            border-radius: 4px;
            font-size: 12px;
            text-align: left;
            font-family: monospace;
            white-space: pre-wrap;
            word-break: break-all;
            display: none;
        }
        .debug.show {
            display: block;
        }
    </style>
</head>
<body>
    <div class="processing">
        <div class="spinner" id="spinner"></div>
        <h2>Processing Authentication...</h2>
        <p class="message" id="status">Please wait while we complete your authentication.</p>
        <div class="debug" id="debug"></div>
    </div>

    <script>
        (function() {
            const statusEl = document.getElementById('status');
            const debugEl = document.getElementById('debug');
            const spinnerEl = document.getElementById('spinner');
            
            function log(msg) {
                console.log('[CometAuth]', msg);
                debugEl.textContent += msg + '\n';
            }
            
            function showDebug() {
                debugEl.classList.add('show');
            }
            
            // Extract code and state from URL parameters
            const urlParams = new URLSearchParams(window.location.search);
            const code = urlParams.get('code');
            const state = urlParams.get('state');
            
            log('Code present: ' + !!code);
            log('State present: ' + !!state);
            log('window.opener present: ' + !!window.opener);
            log('Current origin: ' + window.location.origin);
            
            // Try to extract the parent origin from the state parameter
            // The state contains: |redirect-cpanel=<encoded_url>|<json_data>
            // We need to get the origin that the parent window is likely on
            let targetOrigin = window.location.origin;
            
            if (window.opener) {
                // Try to get the opener's origin directly (may fail due to cross-origin)
                try {
                    targetOrigin = window.opener.location.origin;
                    log('Got opener origin directly: ' + targetOrigin);
                } catch (e) {
                    // Cross-origin - opener.location is not accessible
                    // Use '*' as target since:
                    // 1. The auth code is one-time use and tied to PKCE verifier
                    // 2. Only the window that has the verifier can use it
                    // 3. The parent validates message origin anyway
                    log('Cannot access opener origin (cross-origin), using "*" target');
                    targetOrigin = '*';
                }
            }
            
            if (code && window.opener) {
                try {
                    log('Attempting postMessage to parent with targetOrigin: ' + targetOrigin);
                    
                    // Send the authorization code back to the parent window
                    window.opener.postMessage({
                        type: 'webpros_auth_callback',
                        code: code,
                        state: state
                    }, targetOrigin);
                    
                    log('postMessage sent successfully');
                    statusEl.textContent = 'Authentication successful! Closing window...';
                    
                    // Close this popup after a short delay
                    setTimeout(function() {
                        log('Attempting to close window...');
                        window.close();
                        // If window didn't close (some browsers block this), show message
                        setTimeout(function() {
                            statusEl.textContent = 'Authentication complete! You may close this window.';
                            spinnerEl.style.display = 'none';
                        }, 500);
                    }, 1500);
                } catch (error) {
                    log('postMessage error: ' + error.message);
                    statusEl.className = 'message error';
                    statusEl.textContent = 'Error communicating with parent window: ' + error.message;
                    spinnerEl.style.display = 'none';
                    showDebug();
                }
            } else if (!code) {
                log('ERROR: No authorization code in URL');
                log('Full URL: ' + window.location.href);
                statusEl.className = 'message error';
                statusEl.textContent = 'No authorization code received. Please close this window and try again.';
                spinnerEl.style.display = 'none';
                showDebug();
            } else if (!window.opener) {
                log('ERROR: window.opener is null - parent window reference lost');
                log('This can happen due to browser security policies after cross-origin navigation.');
                log('Code received: ' + code.substring(0, 20) + '...');
                statusEl.className = 'message error';
                statusEl.textContent = 'Parent window connection lost. This can happen due to browser security settings. Please close this window and try again.';
                spinnerEl.style.display = 'none';
                showDebug();
            }
        })();
    </script>
</body>
</html>
HTML
}

1;
