Banner creatives must be images: server-side image check (extension or image/* HEAD), form hint, chatbot note
Campaign #134 (NAS ad 2806) had the member's join-page link in the image field and served a broken banner 141 times on the network; #102 did the same with an imgbb page link. imageCheck() accepts /uploads/ and instantadpay.com/banners files and image extensions outright, otherwise HEADs the URL (one redirect) and requires image/*. Wired into member and house campaign creation for banners and login-ad creatives. Form placeholder + hint point at Promo tools > Banners > Copy image URL. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -342,6 +342,30 @@ function serveJoinPage(res, tok, angle, ang, setCookies) {
|
||||
res.writeHead(200, baseHeaders(headers));
|
||||
res.end(html);
|
||||
}
|
||||
// A banner creative must be an IMAGE (2026-09-16: campaign #134 had the member's join-page link in the
|
||||
// image field and served a broken banner 141 times on the network). Accept /uploads/ files and
|
||||
// image extensions outright; otherwise HEAD the URL and require an image/* content-type.
|
||||
async function imageCheck(url) {
|
||||
const u = String(url || '').trim();
|
||||
if (/^\/uploads\//.test(u) || /^https?:\/\/instantadpay\.com\/(uploads|banners)\//i.test(u)) return { ok: true };
|
||||
if (/\.(png|jpe?g|gif|webp|svg)([?#].*)?$/i.test(u)) return { ok: true };
|
||||
try {
|
||||
const h = await headUrl(u);
|
||||
if (h && /^image\//i.test(String(h.contentType || ''))) return { ok: true };
|
||||
return { ok: false, reason: 'That link is a web page, not an image. Paste the direct image link (it usually ends in .png or .jpg), or use Promo tools > Banners > Copy image URL.' };
|
||||
} catch (e) { return { ok: false, reason: 'Could not load that image link. Paste the direct image link (ends in .png or .jpg), or use Promo tools > Banners > Copy image URL.' }; }
|
||||
}
|
||||
function headUrl(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let u; try { u = new URL(url); } catch (e) { return reject(new Error('bad url')); }
|
||||
const mod = u.protocol === 'http:' ? require('http') : require('https');
|
||||
const req = mod.request({ method: 'HEAD', hostname: u.hostname, port: u.port || undefined, path: u.pathname + u.search, timeout: 6000, headers: { 'User-Agent': 'Mozilla/5.0 (compatible; InstantAdPay-ImageCheck/1.0)' } }, r => {
|
||||
if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location && !u.searchParams.has('_r')) { try { const nx = new URL(r.headers.location, url); nx.searchParams.set('_r', '1'); return resolve(headUrl(nx.toString())); } catch (e) {} }
|
||||
resolve({ status: r.statusCode, contentType: r.headers['content-type'] || '' }); r.resume();
|
||||
});
|
||||
req.on('error', reject); req.on('timeout', () => req.destroy(new Error('timeout'))); req.end();
|
||||
});
|
||||
}
|
||||
async function frameCheck(url) {
|
||||
const h = await frameFetch(url, 0);
|
||||
if (h.error) return { ok: false, reason: 'We checked your URL and ' + h.error + '. Fix the URL and try again.' };
|
||||
@@ -2616,6 +2640,7 @@ const server = http.createServer(async (req, res) => {
|
||||
const memberId = await auth.refreshMemberId(s); // 0 is fine: earned credits fund banner/text
|
||||
const b = await readBody(req);
|
||||
if (!['login', 'solo', 'video', 'featured'].includes(String(b.type || ''))) { // banner/text surf views frame the target; login/video/solo/featured open in a new tab or play in our own player
|
||||
if (b.type === 'banner' || (b.type === 'login' && b.imageUrl)) { const ic = await imageCheck(b.imageUrl); if (!ic.ok) return json(res, 400, { error: ic.reason }); }
|
||||
const fc = await frameCheck(b.targetUrl);
|
||||
if (!fc.ok) return json(res, 400, { error: fc.reason });
|
||||
}
|
||||
@@ -2744,6 +2769,7 @@ const server = http.createServer(async (req, res) => {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const b = await readBody(req);
|
||||
if (!['login', 'solo', 'video', 'featured'].includes(String(b.type || ''))) {
|
||||
if (b.type === 'banner' || (b.type === 'login' && b.imageUrl)) { const ic = await imageCheck(b.imageUrl); if (!ic.ok) return json(res, 400, { error: ic.reason }); }
|
||||
const fc = await frameCheck(b.targetUrl);
|
||||
if (!fc.ok) return json(res, 400, { error: fc.reason });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user