website fix

This commit is contained in:
Frank Harris 2025-10-22 10:03:37 -04:00
parent e14794bc59
commit 309d08497b
58 changed files with 1690 additions and 363 deletions

View file

@ -0,0 +1 @@
This folder contains compatibility wrappers for PayPal API endpoints. The canonical implementations live in /_website/api/.

View file

@ -0,0 +1,7 @@
<?php
// Local _website copy of paypal/config.php - configuration is centralized in includes/config.inc.php
// This file is intentionally lightweight and will include the site config.
require_once(__DIR__ . '/../includes/config.inc.php');
// If you need PayPal-specific overrides, add them here.
?>

103
_website/paypal/pay.php Normal file
View file

@ -0,0 +1,103 @@
<?php
// ==== YOUR CART DATA (server authoritative) ====
// TODO: set these from your cart/session/DB:
$amount = number_format(19.99, 2, '.', '');
$currency = 'USD';
$invoiceId = 'INV-' . date('Ymd-His') . '-' . bin2hex(random_bytes(3));
$customId = 'user_1234_order_5678';
$description = 'Game server monthly plan';
// Site base (adjust if different)
$siteBase = 'https://panel.iaregamer.com';
// Where your API endpoints live:
$returnUrl = $siteBase . '/_website/return.php?invoice=' . urlencode($invoiceId);
$cancelUrl = $siteBase . '/_website/return.php?invoice=' . urlencode($invoiceId) . '&cancel=1';
// Where your API endpoints live:
$apiBase = '/_website/api';
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Checkout</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- PayPal JS SDK (Sandbox). Use LIVE client-id when you go live. -->
<script src="https://www.paypal.com/sdk/js?client-id=AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c&currency=USD&intent=capture"></script>
<style>body{font-family:system-ui,Arial,sans-serif;max-width:700px;margin:40px auto;padding:0 16px}</style>
</head>
<body>
<h1>Complete your purchase</h1>
<p><strong>Amount:</strong> <?= htmlspecialchars($currency) ?> <?= htmlspecialchars($amount) ?></p>
<p><strong>Invoice:</strong> <?= htmlspecialchars($invoiceId) ?></p>
<div id="paypal-button-container"></div>
<div id="status" style="margin-top:16px"></div>
<script>
const statusEl = document.getElementById('status');
const amount = "<?= $amount ?>";
const currency = "<?= $currency ?>";
const invoice_id = "<?= $invoiceId ?>";
const custom_id = "<?= htmlspecialchars($customId, ENT_QUOTES) ?>";
const description = "<?= htmlspecialchars($description, ENT_QUOTES) ?>";
const return_url = "<?= $returnUrl ?>";
const cancel_url = "<?= $cancelUrl ?>";
function setStatus(msg){ statusEl.textContent = msg; }
paypal.Buttons({
// Show a single, small PayPal button
style: {
layout: 'vertical', // or 'horizontal'
color: 'gold', // gold | blue | silver | black | white
shape: 'pill', // pill | rect
label: 'paypal', // paypal | pay | checkout | buynow
height: 35, // 25
55 (smaller button = lower height)
tagline: false
},
fundingSource: paypal.FUNDING.PAYPAL, // only the PayPal button
createOrder: function() {
// (unchanged) 5 your fetch to create_order.php
return fetch("<?= $apiBase ?>/create_order.php", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({
amount, currency, invoice_id, custom_id, description,
return_url, cancel_url,
items, line_invoices
})
})
.then(r => r.json())
.then(d => {
if (!d.id) throw new Error(d.error || 'No order id');
return d.id;
});
},
onApprove: function(data) {
// (unchanged) 5 capture then redirect
return fetch("<?= $apiBase ?>/capture_order.php", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ order_id: data.orderID })
})
.then(r => r.json())
.then(c => {
if (c.status === 'COMPLETED') {
window.location.href = return_url;
} else {
document.getElementById('pp-status').textContent = 'Capture status: ' + c.status;
}
});
},
onCancel: function(){ window.location.href = cancel_url; },
onError: function(err){ document.getElementById('pp-status').textContent = 'PayPal error: ' + err; }
}).render('#paypal-button-container');
</script>
</body>
</html>

View file

@ -0,0 +1,4 @@
<?php
// Compatibility wrapper for old /paypal/return.php — route to unified return page
header('Location: /_website/return.php' . (isset($_SERVER['QUERY_STRING']) && $_SERVER['QUERY_STRING'] ? '?' . $_SERVER['QUERY_STRING'] : ''));
exit;

161
_website/paypal/webhook.php Normal file
View file

