<?php
/**
 * Validate an email address.
 * Provide email address (raw input)
 * Returns true if the email address has the email 
 * address format and the domain exists.
 */
function validEmail($email) {
    $isValid = true;
    $atIndex = strrpos($email, "@");
    
    if ($atIndex === false) {
        // No @ symbol found
        $isValid = false;
    } else {
        $domain = substr($email, $atIndex + 1);
        $local = substr($email, 0, $atIndex);
        $localLen = strlen($local);
        $domainLen = strlen($domain);

        if ($localLen < 1 || $localLen > 64) {
            // Local part length exceeded
            $isValid = false;
        } else if ($domainLen < 1 || $domainLen > 255) {
            // Domain part length exceeded
            $isValid = false;
        } else if ($local[0] == '.' || $local[$localLen - 1] == '.') {
            // Local part starts or ends with '.'
            $isValid = false;
        } else if (preg_match('/\\.\\./', $local)) {
            // Local part has two consecutive dots
            $isValid = false;
        } else if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
            // Invalid characters in domain part
            $isValid = false;
        } else if (preg_match('/\\.\\./', $domain)) {
            // Domain part has two consecutive dots
            $isValid = false;
        } else if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) {
            // Invalid characters in local part unless quoted
            if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) {
                $isValid = false;
            }
        }
        
        // Check for DNS records (MX or A)
        if ($isValid && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) {
            $isValid = false;
        }
    }
    
    return $isValid;
}

/**
 * Generate a pagination string.
 * 
 * @param int $totalitems Total number of items (required)
 * @param int $page Current page number (optional, default 1)
 * @param int $limit Items per page (default 15)
 * @param int $adjacents Number of adjacent pages to show
 * @param string $targetpage URL to link to (default "/")
 * @param string $pagestring URL parameter for the page number (default "?page=")
 * @param bool $hideNextPrev Whether to hide Next/Prev buttons (default false)
 * @return string Pagination HTML
 */
function getPaginationString($totalitems, $page = 1, $limit = 15, $adjacents = 1, $targetpage = "/", $pagestring = "?page=", $hideNextPrev = false) {
    // Initialize defaults
    if (!$adjacents) $adjacents = 1;
    if (!$limit) $limit = 15;
    if (!$page) $page = 1;
    if (!$targetpage) $targetpage = "/";

    // Pagination variables
    $prev = $page - 1;
    $next = $page + 1;
    $lastpage = ceil($totalitems / $limit);
    $lpm1 = $lastpage - 1;

    $pagination = "";
    if ($lastpage > 1) {
        $pagination .= "<div class=\"pagination\">";

        // Previous button
        if (!$hideNextPrev) {
            if ($page > 1) {
                $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . $prev, ENT_QUOTES, 'UTF-8') . "\">&laquo; prev</a>";
            } else {
                $pagination .= "<span class=\"disabled\">&laquo; prev</span>";
            }
        }

        // Pages logic
        if ($lastpage < 6 + ($adjacents * 2)) {
            // Not enough pages to break it up
            for ($counter = 1; $counter <= $lastpage; $counter++) {
                if ($counter == $page) {
                    $pagination .= "<span class=\"current\">$counter</span>";
                } else {
                    $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . $counter, ENT_QUOTES, 'UTF-8') . "\">$counter</a>";
                }
            }
        } else if ($lastpage >= 6 + ($adjacents * 2)) {
            // Enough pages to hide some
            if ($page < 1 + ($adjacents * 3)) {
                // Close to beginning; only hide later pages
                for ($counter = 1; $counter < 4 + ($adjacents * 2); $counter++) {
                    if ($counter == $page) {
                        $pagination .= "<span class=\"current\">$counter</span>";
                    } else {
                        $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . $counter, ENT_QUOTES, 'UTF-8') . "\">$counter</a>";
                    }
                }
                $pagination .= "<span class=\"elipses\">...</span>";
                $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . $lpm1, ENT_QUOTES, 'UTF-8') . "\">$lpm1</a>";
                $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . $lastpage, ENT_QUOTES, 'UTF-8') . "\">$lastpage</a>";
            } elseif ($lastpage - ($adjacents * 2) > $page && $page > ($adjacents * 2)) {
                // In the middle; hide some front and back
                $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . "1", ENT_QUOTES, 'UTF-8') . "\">1</a>";
                $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . "2", ENT_QUOTES, 'UTF-8') . "\">2</a>";
                $pagination .= "<span class=\"elipses\">...</span>";
                for ($counter = $page - $adjacents; $counter <= $page + $adjacents; $counter++) {
                    if ($counter == $page) {
                        $pagination .= "<span class=\"current\">$counter</span>";
                    } else {
                        $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . $counter, ENT_QUOTES, 'UTF-8') . "\">$counter</a>";
                    }
                }
                $pagination .= "...";
                $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . $lpm1, ENT_QUOTES, 'UTF-8') . "\">$lpm1</a>";
                $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . $lastpage, ENT_QUOTES, 'UTF-8') . "\">$lastpage</a>";
            } else {
                // Close to end; only hide early pages
                $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . "1", ENT_QUOTES, 'UTF-8') . "\">1</a>";
                $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . "2", ENT_QUOTES, 'UTF-8') . "\">2</a>";
                $pagination .= "<span class=\"elipses\">...</span>";
                for ($counter = $lastpage - (1 + ($adjacents * 3)); $counter <= $lastpage; $counter++) {
                    if ($counter == $page) {
                        $pagination .= "<span class=\"current\">$counter</span>";
                    } else {
                        $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . $counter, ENT_QUOTES, 'UTF-8') . "\">$counter</a>";
                    }
                }
            }
        }

        // Next button
        if (!$hideNextPrev) {
            if ($page < $counter - 1) {
                $pagination .= "<a href=\"" . htmlspecialchars($targetpage . $pagestring . $next, ENT_QUOTES, 'UTF-8') . "\">next &raquo;</a>";
            } else {
                $pagination .= "<span class=\"disabled\">next &raquo;</span>";
            }
        }

        $pagination .= "</div>\n";
    }

    return $pagination;
}

