mirror of
https://github.com/myronblair/tomtomgames
synced 2026-06-30 09:41:11 -05:00
Add URL-based referral share verification with auto-scraper
Players now paste the URL of their post instead of just clicking a platform button. The server fetches the URL and looks for the player's referral code in the page content. If found, the share is auto-approved and tokens are awarded immediately. If not (login wall, private page, code missing), it falls into the pending queue with a reason so admins can click the link directly for manual review. - api/referrals.php: replace submit_share with URL-accepting version; add scrapeForReferralCode() (SSRF-guarded cURL, 8s timeout, 512KB cap) and inferPlatformFromUrl() helpers - db/schema.sql: add share_url, auto_verified, verify_result columns - index.php: replace platform buttons with URL input form; show auto- verify result inline; shares list shows URL and auto-verify badge - admin/index.php: share cards show clickable URL, auto-check result label, and auto-verified tag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+22
-7
@@ -2601,14 +2601,29 @@ async function loadAdminShares(status, btn) {
|
||||
const el = document.getElementById('ref-shares-admin-list');
|
||||
const d = await fetch('/api/referrals.php?action=admin_shares&status='+status).then(r=>r.json());
|
||||
if (!d.success||!d.shares.length) { el.innerHTML='<div class="empty" style="padding:20px;text-align:center">No '+status+' shares.</div>'; return; }
|
||||
const verifyLabels = {
|
||||
code_found: '✅ Code found in page',
|
||||
login_required: '🔒 Login required',
|
||||
unreachable: '⚠️ URL unreachable',
|
||||
not_found: '❌ Code not in page',
|
||||
site_found_no_code: '⚠️ Site mentioned, code missing',
|
||||
invalid_url: '❌ Invalid URL',
|
||||
};
|
||||
el.innerHTML = d.shares.map(s => {
|
||||
const btns = status==='pending' ?
|
||||
'<div style="display:flex;gap:6px">'+
|
||||
'<button data-sid="'+s.id+'" onclick="resolveShare(this.dataset.sid,"approved")" class="btn btn-green" style="font-size:14px;padding:7px 12px">Approve</button>'+
|
||||
'<button data-sid="'+s.id+'" onclick="resolveShare(this.dataset.sid,"denied")" class="btn btn-red" style="font-size:14px;padding:7px 12px">Deny</button></div>' : '';
|
||||
return '<div class="card" style="margin-bottom:8px;display:flex;align-items:center;gap:12px">'+
|
||||
'<div style="flex:1"><div style="font-size:15px;font-weight:700">'+escHtmlA(s.alias||s.username)+' | <span style="color:var(--cyan)">'+escHtmlA(s.platform)+'</span></div>'+
|
||||
'<div style="font-size:15px;color:var(--text2)">'+(s.created_at||'').substring(0,16)+' | Bonus: '+s.bonus_tokens+' tokens</div></div>'+btns+'</div>';
|
||||
const autoTag = parseInt(s.auto_verified) ? '<span style="font-size:12px;color:var(--green);margin-left:6px">⚡ auto-verified</span>' : '';
|
||||
const verifyNote = s.verify_result && !parseInt(s.auto_verified)
|
||||
? '<div style="font-size:13px;color:var(--text2);margin-top:2px">Auto-check: '+(verifyLabels[s.verify_result]||s.verify_result)+'</div>' : '';
|
||||
const urlLink = s.share_url
|
||||
? '<div style="margin-top:4px"><a href="'+escHtmlA(s.share_url)+'" target="_blank" rel="noopener" style="color:var(--cyan);font-size:13px;word-break:break-all">🔗 '+escHtmlA(s.share_url.length>70?s.share_url.substring(0,70)+'…':s.share_url)+'</a></div>'
|
||||
: '<div style="font-size:13px;color:var(--text2);margin-top:2px;font-style:italic">No URL submitted</div>';
|
||||
const btns = status==='pending'
|
||||
? '<div style="display:flex;gap:6px;margin-top:8px">'+
|
||||
'<button data-sid="'+s.id+'" onclick="resolveShare(this.dataset.sid,"approved")" class="btn btn-green" style="font-size:14px;padding:7px 12px">Approve</button>'+
|
||||
'<button data-sid="'+s.id+'" onclick="resolveShare(this.dataset.sid,"denied")" class="btn btn-red" style="font-size:14px;padding:7px 12px">Deny</button></div>' : '';
|
||||
return '<div class="card" style="margin-bottom:8px">'+
|
||||
'<div style="font-size:15px;font-weight:700">'+escHtmlA(s.alias||s.username)+' | <span style="color:var(--cyan)">'+escHtmlA(s.platform)+'</span>'+autoTag+'</div>'+
|
||||
'<div style="font-size:14px;color:var(--text2);margin-top:2px">'+(s.created_at||'').substring(0,16)+' | Bonus: '+s.bonus_tokens+' tokens</div>'+
|
||||
urlLink+verifyNote+btns+'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
|
||||
+114
-7
@@ -127,21 +127,128 @@ if ($method === 'GET') {
|
||||
echo json_encode(['success'=>false,'error'=>'Unknown action']); exit;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────
|
||||
|
||||
function inferPlatformFromUrl(string $url): string {
|
||||
$host = strtolower(parse_url($url, PHP_URL_HOST) ?? '');
|
||||
if (str_contains($host, 'reddit.com')) return 'reddit';
|
||||
if (str_contains($host, 'twitter.com') || str_contains($host, 'x.com')) return 'twitter';
|
||||
if (str_contains($host, 'facebook.com')) return 'facebook';
|
||||
if (str_contains($host, 'instagram.com')) return 'instagram';
|
||||
if (str_contains($host, 'tiktok.com')) return 'tiktok';
|
||||
if (str_contains($host, 'youtube.com')) return 'youtube';
|
||||
if (str_contains($host, 'discord.com')) return 'discord';
|
||||
if (str_contains($host, 'twitch.tv')) return 'twitch';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function scrapeForReferralCode(string $url, string $code): array {
|
||||
// SSRF guard — only http/https, no private/reserved IPs
|
||||
$parsed = parse_url($url);
|
||||
if (!$parsed || !in_array(strtolower($parsed['scheme'] ?? ''), ['http','https'])) {
|
||||
return ['verified'=>false,'reason'=>'invalid_url'];
|
||||
}
|
||||
$host = $parsed['host'] ?? '';
|
||||
$ip = gethostbyname($host);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||
return ['verified'=>false,'reason'=>'invalid_url'];
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 8,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 3,
|
||||
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_HTTPHEADER => ['Accept: text/html,application/xhtml+xml,*/*;q=0.8'],
|
||||
CURLOPT_BUFFERSIZE => 131072, // read up to 128KB
|
||||
]);
|
||||
$html = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlErr = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlErr || !$html) return ['verified'=>false,'reason'=>'unreachable'];
|
||||
if ($httpCode === 401 || $httpCode === 403) return ['verified'=>false,'reason'=>'login_required'];
|
||||
if ($httpCode >= 400) return ['verified'=>false,'reason'=>'unreachable'];
|
||||
|
||||
// Truncate to 512KB to avoid scanning huge pages
|
||||
$content = substr($html, 0, 524288);
|
||||
|
||||
if (stripos($content, $code) !== false) return ['verified'=>true, 'reason'=>'code_found'];
|
||||
if (stripos($content, 'tomtomgames.com') !== false) return ['verified'=>false,'reason'=>'site_found_no_code'];
|
||||
return ['verified'=>false,'reason'=>'not_found'];
|
||||
}
|
||||
|
||||
// ── POST actions ──────────────────────────────────────────
|
||||
if ($method !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; }
|
||||
$d = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if ($action === 'submit_share') {
|
||||
$url = trim($d['url'] ?? '');
|
||||
$platform = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['platform'] ?? '')));
|
||||
if (!$platform) { echo json_encode(['success'=>false,'error'=>'Platform required']); exit; }
|
||||
// Get bonus tokens for this platform from tiers config
|
||||
$bonus = 5; // default
|
||||
|
||||
if (!$url) { echo json_encode(['success'=>false,'error'=>'Please paste the URL of your post']); exit; }
|
||||
if (!filter_var($url, FILTER_VALIDATE_URL)) { echo json_encode(['success'=>false,'error'=>'That doesn\'t look like a valid URL']); exit; }
|
||||
|
||||
if (!$platform) $platform = inferPlatformFromUrl($url);
|
||||
|
||||
// Get referrer's code for scraping
|
||||
$codeRow = db()->prepare("SELECT referral_code FROM users WHERE id=?");
|
||||
$codeRow->execute([$userId]);
|
||||
$referralCode = (string)$codeRow->fetchColumn();
|
||||
|
||||
// Reject duplicate URLs from same user
|
||||
$dup = db()->prepare("SELECT id FROM referral_social_shares WHERE user_id=? AND share_url=?");
|
||||
$dup->execute([$userId, $url]);
|
||||
if ($dup->fetchColumn()) { echo json_encode(['success'=>false,'error'=>'You already submitted this URL']); exit; }
|
||||
|
||||
$bonus = 5;
|
||||
$scrape = scrapeForReferralCode($url, $referralCode);
|
||||
$status = $scrape['verified'] ? 'approved' : 'pending';
|
||||
|
||||
$reasonMessages = [
|
||||
'code_found' => null, // auto-verified, no extra message needed
|
||||
'login_required' => 'This post requires a login to view — our team will review it manually.',
|
||||
'unreachable' => "We couldn't reach that URL — our team will review it manually.",
|
||||
'not_found' => "Your referral code wasn't found on that page — our team will review it manually.",
|
||||
'site_found_no_code'=> 'TomTomGames was mentioned but your referral code wasn\'t found — our team will review it manually.',
|
||||
'invalid_url' => 'That URL doesn\'t look valid — please check and try again.',
|
||||
];
|
||||
|
||||
if ($scrape['reason'] === 'invalid_url') {
|
||||
echo json_encode(['success'=>false,'error'=>$reasonMessages['invalid_url']]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
db()->prepare("INSERT INTO referral_social_shares (user_id,platform,bonus_tokens) VALUES (?,?,?)")
|
||||
->execute([$userId, $platform, $bonus]);
|
||||
echo json_encode(['success'=>true]);
|
||||
db()->beginTransaction();
|
||||
db()->prepare("INSERT INTO referral_social_shares (user_id,platform,bonus_tokens,share_url,auto_verified,verify_result,status) VALUES (?,?,?,?,?,?,?)")
|
||||
->execute([$userId, $platform, $bonus, $url, $scrape['verified']?1:0, $scrape['reason'], $status]);
|
||||
$shareId = (int)db()->lastInsertId();
|
||||
|
||||
if ($scrape['verified']) {
|
||||
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$bonus, $userId]);
|
||||
logAdminAction('SOCIAL_SHARE_AUTO_VERIFIED', (int)$_SESSION['user_id'], 'referral_share', $shareId,
|
||||
'Auto-verified share on '.$platform.' — awarded '.$bonus.' tokens. URL: '.$url, '', 'approved', 'info');
|
||||
}
|
||||
db()->commit();
|
||||
|
||||
$resp = ['success'=>true,'auto_verified'=>$scrape['verified'],'platform'=>$platform];
|
||||
if ($scrape['verified']) {
|
||||
$resp['tokens'] = $bonus;
|
||||
$resp['message'] = '🎉 Verified! +'.$bonus.' tokens awarded automatically.';
|
||||
} else {
|
||||
$resp['pending'] = true;
|
||||
$resp['message'] = $reasonMessages[$scrape['reason']] ?? 'Submitted for manual review.';
|
||||
}
|
||||
echo json_encode($resp);
|
||||
} catch(Exception $e) {
|
||||
echo json_encode(['success'=>false,'error'=>'Already submitted for this platform']);
|
||||
db()->rollBack();
|
||||
echo json_encode(['success'=>false,'error'=>'Failed to submit share']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
+4
-1
@@ -311,6 +311,9 @@ CREATE TABLE `referral_social_shares` (
|
||||
`user_id` int(11) NOT NULL,
|
||||
`platform` varchar(50) NOT NULL,
|
||||
`bonus_tokens` decimal(10,2) DEFAULT 0.00,
|
||||
`share_url` varchar(500) DEFAULT NULL,
|
||||
`auto_verified` tinyint(1) DEFAULT 0,
|
||||
`verify_result` varchar(100) DEFAULT NULL,
|
||||
`status` enum('pending','approved','denied') DEFAULT 'pending',
|
||||
`admin_id` int(11) DEFAULT NULL,
|
||||
`created_at` datetime DEFAULT current_timestamp(),
|
||||
@@ -318,7 +321,7 @@ CREATE TABLE `referral_social_shares` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
CONSTRAINT `referral_social_shares_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `referral_tiers`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
|
||||
@@ -969,14 +969,13 @@ body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(
|
||||
|
||||
<!-- Social share -->
|
||||
<div class="card" style="margin-bottom:12px">
|
||||
<div style="font-size:14px;font-weight:700;color:var(--text2);margin-bottom:8px;text-transform:uppercase;letter-spacing:1px">Social Share Bonus</div>
|
||||
<div style="font-size:14px;color:var(--text2);margin-bottom:10px;line-height:1.5">Share TomTomGames on social media and earn bonus tokens when approved by our team.</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap" id="ref-share-btns">
|
||||
<button onclick="submitShare('facebook')" class="ref-share-btn" style="background:rgba(24,119,242,.15);border:1px solid rgba(24,119,242,.3);color:#1877f2">📘 Facebook</button>
|
||||
<button onclick="submitShare('twitter')" class="ref-share-btn" style="background:rgba(29,161,242,.15);border:1px solid rgba(29,161,242,.3);color:#1da1f2">🐦 Twitter/X</button>
|
||||
<button onclick="submitShare('instagram')" class="ref-share-btn" style="background:rgba(225,48,108,.15);border:1px solid rgba(225,48,108,.3);color:#e1306c">📸 Instagram</button>
|
||||
<button onclick="submitShare('tiktok')" class="ref-share-btn" style="background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.15);color:#fff">🎵 TikTok</button>
|
||||
<div style="font-size:14px;font-weight:700;color:var(--text2);margin-bottom:6px;text-transform:uppercase;letter-spacing:1px">Share & Earn Bonus Tokens</div>
|
||||
<div style="font-size:14px;color:var(--text2);margin-bottom:12px;line-height:1.5">Share your referral link anywhere — Reddit, Twitter/X, a forum, your blog. Paste the link to your post below and we'll try to verify it automatically.</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;margin-bottom:6px">
|
||||
<input class="fi" id="share-url-input" type="url" placeholder="https://reddit.com/r/gaming/comments/…" style="flex:1;font-size:14px">
|
||||
<button onclick="submitShareUrl()" style="background:var(--gold);border:none;color:#000;border-radius:8px;padding:10px 16px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap">🔍 Submit</button>
|
||||
</div>
|
||||
<div style="font-size:13px;color:var(--text2)">Public posts (Reddit, forums, blogs) verify instantly. Facebook/Instagram/TikTok need manual review.</div>
|
||||
<div id="ref-share-alert" class="alert" style="margin-top:8px"></div>
|
||||
<div id="ref-shares-list" style="margin-top:10px"></div>
|
||||
</div>
|
||||
@@ -2277,9 +2276,23 @@ async function loadReferralDashboard() {
|
||||
|
||||
// Social shares
|
||||
const sharesList = document.getElementById('ref-shares-list');
|
||||
if (sharesList && (d.social_shares||[]).length) {
|
||||
sharesList.innerHTML = '<div style="font-size:15px;font-weight:700;color:var(--text2);margin-bottom:6px">Submitted Shares:</div>' +
|
||||
d.social_shares.map(s => `<div style="font-size:14px;margin-bottom:4px">${escHtml(s.platform)} — <span class="ref-status-${s.status}">${s.status}</span>${s.status==='approved'?' (+'+s.bonus_tokens+' tokens)':''}</div>`).join('');
|
||||
if (sharesList) {
|
||||
const shares = d.social_shares || [];
|
||||
if (!shares.length) {
|
||||
sharesList.innerHTML = '';
|
||||
} else {
|
||||
sharesList.innerHTML = '<div style="font-size:13px;font-weight:700;color:var(--text2);margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px">Your Submitted Shares</div>' +
|
||||
shares.map(s => {
|
||||
const autoTag = parseInt(s.auto_verified) ? ' <span style="font-size:12px;background:rgba(0,255,128,.1);border:1px solid rgba(0,255,128,.2);color:var(--green);border-radius:4px;padding:1px 5px">⚡ auto</span>' : '';
|
||||
const urlFrag = s.share_url
|
||||
? `<div style="margin-top:3px"><a href="${escHtml(s.share_url)}" target="_blank" rel="noopener" style="color:var(--cyan);font-size:12px;word-break:break-all">${escHtml(s.share_url.length>60?s.share_url.substring(0,60)+'…':s.share_url)}</a></div>`
|
||||
: '';
|
||||
return `<div style="padding:8px 0;border-bottom:1px solid var(--border)">
|
||||
<div style="font-size:14px"><strong>${escHtml(s.platform)}</strong> — <span class="ref-status-${s.status}">${s.status}</span>${autoTag}${s.status==='approved'?' <span style="color:var(--green)">+'+s.bonus_tokens+' tokens</span>':''}</div>
|
||||
${urlFrag}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2293,14 +2306,20 @@ function copyReferralLink() {
|
||||
if (msg) { msg.style.display='block'; setTimeout(()=>msg.style.display='none', 2000); }
|
||||
}
|
||||
|
||||
async function submitShare(platform) {
|
||||
const al = document.getElementById('ref-share-alert');
|
||||
const d = await api('/api/referrals.php?action=submit_share', { platform });
|
||||
async function submitShareUrl() {
|
||||
const al = document.getElementById('ref-share-alert');
|
||||
const inp = document.getElementById('share-url-input');
|
||||
const url = inp?.value.trim();
|
||||
if (!url) { showAlert(al, 'Please paste the URL of your post.', 'error'); return; }
|
||||
showAlert(al, '🔍 Checking your post…', 'info');
|
||||
const d = await api('/api/referrals.php?action=submit_share', { url });
|
||||
if (d.success) {
|
||||
showAlert(al, '✓ Share submitted for ' + platform + '! Our team will review and award your bonus tokens.', 'success');
|
||||
setTimeout(() => { al.className='alert'; al.textContent=''; }, 5000);
|
||||
showAlert(al, d.message, d.auto_verified ? 'success' : 'info');
|
||||
if (inp) inp.value = '';
|
||||
loadReferralDashboard();
|
||||
} else showAlert(al, d.error || 'Error', 'error');
|
||||
} else {
|
||||
showAlert(al, d.error || 'Error submitting share.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Pass referral code during registration
|
||||
|
||||
Reference in New Issue
Block a user