diff --git a/admin/index.php b/admin/index.php index 66ea8c2..0e14c4d 100644 --- a/admin/index.php +++ b/admin/index.php @@ -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='
No '+status+' shares.
'; 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' ? - '
'+ - ''+ - '
' : ''; - return '
'+ - '
'+escHtmlA(s.alias||s.username)+' | '+escHtmlA(s.platform)+'
'+ - '
'+(s.created_at||'').substring(0,16)+' | Bonus: '+s.bonus_tokens+' tokens
'+btns+'
'; + const autoTag = parseInt(s.auto_verified) ? '⚑ auto-verified' : ''; + const verifyNote = s.verify_result && !parseInt(s.auto_verified) + ? '
Auto-check: '+(verifyLabels[s.verify_result]||s.verify_result)+'
' : ''; + const urlLink = s.share_url + ? '
πŸ”— '+escHtmlA(s.share_url.length>70?s.share_url.substring(0,70)+'…':s.share_url)+'
' + : '
No URL submitted
'; + const btns = status==='pending' + ? '
'+ + ''+ + '
' : ''; + return '
'+ + '
'+escHtmlA(s.alias||s.username)+' | '+escHtmlA(s.platform)+''+autoTag+'
'+ + '
'+(s.created_at||'').substring(0,16)+' | Bonus: '+s.bonus_tokens+' tokens
'+ + urlLink+verifyNote+btns+'
'; }).join(''); } diff --git a/api/referrals.php b/api/referrals.php index b335c9b..b0144c1 100644 --- a/api/referrals.php +++ b/api/referrals.php @@ -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; } diff --git a/db/schema.sql b/db/schema.sql index 846cc74..9a5c8cc 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -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 */; diff --git a/index.php b/index.php index 3d222b7..11eaab6 100644 --- a/index.php +++ b/index.php @@ -969,14 +969,13 @@ body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(
-
Social Share Bonus
-
Share TomTomGames on social media and earn bonus tokens when approved by our team.
-
- - - - +
Share & Earn Bonus Tokens
+
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.
+
+ +
+
Public posts (Reddit, forums, blogs) verify instantly. Facebook/Instagram/TikTok need manual review.
@@ -2277,9 +2276,23 @@ async function loadReferralDashboard() { // Social shares const sharesList = document.getElementById('ref-shares-list'); - if (sharesList && (d.social_shares||[]).length) { - sharesList.innerHTML = '
Submitted Shares:
' + - d.social_shares.map(s => `
${escHtml(s.platform)} β€” ${s.status}${s.status==='approved'?' (+'+s.bonus_tokens+' tokens)':''}
`).join(''); + if (sharesList) { + const shares = d.social_shares || []; + if (!shares.length) { + sharesList.innerHTML = ''; + } else { + sharesList.innerHTML = '
Your Submitted Shares
' + + shares.map(s => { + const autoTag = parseInt(s.auto_verified) ? ' ⚑ auto' : ''; + const urlFrag = s.share_url + ? `
${escHtml(s.share_url.length>60?s.share_url.substring(0,60)+'…':s.share_url)}
` + : ''; + return `
+
${escHtml(s.platform)} β€” ${s.status}${autoTag}${s.status==='approved'?' +'+s.bonus_tokens+' tokens':''}
+ ${urlFrag} +
`; + }).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