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:
2026-06-06 10:16:45 +00:00
parent f96c1b33c0
commit 8238db3026
4 changed files with 175 additions and 31 deletions
+114 -7
View File
@@ -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;
}