@ -0,0 +1,161 @@
<?php
// Full webhook implementation (migrated from top-level paypal/webhook.php)
// Uses central site config where possible; fall back to local defaults.
require_once(__DIR__ . '/../includes/config.inc.php');
$config = [
'sandbox' => true,
'client_id' => '',
'client_secret' => '',
'webhook_id' => '',
'data_dir' => realpath(__DIR__ . '/..') . DIRECTORY_SEPARATOR . 'data',
'log_file' => realpath(__DIR__ . '/..') . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'webhook.log',
];
// Allow includes/config.inc.php to override SITE_DATA_DIR if set
if (defined('SITE_DATA_DIR') && SITE_DATA_DIR) {
$config['data_dir'] = rtrim(SITE_DATA_DIR, "\\/") . DIRECTORY_SEPARATOR;
}
@mkdir($config['data_dir'], 0775, true);
function log_line($m){global $config; @file_put_contents($config['log_file'],'['.date('c')."] $m\n",FILE_APPEND);}
function api_base(){global $config; return $config['sandbox'] ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';}
http_response_code(200);
$raw = file_get_contents('php://input');
$headers = array_change_key_case(getallheaders() ?: [], CASE_UPPER);
log_line("HIT ip=".($_SERVER['REMOTE_ADDR']??'') ." bytes=".strlen($raw));
if (!$raw) { log_line("NO_BODY"); exit; }
// 1) OAuth2
$ch = curl_init(api_base().'/v1/oauth2/token');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_POST=>true,
CURLOPT_POSTFIELDS=>'grant_type=client_credentials',
CURLOPT_HTTPHEADER=>['Accept: application/json'],
CURLOPT_USERPWD=>$config['client_id'].':'.$config['client_secret'],
]);
$tokenResp = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http!==200){ log_line("OAUTH_FAIL http=$http resp=$tokenResp"); exit; }
$access_token = json_decode($tokenResp, true)['access_token'] ?? null;
if (!$access_token){ log_line("OAUTH_NO_TOKEN"); exit; }
// 2) Verify webhook signature
$verifyPayload = [
'transmission_id' => $headers['PAYPAL-TRANSMISSION-ID'] ?? '',
'transmission_time' => $headers['PAYPAL-TRANSMISSION-TIME'] ?? '',
'cert_url' => $headers['PAYPAL-CERT-URL'] ?? '',
'auth_algo' => $headers['PAYPAL-AUTH-ALGO'] ?? '',
'transmission_sig' => $headers['PAYPAL-TRANSMISSION-SIG'] ?? '',
'webhook_id' => $config['webhook_id'],
'webhook_event' => json_decode($raw, true),
];
$ch = curl_init(api_base().'/v1/notifications/verify-webhook-signature');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_POST=>true,
CURLOPT_POSTFIELDS=>json_encode($verifyPayload),
CURLOPT_HTTPHEADER=>[
'Content-Type: application/json',
'Authorization: Bearer '.$access_token
],
]);
$verifyResp = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$verifyJson = json_decode($verifyResp, true);
if ($http!==200 || ($verifyJson['verification_status'] ?? '') !== 'SUCCESS'){
log_line("VERIFY_FAIL http=$http status=".($verifyJson['verification_status']??'NONE'));
exit;
}
log_line("VERIFY_OK");
// 3) Parse and persist (now with items)
$evt = json_decode($raw, true);
$type = $evt['event_type'] ?? '';
$res = $evt['resource'] ?? [];
// Extract common fields
$invoice = $res['invoice_id'] ?? ($res['invoice_number'] ?? null);
$custom = $res['custom_id'] ?? ($res['custom'] ?? null);
// Amounts/payer
$amount = $res['amount']['value'] ?? ($res['amount']['total'] ?? null);
$currency = $res['amount']['currency_code'] ?? ($res['amount']['currency'] ?? null);
$payer = $res['payer']['email_address'] ?? ($res['payer']['payer_info']['email'] ?? null);
// Try to capture line items if present directly in this event:
$items = [];
if (isset($res['purchase_units'][0]['items']) && is_array($res['purchase_units'][0]['items'])) {
$items = $res['purchase_units'][0]['items'];
}
// If capture event, try to fetch the parent ORDER to get items
if (!$items && $type === 'PAYMENT.CAPTURE.COMPLETED') {
$orderId =
$res['supplementary_data']['related_ids']['order_id'] // preferred
?? null;
if (!$orderId && isset($res['links']) && is_array($res['links'])) {
// Fallback: look for a link to the parent order
foreach ($res['links'] as $lnk) {
if (!empty($lnk['href']) && !empty($lnk['rel']) && stripos($lnk['href'], '/v2/checkout/orders/') !== false) {
$orderId = basename(parse_url($lnk['href'], PHP_URL_PATH));
break;
}
}
}
if ($orderId) {
$ch = curl_init(api_base()."/v2/checkout/orders/".urlencode($orderId));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.$access_token,
'Content-Type: application/json'
],
]);
$orderJson = curl_exec($ch);
$httpOrder = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpOrder === 200) {
$order = json_decode($orderJson, true);
if (isset($order['purchase_units'][0]['items']) && is_array($order['purchase_units'][0]['items'])) {
$items = $order['purchase_units'][0]['items'];
}
// If the order has invoice/custom (sometimes more reliable), prefer those:
if (!$invoice) { $invoice = $order['purchase_units'][0]['invoice_id'] ?? $invoice; }
if (!$custom) { $custom = $order['purchase_units'][0]['custom_id'] ?? $custom; }
} else {
log_line("ORDER_FETCH_FAIL id=$orderId http=$httpOrder");
}
}
}
$status = 'IGNORED';
// We persist on payment completed events
if (in_array($type, ['PAYMENT.CAPTURE.COMPLETED','PAYMENT.SALE.COMPLETED'], true)) {
$record = [
'event_type' => $type,
'status' => 'PAID',
'amount' => $amount,
'currency' => $currency,
'payer' => $payer,
'invoice' => $invoice,
'custom' => $custom,
'resource_id' => $res['id'] ?? null,
'items' => $items, // Persist line items for your return.php/UI
'ts' => date('c'),
];
$name = $invoice ?: 'NO-INVOICE';
@file_put_contents($config['data_dir']."/$name.json", json_encode($record, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
$status = 'WROTE_FILE';
}
log_line("EVENT $type invoice=".($invoice ?: 'none')." items_count=".count($items)." status=$status");