/**
 * Is Cloudflare Turnstile configured? When it isn't, the app falls back to
 * the built-in image captcha so bot protection works out of the box.
 */
function turnstile_enabled() {
    return defined('TURNSTILE_SITEKEY') && TURNSTILE_SITEKEY !== ''
        && defined('TURNSTILE_SECRET')  && TURNSTILE_SECRET  !== '';
}

/**
 * Render the Cloudflare Turnstile widget (the loader script is included once in
 * header.php). Returns '' if Turnstile isn't configured so forms still work.
 */
function turnstile_widget() {
    if (!turnstile_enabled()) { return ''; }
    return '<div class="cf-turnstile" data-sitekey="' . htmlspecialchars(TURNSTILE_SITEKEY, ENT_QUOTES, 'UTF-8') . '" data-theme="light"></div>';
}

/**
 * Human-verification widget for public forms.
 * Uses Cloudflare Turnstile when keys are configured; otherwise renders the
 * built-in image captcha (captcha/CaptchaSecurityImages.php).
 */
function captcha_widget() {
    if (turnstile_enabled()) {
        return turnstile_widget();
    }
    return '<img src="captcha/CaptchaSecurityImages.php?width=160&height=50&characters=5&' . time() . '" alt="captcha" /><br />'
         . '<label>Enter the code shown above:</label><br />'
         . '<input name="captcha" type="text" autocomplete="off" />';
}

/**
 * Verify the human check for a submitted form.
 * Turnstile when configured, image captcha otherwise. The image captcha code
 * is single-use: it is consumed on every attempt.
 */
function captcha_verify(array $post) {
    if (turnstile_enabled()) {
        return turnstile_verify($post['cf-turnstile-response'] ?? '');
    }
    $expected = $_SESSION['security_code'] ?? '';
    unset($_SESSION['security_code']); // one-time code
    $given = trim((string)($post['captcha'] ?? ''));
    return $expected !== '' && $given !== '' && strcasecmp($given, $expected) === 0;
}

/**
 * Verify a Cloudflare Turnstile response token server-side.
 * - Not configured: allow (true) — callers use captcha_verify(), which falls
 *   back to the image captcha instead.
 * - No token (bot / no JS): reject (false).
 * - Transport error reaching Cloudflare: allow (true) + log, so a Cloudflare
 *   blip can't lock every legitimate user out.
 * - Explicit failure from Cloudflare: reject (false).
 */
function turnstile_verify($token) {
    if (!turnstile_enabled()) { return true; }
    $token = trim((string)$token);
    if ($token === '') { return false; }
    $ch = curl_init('https://challenges.cloudflare.com/turnstile/v0/siteverify');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => http_build_query([
            'secret'   => TURNSTILE_SECRET,
            'response' => $token,
            'remoteip' => (string)($_SERVER['REMOTE_ADDR'] ?? ''),
        ]),
        CURLOPT_TIMEOUT        => 8,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);
    $raw    = curl_exec($ch);
    $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($raw === false || $status !== 200) {
        error_log('turnstile_verify: transport error (http ' . $status . ') - allowing');
        return true;
    }
    $data = json_decode((string)$raw, true);
    return is_array($data) && !empty($data['success']);
}
?>