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
+22 -7
View File
@@ -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,&quot;approved&quot;)" class="btn btn-green" style="font-size:14px;padding:7px 12px">Approve</button>'+
'<button data-sid="'+s.id+'" onclick="resolveShare(this.dataset.sid,&quot;denied&quot;)" 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,&quot;approved&quot;)" class="btn btn-green" style="font-size:14px;padding:7px 12px">Approve</button>'+
'<button data-sid="'+s.id+'" onclick="resolveShare(this.dataset.sid,&quot;denied&quot;)" 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('');
}