Changeset 3783117
- Timestamp:
- 11/04/2026 05:26:12 AM (3 hours ago)
- Location:
- fast2sms
- Files:
-
- 51 added
- 8 edited
-
tags/1.17.0 (added)
-
tags/1.17.0/admin (added)
-
tags/1.17.0/admin/index.php (added)
-
tags/1.17.0/admin/views (added)
-
tags/1.17.0/admin/views/app.php (added)
-
tags/1.17.0/admin/views/index.php (added)
-
tags/1.17.0/assets (added)
-
tags/1.17.0/assets/css (added)
-
tags/1.17.0/assets/css/admin.css (added)
-
tags/1.17.0/assets/css/index.php (added)
-
tags/1.17.0/assets/css/otp.css (added)
-
tags/1.17.0/assets/images (added)
-
tags/1.17.0/assets/images/icon.png (added)
-
tags/1.17.0/assets/images/logo.png (added)
-
tags/1.17.0/assets/index.php (added)
-
tags/1.17.0/assets/js (added)
-
tags/1.17.0/assets/js/admin.js (added)
-
tags/1.17.0/assets/js/cart-capture.js (added)
-
tags/1.17.0/assets/js/cod.js (added)
-
tags/1.17.0/assets/js/index.php (added)
-
tags/1.17.0/assets/js/notice.js (added)
-
tags/1.17.0/assets/js/otp.js (added)
-
tags/1.17.0/assets/js/stock-notify.js (added)
-
tags/1.17.0/fast2sms.php (added)
-
tags/1.17.0/includes (added)
-
tags/1.17.0/includes/class-f2s-abandoned-cart.php (added)
-
tags/1.17.0/includes/class-f2s-admin.php (added)
-
tags/1.17.0/includes/class-f2s-api.php (added)
-
tags/1.17.0/includes/class-f2s-backinstock.php (added)
-
tags/1.17.0/includes/class-f2s-campaigns.php (added)
-
tags/1.17.0/includes/class-f2s-checkout.php (added)
-
tags/1.17.0/includes/class-f2s-cod.php (added)
-
tags/1.17.0/includes/class-f2s-crypto.php (added)
-
tags/1.17.0/includes/class-f2s-events.php (added)
-
tags/1.17.0/includes/class-f2s-form-replies.php (added)
-
tags/1.17.0/includes/class-f2s-forms.php (added)
-
tags/1.17.0/includes/class-f2s-install.php (added)
-
tags/1.17.0/includes/class-f2s-invoice.php (added)
-
tags/1.17.0/includes/class-f2s-optout.php (added)
-
tags/1.17.0/includes/class-f2s-otp-login.php (added)
-
tags/1.17.0/includes/class-f2s-otp.php (added)
-
tags/1.17.0/includes/class-f2s-phone.php (added)
-
tags/1.17.0/includes/class-f2s-privacy.php (added)
-
tags/1.17.0/includes/class-f2s-queue.php (added)
-
tags/1.17.0/includes/class-f2s-settings.php (added)
-
tags/1.17.0/includes/index.php (added)
-
tags/1.17.0/languages (added)
-
tags/1.17.0/languages/index.php (added)
-
tags/1.17.0/logo.png (added)
-
tags/1.17.0/readme.txt (added)
-
tags/1.17.0/uninstall.php (added)
-
trunk/assets/js/admin.js (modified) (16 diffs)
-
trunk/fast2sms.php (modified) (2 diffs)
-
trunk/includes/class-f2s-admin.php (modified) (3 diffs)
-
trunk/includes/class-f2s-form-replies.php (modified) (15 diffs)
-
trunk/includes/class-f2s-forms.php (modified) (5 diffs)
-
trunk/includes/class-f2s-queue.php (modified) (1 diff)
-
trunk/includes/class-f2s-settings.php (modified) (1 diff)
-
trunk/readme.txt (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
fast2sms/trunk/assets/js/admin.js
r3780452 r3783117 1375 1375 } 1376 1376 1377 function frTplText(channel, sms, wa) { 1378 var parts = []; 1379 if (channel !== 'sms' && channel !== 'none' && wa.message_id) { parts.push('WhatsApp: ' + (wa.name || '#' + wa.message_id)); } 1380 if (channel !== 'whatsapp' && channel !== 'none' && sms.message_id) { parts.push('SMS: #' + sms.message_id); } 1381 return parts.join(' + '); 1382 } 1383 1377 1384 function frSummary(r) { 1378 var parts = []; 1379 if (r.channel !== 'sms' && r.whatsapp.message_id) { parts.push('WhatsApp: ' + (r.whatsapp.name || '#' + r.whatsapp.message_id)); } 1380 if (r.channel !== 'whatsapp' && r.sms.message_id) { parts.push('SMS: #' + r.sms.message_id); } 1381 return parts.join(' + ') || 'No template picked'; 1385 var out = r.channel === 'none' ? 'Customer: no message' : 'Customer → ' + (frTplText(r.channel, r.sms, r.whatsapp) || 'no template picked'); 1386 if (r.admin && Number(r.admin.enabled)) { 1387 out += ' · Admin alert → ' + (frTplText(r.admin.channel, r.admin.sms, r.admin.whatsapp) || 'no template picked'); 1388 } 1389 return out; 1382 1390 } 1383 1391 … … 1392 1400 toggleHtml('f2s-fr-on-' + i, Number(r.enabled)) + 1393 1401 '<div class="f2s-fr-main"><strong>' + esc(formLabel) + '</strong> <span class="f2s-pill f2s-pill-gray">' + esc(frSourceLabel(r.source)) + '</span>' + 1394 '<div class="f2s-muted" style="font-size:12px"> →' + esc(frSummary(r)) + (Number(r.verified_only) ? ' · only OTP-verified numbers' : '') + '</div></div>' +1402 '<div class="f2s-muted" style="font-size:12px">' + esc(frSummary(r)) + (Number(r.verified_only) ? ' · only OTP-verified numbers' : '') + '</div></div>' + 1395 1403 '<button type="button" class="f2s-btn f2s-btn-ghost f2s-btn-sm f2s-fr-edit">Edit</button>' + 1396 1404 '<button type="button" class="f2s-btn f2s-btn-ghost f2s-btn-sm f2s-fr-del" title="Delete">🗑</button>' + … … 1402 1410 return '<div class="f2s-card" id="f2s-fr-card">' + 1403 1411 '<h2>💬 Auto-reply when someone submits a form</h2>' + 1404 '<p class="f2s-sub">Send your approved WhatsApp or SMS template to the person who filled the form — e.g. “Thank you for contacting us, {name}!” . Pick the form, the template, and which form field goes into each template variable.</p>' +1412 '<p class="f2s-sub">Send your approved WhatsApp or SMS template to the person who filled the form — e.g. “Thank you for contacting us, {name}!” — and, if you like, alert your admin number with the submission details. Pick the form, the template, and which form field goes into each template variable.</p>' + 1405 1413 '<div id="f2s-fr-sources" class="f2s-muted" style="font-size:12px;margin-bottom:10px">Checking your form plugins…</div>' + 1406 1414 '<div id="f2s-fr-listbox">' + frListHtml() + '</div>' + … … 1439 1447 '<div class="f2s-field"><label>2. Which field has the customer’s mobile number?</label>' + 1440 1448 '<select class="f2s-select" id="f2s-fr-phone" style="max-width:420px"></select></div>' + 1441 '<div class="f2s-field"><label>3. Send by</label>' + segHtml('fr-channel', [{ v: 'whatsapp', l: 'WhatsApp' }, { v: 'sms', l: 'SMS' }, { v: 'both', l: 'Both' }], r.channel || 'whatsapp') + '</div>' +1442 '<div class="f2s-field" ><label>4. Template — and what goes into each variable</label>' +1449 '<div class="f2s-field"><label>3. Message to the customer</label>' + segHtml('fr-channel', [{ v: 'whatsapp', l: 'WhatsApp' }, { v: 'sms', l: 'SMS' }, { v: 'both', l: 'Both' }, { v: 'none', l: 'Don’t send' }], r.channel || 'whatsapp') + '</div>' + 1450 '<div class="f2s-field" id="f2s-fr-cust-tpl"><label>4. Customer template — and what goes into each variable</label>' + 1443 1451 '<div id="f2s-fr-wa" class="f2s-subcard"><div class="f2s-subcard-head"><span class="ch-dot ch-wa"></span>WhatsApp template</div>' + 1444 1452 '<div style="margin-bottom:10px">' + comboHtml('whatsapp') + '</div><div id="f2s-fr-wa-map"></div>' + … … 1451 1459 '</div></div>' + 1452 1460 '<div class="f2s-field f2s-row"><span style="min-width:320px">5. Only send if the number was verified with OTP <span class="f2s-muted" style="font-size:11.5px;display:block">Needs the OTP box on this form (see “OTP verification on forms” below). Best protection against fake numbers.</span></span>' + toggleHtml('f2s-fr-verified', Number(r.verified_only)) + '</div>' + 1461 '<div class="f2s-field f2s-row"><span style="min-width:320px">6. Also alert the admin <span class="f2s-muted" style="font-size:11.5px;display:block">Sends the submission details to one or more admin numbers whenever someone fills this form.</span></span>' + toggleHtml('f2s-fr-admin-on', Number(r.admin.enabled)) + '</div>' + 1462 '<div id="f2s-fr-admin" class="f2s-subcard" style="' + (Number(r.admin.enabled) ? '' : 'display:none') + '">' + 1463 '<div class="f2s-subcard-head">🔔 Admin alert</div>' + 1464 '<div class="f2s-field"><label>Send to</label>' + 1465 segHtml('fr-admin-to', [{ v: 'settings', l: 'Admin numbers in Settings (' + (FAST2SMS.adminPhones || []).length + ')' }, { v: 'custom', l: 'These numbers' }], r.admin.to === 'custom' ? 'custom' : 'settings') + 1466 '<div id="f2s-fr-admin-nums" style="margin-top:8px;' + (r.admin.to === 'custom' ? '' : 'display:none') + '">' + 1467 '<div id="f2s-fr-num-chips" class="f2s-row" style="gap:6px;flex-wrap:wrap;margin-bottom:6px"></div>' + 1468 '<input class="f2s-input" id="f2s-fr-num-add" style="max-width:420px" placeholder="Add numbers — one or more, separated by commas (e.g. 9876543210, 9123456789)" />' + 1469 '<p class="hint">Up to 10 numbers. Every number listed gets the alert.</p>' + 1470 '</div>' + 1471 '<p class="hint" id="f2s-fr-admin-settings-hint" style="' + (r.admin.to === 'custom' ? 'display:none' : '') + '">' + 1472 ((FAST2SMS.adminPhones || []).length ? 'All ' + (FAST2SMS.adminPhones || []).length + ' admin number(s) from Settings get the alert.' : '⚠️ No admin number in Settings yet — add one there, or choose “These numbers”.') + '</p>' + 1473 '</div>' + 1474 '<div class="f2s-field"><label>Send by</label>' + segHtml('fr-admin-channel', [{ v: 'whatsapp', l: 'WhatsApp' }, { v: 'sms', l: 'SMS' }, { v: 'both', l: 'Both' }], r.admin.channel || 'whatsapp') + '</div>' + 1475 '<div id="f2s-fr-aw"><div class="f2s-muted" style="font-size:12px;margin-bottom:6px">WhatsApp template</div><div style="margin-bottom:10px">' + comboHtml('whatsapp') + '</div><div id="f2s-fr-aw-map"></div>' + 1476 '<div class="f2s-wa-media" style="display:none"><div class="f2s-field"><label>Header media URL <span class="f2s-pill f2s-pill-blue f2s-wa-header-type" style="margin-left:6px"></span></label>' + 1477 '<input class="f2s-input f2s-wa-media-url" placeholder="https://yoursite.com/logo.png" value="' + esc(r.admin.whatsapp.media_url || '') + '" /></div>' + 1478 '<div class="f2s-field"><label>Document filename (optional)</label><input class="f2s-input f2s-wa-doc-name" style="max-width:300px" value="' + esc(r.admin.whatsapp.document_filename || '') + '" /></div></div>' + 1479 '</div>' + 1480 '<div id="f2s-fr-as"><div class="f2s-muted" style="font-size:12px;margin:6px 0">SMS template (DLT)</div><div style="margin-bottom:10px">' + comboHtml('sms') + '</div><div id="f2s-fr-as-map"></div></div>' + 1481 '<p class="hint">Tip: map the customer’s name, 📱 Mobile number, and any other field (e.g. website, message) into your admin template.</p>' + 1482 '</div>' + 1453 1483 '<div class="f2s-row" style="gap:8px;margin-top:6px"><button type="button" class="f2s-btn" id="f2s-fr-save">Save auto-reply</button>' + 1454 1484 '<button type="button" class="f2s-btn f2s-btn-ghost" id="f2s-fr-cancel">Cancel</button></div>' + 1455 '</div>' + previewColHtml() + '</div></div>'; 1485 '</div>' + previewColHtml().replace('<p class="f2s-preview-label">Live preview</p>', 1486 '<p class="f2s-preview-label">Live preview</p><div id="f2s-fr-prevsel" style="margin-bottom:8px;' + (Number(r.admin.enabled) ? '' : 'display:none') + '">' + 1487 segHtml('fr-prev', [{ v: 'customer', l: 'Customer' }, { v: 'admin', l: 'Admin alert' }], 'customer') + '</div>') + '</div></div>'; 1488 } 1489 1490 /* Same rule as the server (Fast2SMS_Phone::normalize): 10-digit Indian mobile, common prefixes allowed. */ 1491 function frIndianMobile(raw) { 1492 var d = String(raw || '').replace(/\D/g, ''); 1493 if (d.length === 14 && d.indexOf('0091') === 0) { d = d.substring(4); } 1494 else if (d.length === 13 && d.indexOf('910') === 0) { d = d.substring(3); } 1495 if (d.length === 12 && d.indexOf('91') === 0) { d = d.substring(2); } 1496 else if (d.length === 11 && d.charAt(0) === '0') { d = d.substring(1); } 1497 return /^[6-9]\d{9}$/.test(d); 1456 1498 } 1457 1499 … … 1466 1508 id: '', enabled: 1, source: '', form: '*', form_title: '', phone_field: '', channel: 'whatsapp', verified_only: 0, 1467 1509 sms: { type: 'sms_quick', sender_id: '', message_id: '', var_map: '', preview: '' }, 1468 whatsapp: { type: 'whatsapp', message_id: '', phone_number_id: '', name: '', var_map: '', button_var_count: 0, media_url: '', document_filename: '', preview: '', header: '', button_text: '', display_name: '' } 1510 whatsapp: { type: 'whatsapp', message_id: '', phone_number_id: '', name: '', var_map: '', button_var_count: 0, media_url: '', document_filename: '', preview: '', header: '', button_text: '', display_name: '' }, 1511 admin: { enabled: 0, channel: 'whatsapp', to: 'settings', numbers: [], 1512 sms: { type: 'sms_quick', sender_id: '', message_id: '', var_map: '', preview: '' }, 1513 whatsapp: { type: 'whatsapp', message_id: '', phone_number_id: '', name: '', var_map: '', button_var_count: 0, media_url: '', document_filename: '', preview: '', header: '', button_text: '', display_name: '' } } 1469 1514 }; 1515 } 1516 1517 /* Editor state for a saved template (no Fast2SMS round-trip needed for the preview). */ 1518 function frTplState(kind, cfg) { 1519 if (!cfg || !cfg.message_id) { return null; } 1520 var total = cfg.var_map ? cfg.var_map.split('|').length : 0; 1521 if (kind === 'sms') { 1522 return { sender_id: cfg.sender_id, message_id: cfg.message_id, preview: cfg.preview, var_count: total }; 1523 } 1524 return { 1525 message_id: cfg.message_id, phone_number_id: cfg.phone_number_id, name: cfg.name, preview: cfg.preview, header: cfg.header, 1526 button_text: cfg.button_text, display_name: cfg.display_name, button_var_count: cfg.button_var_count, 1527 var_count: total - (Number(cfg.button_var_count) || 0) 1528 }; 1529 } 1530 1531 function frSmsCfg(t, map) { 1532 return t ? { type: 'sms_quick', sender_id: t.sender_id, message_id: t.message_id, var_map: map, preview: t.preview || '' } : frBlank().sms; 1533 } 1534 function frWaCfg(t, map, media, doc) { 1535 return t ? { 1536 type: 'whatsapp', message_id: t.message_id, phone_number_id: t.phone_number_id, name: t.name || '', 1537 var_map: map, button_var_count: Number(t.button_var_count) || 0, media_url: media || '', document_filename: doc || '', 1538 preview: t.preview || '', header: t.header || '', button_text: t.button_text || '', display_name: t.display_name || '' 1539 } : frBlank().whatsapp; 1470 1540 } 1471 1541 … … 1488 1558 function frOpenEditor(p, index) { 1489 1559 var editing = index >= 0 ? JSON.parse(JSON.stringify(FAST2SMS.formReplies[index])) : frBlank(); 1560 if (!editing.admin) { editing.admin = frBlank().admin; } // saved before admin alerts existed. 1561 var numState = { list: (editing.admin.numbers || []).filter(function (n) { return n && n.id; }) }; 1562 numState.keep = numState.list.map(function (n) { return n.id; }); 1490 1563 var box = $('#f2s-fr-editor', p); 1491 1564 box.innerHTML = frEditorHtml(editing); … … 1496 1569 1497 1570 var st = { 1498 smsTpl: editing.sms.message_id ? { 1499 sender_id: editing.sms.sender_id, message_id: editing.sms.message_id, preview: editing.sms.preview, 1500 var_count: editing.sms.var_map ? editing.sms.var_map.split('|').length : 0 1501 } : null, 1502 waTpl: editing.whatsapp.message_id ? { 1503 message_id: editing.whatsapp.message_id, phone_number_id: editing.whatsapp.phone_number_id, name: editing.whatsapp.name, 1504 preview: editing.whatsapp.preview, header: editing.whatsapp.header, button_text: editing.whatsapp.button_text, 1505 display_name: editing.whatsapp.display_name, button_var_count: editing.whatsapp.button_var_count, 1506 var_count: (editing.whatsapp.var_map ? editing.whatsapp.var_map.split('|').length : 0) - (Number(editing.whatsapp.button_var_count) || 0) 1507 } : null 1571 smsTpl: frTplState('sms', editing.sms), 1572 waTpl: frTplState('whatsapp', editing.whatsapp), 1573 aSmsTpl: frTplState('sms', editing.admin.sms), 1574 aWaTpl: frTplState('whatsapp', editing.admin.whatsapp) 1508 1575 }; 1509 1576 … … 1528 1595 } 1529 1596 1597 function renderAdminMaps(waMap, smsMap) { 1598 var opts = frVarOptions(fields()); 1599 $('#f2s-fr-aw-map', box).innerHTML = st.aWaTpl 1600 ? varMapRows(Math.max(0, Number(st.aWaTpl.var_count) || 0), opts, waMap, 'fraw', Number(st.aWaTpl.button_var_count) || 0) : ''; 1601 $('#f2s-fr-as-map', box).innerHTML = st.aSmsTpl ? varMapRows(Number(st.aSmsTpl.var_count) || 0, opts, smsMap, 'fras') : ''; 1602 } 1603 1530 1604 function renderMaps(waMap, smsMap) { 1531 1605 var opts = frVarOptions(fields()); … … 1546 1620 function preview() { 1547 1621 var ch = segVal(box, 'fr-channel') || 'whatsapp'; 1622 var adminOn = $('#f2s-fr-admin-on', box).checked; 1623 var ach = segVal(box, 'fr-admin-channel') || 'whatsapp'; 1624 $('#f2s-fr-cust-tpl', box).style.display = ch === 'none' ? 'none' : ''; 1548 1625 $('#f2s-fr-wa', box).style.display = ch === 'sms' ? 'none' : ''; 1549 1626 $('#f2s-fr-sms', box).style.display = ch === 'whatsapp' ? 'none' : ''; 1627 $('#f2s-fr-admin', box).style.display = adminOn ? '' : 'none'; 1628 $('#f2s-fr-aw', box).style.display = ach === 'sms' ? 'none' : ''; 1629 $('#f2s-fr-as', box).style.display = ach === 'whatsapp' ? 'none' : ''; 1630 $('#f2s-fr-prevsel', box).style.display = adminOn ? '' : 'none'; 1631 var custom = segVal(box, 'fr-admin-to') === 'custom'; 1632 $('#f2s-fr-admin-nums', box).style.display = custom ? '' : 'none'; 1633 $('#f2s-fr-admin-settings-hint', box).style.display = custom ? 'none' : ''; 1634 var showAdmin = adminOn && (segVal(box, 'fr-prev') === 'admin' || ch === 'none'); 1635 var sTpl = showAdmin ? st.aSmsTpl : st.smsTpl; 1636 var wTpl = showAdmin ? st.aWaTpl : st.waTpl; 1550 1637 paintPreview($('.f2s-split', box), { 1551 channel: ch,1638 channel: showAdmin ? ach : (ch === 'none' ? 'whatsapp' : ch), 1552 1639 route: 'quick', 1553 smsSender: s t.smsTpl ? st.smsTpl.sender_id : '',1554 smsPreview: s t.smsTpl ? st.smsTpl.preview : '',1555 smsTokens: labelTokens(varTokens(box, 'frsms')),1640 smsSender: sTpl ? sTpl.sender_id : '', 1641 smsPreview: sTpl ? sTpl.preview : '', 1642 smsTokens: labelTokens(varTokens(box, showAdmin ? 'fras' : 'frsms')), 1556 1643 manualText: '', 1557 waPreview: st.waTpl ? st.waTpl.preview : '',1558 waHeader: st.waTpl ? (st.waTpl.header || '') : '',1559 waName: st.waTpl ? (st.waTpl.display_name || '') : '',1560 waBtnText: st.waTpl ? (st.waTpl.button_text || '') : '',1561 waTokens: labelTokens(varTokens(box, 'frwa'))1644 waPreview: wTpl ? wTpl.preview : '', 1645 waHeader: wTpl ? (wTpl.header || '') : '', 1646 waName: wTpl ? (wTpl.display_name || '') : '', 1647 waBtnText: wTpl ? (wTpl.button_text || '') : '', 1648 waTokens: labelTokens(varTokens(box, showAdmin ? 'fraw' : 'frwa')) 1562 1649 }); 1563 1650 } 1564 1651 1565 1652 renderPhone(editing.phone_field); 1653 function renderChips() { 1654 $('#f2s-fr-num-chips', box).innerHTML = numState.list.filter(function (n) { return numState.keep.indexOf(n.id) !== -1; }).map(function (n) { 1655 return '<span class="f2s-chip">' + esc(n.mask) + '<button type="button" data-id="' + esc(n.id) + '" title="Remove">✕</button></span>'; 1656 }).join(''); 1657 } 1658 $('#f2s-fr-num-chips', box).addEventListener('click', function (e) { 1659 var b = e.target.closest('button'); 1660 if (!b) { return; } 1661 var id = b.dataset.id; 1662 numState.keep = numState.keep.filter(function (x) { return x !== id; }); 1663 renderChips(); 1664 }); 1665 renderChips(); 1666 1566 1667 renderMaps(editing.whatsapp.var_map, editing.sms.var_map); 1668 renderAdminMaps(editing.admin.whatsapp.var_map, editing.admin.sms.var_map); 1567 1669 if (st.waTpl && st.waTpl.header) { applyWaHeader($('#f2s-fr-wa', box), st.waTpl); } 1568 1670 preview(); … … 1570 1672 formSel.addEventListener('change', function () { 1571 1673 var waMap = collectVarMap(box, 'frwa'), smsMap = collectVarMap(box, 'frsms'); 1674 var aWaMap = collectVarMap(box, 'fraw'), aSmsMap = collectVarMap(box, 'fras'); 1572 1675 renderPhone(''); 1573 1676 renderMaps(waMap, smsMap); 1677 renderAdminMaps(aWaMap, aSmsMap); 1574 1678 preview(); 1575 1679 }); … … 1600 1704 }); 1601 1705 if (st.waTpl) { $('#f2s-fr-wa .f2s-combo-btn', box).textContent = (st.waTpl.name || '#' + st.waTpl.message_id) + ' · saved template'; } 1706 1707 // Admin alert templates. 1708 initCombo($('#f2s-fr-aw .f2s-combo', box), { 1709 kind: 'whatsapp', 1710 selectedId: function () { return st.aWaTpl ? st.aWaTpl.message_id : ''; }, 1711 onPick: function (t) { 1712 st.aWaTpl = t; 1713 renderAdminMaps('', collectVarMap(box, 'fras')); 1714 applyWaHeader($('#f2s-fr-aw', box), t); 1715 preview(); 1716 } 1717 }); 1718 initCombo($('#f2s-fr-as .f2s-combo', box), { 1719 kind: 'sms', 1720 selectedId: function () { return st.aSmsTpl ? st.aSmsTpl.message_id : ''; }, 1721 onPick: function (t) { 1722 st.aSmsTpl = t; 1723 renderAdminMaps(collectVarMap(box, 'fraw'), ''); 1724 preview(); 1725 } 1726 }); 1727 if (st.aWaTpl) { $('#f2s-fr-aw .f2s-combo-btn', box).textContent = (st.aWaTpl.name || '#' + st.aWaTpl.message_id) + ' · saved template'; } 1728 if (st.aWaTpl && st.aWaTpl.header) { applyWaHeader($('#f2s-fr-aw', box), st.aWaTpl); } 1729 $('#f2s-fr-admin-on', box).addEventListener('change', function () { 1730 if (this.checked && $('#f2s-fr-prevsel', box)) { /* show the admin preview right away */ 1731 var btn = $('#f2s-fr-prevsel [data-val="admin"]', box); 1732 if (btn) { btn.click(); } 1733 } 1734 preview(); 1735 }); 1602 1736 1603 1737 function build() { … … 1616 1750 channel: ch, 1617 1751 verified_only: $('#f2s-fr-verified', box).checked ? 1 : 0, 1618 sms: st.smsTpl ? { 1619 type: 'sms_quick', sender_id: st.smsTpl.sender_id, message_id: st.smsTpl.message_id, 1620 var_map: collectVarMap(box, 'frsms'), preview: st.smsTpl.preview || '' 1621 } : frBlank().sms, 1622 whatsapp: st.waTpl ? { 1623 type: 'whatsapp', message_id: st.waTpl.message_id, phone_number_id: st.waTpl.phone_number_id, name: st.waTpl.name || '', 1624 var_map: collectVarMap(box, 'frwa'), button_var_count: Number(st.waTpl.button_var_count) || 0, 1625 media_url: $('.f2s-wa-media-url', waBox).value, document_filename: $('.f2s-wa-doc-name', waBox).value, 1626 preview: st.waTpl.preview || '', header: st.waTpl.header || '', button_text: st.waTpl.button_text || '', display_name: st.waTpl.display_name || '' 1627 } : frBlank().whatsapp 1752 sms: frSmsCfg(st.smsTpl, collectVarMap(box, 'frsms')), 1753 whatsapp: frWaCfg(st.waTpl, collectVarMap(box, 'frwa'), $('.f2s-wa-media-url', waBox).value, $('.f2s-wa-doc-name', waBox).value), 1754 admin: { 1755 enabled: $('#f2s-fr-admin-on', box).checked ? 1 : 0, 1756 channel: segVal(box, 'fr-admin-channel') || 'whatsapp', 1757 to: segVal(box, 'fr-admin-to') === 'custom' ? 'custom' : 'settings', 1758 keep: numState.keep.slice(), 1759 add: ($('#f2s-fr-num-add', box).value || '').trim(), 1760 sms: frSmsCfg(st.aSmsTpl, collectVarMap(box, 'fras')), 1761 whatsapp: frWaCfg(st.aWaTpl, collectVarMap(box, 'fraw'), $('#f2s-fr-aw .f2s-wa-media-url', box).value, $('#f2s-fr-aw .f2s-wa-doc-name', box).value) 1762 } 1628 1763 }; 1629 1764 } … … 1631 1766 function problems(r) { 1632 1767 if (!r.source) { return 'Choose a form first.'; } 1633 if (r.channel !== 'sms' && !r.whatsapp.message_id) { return 'Pick a WhatsApp template.'; } 1634 if (r.channel !== 'whatsapp' && !r.sms.message_id) { return 'Pick an SMS template.'; } 1768 if (r.channel === 'none' && !Number(r.admin.enabled)) { return 'Turn on the customer message or the admin alert.'; } 1769 if (r.channel !== 'none') { 1770 if (r.channel !== 'sms' && !r.whatsapp.message_id) { return 'Pick a WhatsApp template for the customer.'; } 1771 if (r.channel !== 'whatsapp' && !r.sms.message_id) { return 'Pick an SMS template for the customer.'; } 1772 } 1773 if (Number(r.admin.enabled)) { 1774 if (r.admin.to === 'custom') { 1775 var typed = (r.admin.add || '').split(/[\s,;]+/).filter(function (x) { return x; }); 1776 var bad = typed.filter(function (x) { return !frIndianMobile(x); }); 1777 if (bad.length) { return 'Not a valid 10-digit Indian mobile number: ' + bad[0]; } 1778 if (!r.admin.keep.length && !typed.length) { return 'Add at least one admin number for this alert.'; } 1779 if (r.admin.keep.length + typed.length > 10) { return 'Up to 10 admin numbers per alert.'; } 1780 } else if (!(FAST2SMS.adminPhones || []).length) { 1781 return 'Add an admin number in Settings first, or choose “These numbers”.'; 1782 } 1783 if (r.admin.channel !== 'sms' && !r.admin.whatsapp.message_id) { return 'Pick a WhatsApp template for the admin alert.'; } 1784 if (r.admin.channel !== 'sms' && r.admin.whatsapp.header && !r.admin.whatsapp.media_url) { return 'The admin WhatsApp template has a ' + r.admin.whatsapp.header.toLowerCase() + ' header — add its media URL.'; } 1785 if (r.admin.channel !== 'whatsapp' && !r.admin.sms.message_id) { return 'Pick an SMS template for the admin alert.'; } 1786 } 1635 1787 return ''; 1636 1788 } … … 1675 1827 sample['f:' + fd.key] = /mail/i.test(hay) ? 'asha@example.com' : (/phone|mobile|whatsapp|tel/i.test(hay) ? '9876543210' : (/name/i.test(hay) ? 'Asha' : fd.label)); 1676 1828 }); 1677 runTest(box, this, r.channel, r.channel !== 'whatsapp' ? r.sms : {}, r.channel !== 'sms' ? r.whatsapp : {}, sample); 1829 var adminView = Number(r.admin.enabled) && (segVal(box, 'fr-prev') === 'admin' || r.channel === 'none'); 1830 var ch = adminView ? r.admin.channel : r.channel; 1831 var smsCfg = adminView ? r.admin.sms : r.sms, waCfg = adminView ? r.admin.whatsapp : r.whatsapp; 1832 runTest(box, this, ch, ch !== 'whatsapp' ? smsCfg : {}, ch !== 'sms' ? waCfg : {}, sample); 1678 1833 }); 1679 1834 } … … 1765 1920 '<p class="f2s-sub">Stop fake submissions — visitors must verify their mobile number with OTP before the form goes through.</p>' + 1766 1921 '<div class="f2s-field"><label>Contact Form 7 ' + pill(FAST2SMS.forms.cf7) + '</label>' + 1767 '<p class="hint">Add the <code>[fast2sms_otp]</code> tag inside your form — enforcement is automatic. Optionally also list form IDs here to enforce server-side :</p>' +1922 '<p class="hint">Add the <code>[fast2sms_otp]</code> tag inside your form — enforcement is automatic. Optionally also list form IDs here to enforce server-side (the number, or the short ID from the shortcode, e.g. <code>743d511</code>):</p>' + 1768 1923 '<input class="f2s-input" id="f2s-forms-cf7" value="' + esc(s.otp_forms_cf7) + '" placeholder="e.g. 123, 456" /></div>' + 1769 1924 '<div class="f2s-field"><label>WPForms ' + pill(FAST2SMS.forms.wpforms) + '</label>' + -
fast2sms/trunk/fast2sms.php
r3780452 r3783117 4 4 * Plugin URI: https://www.fast2sms.com/help/fast2sms-wordpress-plugin-setup-guide/ 5 5 * Description: Official Fast2SMS plugin. SMS & WhatsApp order notifications, OTP login/signup, COD order verification, abandoned cart recovery, bulk campaigns and form OTP for WordPress & WooCommerce. 6 * Version: 1.1 6.06 * Version: 1.17.0 7 7 * Requires at least: 5.8 8 8 * Requires PHP: 7.4 … … 21 21 } 22 22 23 define( 'FAST2SMS_VERSION', '1.1 6.0' );23 define( 'FAST2SMS_VERSION', '1.17.0' ); 24 24 define( 'FAST2SMS_PLUGIN_FILE', __FILE__ ); 25 25 define( 'FAST2SMS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); -
fast2sms/trunk/includes/class-f2s-admin.php
r3780452 r3783117 228 228 wp_send_json_error( array( 'message' => __( 'Bad request.', 'fast2sms' ) ) ); 229 229 } 230 wp_send_json_success( array( 'replies' => Fast2SMS_Form_Replies:: save_all( $raw) ) );230 wp_send_json_success( array( 'replies' => Fast2SMS_Form_Replies::for_admin_ui( Fast2SMS_Form_Replies::save_all( $raw ) ) ) ); 231 231 } 232 232 … … 276 276 'hasKey' => Fast2SMS_Settings::has_key(), 277 277 'isAdmin' => current_user_can( 'manage_options' ), 278 'formReplies' => Fast2SMS_Form_Replies:: all(),278 'formReplies' => Fast2SMS_Form_Replies::for_admin_ui(), 279 279 'keyLast4' => (string) Fast2SMS_Settings::get( 'key_last4' ), 280 280 'wcActive' => fast2sms_wc_active(), … … 737 737 $labels['campaign'] = __( 'Campaign', 'fast2sms' ); 738 738 $labels['form_reply'] = __( 'Form auto-reply', 'fast2sms' ); 739 $labels['form_admin'] = __( 'Form alert (admin)', 'fast2sms' ); 739 740 740 741 $out = array(); -
fast2sms/trunk/includes/class-f2s-form-replies.php
r3780452 r3783117 25 25 const SEEN_OPTION = 'fast2sms_form_fields_seen'; 26 26 const MAX_REPLIES = 50; 27 28 /** Admin alerts one form may send per hour (submissions, each to every admin number). */ 29 const ADMIN_ALERTS_PER_HOUR = 30; 27 30 28 31 /** Submissions already handled in this request (some plugins fire twice). */ … … 100 103 * @return array|null Clean reply, or null when unusable. 101 104 */ 102 public static function sanitize( $raw ) {105 public static function sanitize( $raw, $prev = null ) { 103 106 if ( ! is_array( $raw ) ) { 104 107 return null; … … 109 112 return null; 110 113 } 111 $channel = in_array( $raw['channel'] ?? '', array( 'sms', 'whatsapp', 'both' ), true ) ? $raw['channel'] : 'whatsapp'; 112 $sms = is_array( $raw['sms'] ?? null ) ? $raw['sms'] : array(); 113 $wa = is_array( $raw['whatsapp'] ?? null ) ? $raw['whatsapp'] : array(); 114 $channel = in_array( $raw['channel'] ?? '', array( 'sms', 'whatsapp', 'both', 'none' ), true ) ? $raw['channel'] : 'whatsapp'; 115 $admin = is_array( $raw['admin'] ?? null ) ? $raw['admin'] : array(); 114 116 $id = preg_replace( '/[^a-z0-9]/', '', strtolower( (string) ( $raw['id'] ?? '' ) ) ); 115 117 … … 121 123 'form_title' => substr( sanitize_text_field( (string) ( $raw['form_title'] ?? '' ) ), 0, 150 ), 122 124 'phone_field' => self::key( sanitize_text_field( (string) ( $raw['phone_field'] ?? '' ) ) ), // '' = detect automatically. 123 'channel' => $channel, 125 'channel' => $channel, // customer message: whatsapp | sms | both | none. 124 126 'verified_only' => empty( $raw['verified_only'] ) ? 0 : 1, 125 'sms' => array( 126 'type' => 'sms_quick', 127 'sender_id' => substr( sanitize_text_field( (string) ( $sms['sender_id'] ?? '' ) ), 0, 20 ), 128 'message_id' => substr( preg_replace( '/[^0-9]/', '', (string) ( $sms['message_id'] ?? '' ) ), 0, 20 ), 129 'var_map' => self::clean_map( $sms['var_map'] ?? '' ), 130 'preview' => substr( sanitize_textarea_field( (string) ( $sms['preview'] ?? '' ) ), 0, 1000 ), 131 ), 132 'whatsapp' => array( 133 'type' => 'whatsapp', 134 'message_id' => substr( preg_replace( '/[^0-9]/', '', (string) ( $wa['message_id'] ?? '' ) ), 0, 20 ), 135 'phone_number_id' => substr( preg_replace( '/[^0-9]/', '', (string) ( $wa['phone_number_id'] ?? '' ) ), 0, 30 ), 136 'name' => substr( sanitize_text_field( (string) ( $wa['name'] ?? '' ) ), 0, 120 ), 137 'var_map' => self::clean_map( $wa['var_map'] ?? '' ), 138 'button_var_count' => min( 2, max( 0, (int) ( $wa['button_var_count'] ?? 0 ) ) ), 139 'media_url' => esc_url_raw( (string) ( $wa['media_url'] ?? '' ) ), 140 'document_filename' => substr( sanitize_file_name( (string) ( $wa['document_filename'] ?? '' ) ), 0, 100 ), 141 'preview' => substr( sanitize_textarea_field( (string) ( $wa['preview'] ?? '' ) ), 0, 2000 ), 142 'header' => in_array( $wa['header'] ?? '', array( 'IMAGE', 'DOCUMENT', 'VIDEO' ), true ) ? $wa['header'] : '', 143 'button_text' => substr( sanitize_text_field( (string) ( $wa['button_text'] ?? '' ) ), 0, 60 ), 144 'display_name' => substr( sanitize_text_field( (string) ( $wa['display_name'] ?? '' ) ), 0, 120 ), 127 'sms' => self::clean_sms( $raw['sms'] ?? array() ), 128 'whatsapp' => self::clean_wa( $raw['whatsapp'] ?? array() ), 129 'admin' => array( // alert to the store's admin numbers. 130 'enabled' => empty( $admin['enabled'] ) ? 0 : 1, 131 'channel' => in_array( $admin['channel'] ?? '', array( 'sms', 'whatsapp', 'both' ), true ) ? $admin['channel'] : 'whatsapp', 132 'to' => 'custom' === ( $admin['to'] ?? '' ) ? 'custom' : 'settings', // Settings numbers, or this form's own list. 133 'numbers_enc' => self::clean_numbers( $admin, is_array( $prev ) ? ( $prev['admin']['numbers_enc'] ?? '' ) : '' ), 134 'sms' => self::clean_sms( $admin['sms'] ?? array() ), 135 'whatsapp' => self::clean_wa( $admin['whatsapp'] ?? array() ), 145 136 ), 146 137 ); 138 } 139 140 /** 141 * This form's own admin numbers (encrypted, max 10). The browser never 142 * gets them back in full: it sends which saved ones to keep ("keep" = 143 * their positions) and any new ones ("add" = free text). 144 */ 145 private static function clean_numbers( array $admin, $prev_enc ) { 146 $saved = self::decrypt_numbers( $prev_enc ); 147 $list = array(); 148 if ( array_key_exists( 'keep', $admin ) ) { 149 $keep = array_map( 'strval', (array) $admin['keep'] ); 150 foreach ( $saved as $phone ) { 151 if ( in_array( self::number_id( $phone ), $keep, true ) ) { 152 $list[] = $phone; 153 } 154 } 155 } else { 156 $list = $saved; // list unchanged (e.g. only switched on/off). 157 } 158 foreach ( preg_split( '/[\s,;]+/', (string) ( $admin['add'] ?? '' ) ) as $candidate ) { 159 $phone = Fast2SMS_Phone::normalize( $candidate ); 160 if ( '' !== $phone ) { 161 $list[] = $phone; 162 } 163 } 164 $list = array_slice( array_values( array_unique( $list ) ), 0, 10 ); 165 return $list ? Fast2SMS_Crypto::encrypt( implode( ',', $list ) ) : ''; 166 } 167 168 /** Stable, non-reversible handle for a saved number (the screen never sees the number). */ 169 private static function number_id( $phone ) { 170 return substr( Fast2SMS_Crypto::hash( 'admin-number:' . $phone ), 0, 12 ); 171 } 172 173 private static function decrypt_numbers( $enc ) { 174 $plain = '' !== (string) $enc ? Fast2SMS_Crypto::decrypt( (string) $enc ) : ''; 175 return array_values( array_filter( array_map( array( 'Fast2SMS_Phone', 'normalize' ), explode( ',', (string) $plain ) ) ) ); 176 } 177 178 /** Numbers an admin alert goes to. */ 179 public static function admin_numbers( array $reply ) { 180 if ( 'custom' === ( $reply['admin']['to'] ?? '' ) ) { 181 return self::decrypt_numbers( $reply['admin']['numbers_enc'] ?? '' ); 182 } 183 return Fast2SMS_Settings::admin_phones(); 184 } 185 186 /** Replies as the admin screen sees them: numbers masked, never the encrypted blob. */ 187 public static function for_admin_ui( ?array $list = null ) { 188 $out = array(); 189 foreach ( null === $list ? self::all() : $list as $reply ) { 190 if ( isset( $reply['admin'] ) ) { 191 $reply['admin']['numbers'] = array_map( function ( $phone ) { 192 return array( 'id' => self::number_id( $phone ), 'mask' => Fast2SMS_Phone::mask( $phone ) ); 193 }, self::decrypt_numbers( $reply['admin']['numbers_enc'] ?? '' ) ); 194 unset( $reply['admin']['numbers_enc'] ); 195 } 196 $out[] = $reply; 197 } 198 return $out; 199 } 200 201 private static function clean_sms( $sms ) { 202 $sms = is_array( $sms ) ? $sms : array(); 203 return array( 204 'type' => 'sms_quick', 205 'sender_id' => substr( sanitize_text_field( (string) ( $sms['sender_id'] ?? '' ) ), 0, 20 ), 206 'message_id' => substr( preg_replace( '/[^0-9]/', '', (string) ( $sms['message_id'] ?? '' ) ), 0, 20 ), 207 'var_map' => self::clean_map( $sms['var_map'] ?? '' ), 208 'preview' => substr( sanitize_textarea_field( (string) ( $sms['preview'] ?? '' ) ), 0, 1000 ), 209 ); 210 } 211 212 private static function clean_wa( $wa ) { 213 $wa = is_array( $wa ) ? $wa : array(); 214 return array( 215 'type' => 'whatsapp', 216 'message_id' => substr( preg_replace( '/[^0-9]/', '', (string) ( $wa['message_id'] ?? '' ) ), 0, 20 ), 217 'phone_number_id' => substr( preg_replace( '/[^0-9]/', '', (string) ( $wa['phone_number_id'] ?? '' ) ), 0, 30 ), 218 'name' => substr( sanitize_text_field( (string) ( $wa['name'] ?? '' ) ), 0, 120 ), 219 'var_map' => self::clean_map( $wa['var_map'] ?? '' ), 220 'button_var_count' => min( 2, max( 0, (int) ( $wa['button_var_count'] ?? 0 ) ) ), 221 'media_url' => esc_url_raw( (string) ( $wa['media_url'] ?? '' ) ), 222 'document_filename' => substr( sanitize_file_name( (string) ( $wa['document_filename'] ?? '' ) ), 0, 100 ), 223 'preview' => substr( sanitize_textarea_field( (string) ( $wa['preview'] ?? '' ) ), 0, 2000 ), 224 'header' => in_array( $wa['header'] ?? '', array( 'IMAGE', 'DOCUMENT', 'VIDEO' ), true ) ? $wa['header'] : '', 225 'button_text' => substr( sanitize_text_field( (string) ( $wa['button_text'] ?? '' ) ), 0, 60 ), 226 'display_name' => substr( sanitize_text_field( (string) ( $wa['display_name'] ?? '' ) ), 0, 120 ), 227 ); 228 } 229 230 /** 231 * Does a per-form admin alert cover this form? Then the older global 232 * "Form submitted (notify admin)" automation stays quiet for it. 233 */ 234 public static function admin_alert_covers( $source, $form_id, array $aliases = array() ) { 235 $ids = array_filter( array_merge( array( (string) $form_id ), array_map( 'strval', $aliases ) ), 'strlen' ); 236 foreach ( self::all() as $reply ) { 237 if ( empty( $reply['enabled'] ) || empty( $reply['admin']['enabled'] ) || $reply['source'] !== $source 238 || ! ( '*' === $reply['form'] || in_array( (string) $reply['form'], $ids, true ) ) ) { 239 continue; 240 } 241 // Only when it can really send: a template picked and at least one number. 242 if ( self::channels_for( $reply['admin']['channel'], $reply['admin']['sms'], $reply['admin']['whatsapp'] ) && self::admin_numbers( $reply ) ) { 243 return true; 244 } 245 } 246 return false; 147 247 } 148 248 … … 165 265 166 266 public static function save_all( array $list ) { 267 $prev = array(); 268 foreach ( self::all() as $old ) { 269 $prev[ $old['id'] ] = $old; 270 } 167 271 $clean = array(); 168 272 foreach ( $list as $raw ) { 169 $reply = self::sanitize( $raw ); 273 $rid = is_array( $raw ) ? preg_replace( '/[^a-z0-9]/', '', strtolower( (string) ( $raw['id'] ?? '' ) ) ) : ''; 274 $reply = self::sanitize( $raw, $prev[ $rid ] ?? null ); 170 275 if ( $reply ) { 171 276 $clean[] = $reply; … … 851 956 } 852 957 958 /** Channels of a message config that actually have a template picked. */ 959 private static function channels_for( $channel, array $sms, array $wa ) { 960 $out = array(); 961 if ( 'none' === $channel ) { 962 return $out; 963 } 964 foreach ( 'both' === $channel ? array( 'whatsapp', 'sms' ) : array( $channel ) as $ch ) { 965 $cfg = 'whatsapp' === $ch ? $wa : $sms; 966 if ( ! empty( $cfg['message_id'] ) ) { 967 unset( $cfg['preview'], $cfg['name'], $cfg['header'], $cfg['button_text'], $cfg['display_name'] ); 968 $out[ $ch ] = $cfg; 969 } 970 } 971 return $out; 972 } 973 974 /** 975 * Queue the customer auto-reply and/or the admin alert for one submission. 976 * 977 * @return int Messages queued. 978 */ 853 979 private static function send_reply( array $reply, $title, array $fields ) { 854 980 global $wpdb; 855 $phone = self::find_phone( $fields, self::key( $reply['phone_field'] ) ); 856 if ( '' === $phone ) { 857 return 0; // No usable Indian mobile number in the form. 981 $phone = self::find_phone( $fields, self::key( $reply['phone_field'] ) ); 982 $customer = '' !== $phone ? self::channels_for( $reply['channel'], $reply['sms'], $reply['whatsapp'] ) : array(); 983 $admin = isset( $reply['admin'] ) && ! empty( $reply['admin']['enabled'] ) ? self::channels_for( $reply['admin']['channel'], $reply['admin']['sms'], $reply['admin']['whatsapp'] ) : array(); 984 $admins = $admin ? self::admin_numbers( $reply ) : array(); 985 if ( ! $admins ) { 986 $admin = array(); 987 } 988 if ( ! $customer && ! $admin ) { 989 return 0; // nothing to send (no customer number, no admin alert). 858 990 } 859 991 $log_channel = 'whatsapp' === $reply['channel'] ? 'whatsapp' : 'sms'; … … 861 993 $prefix = 'form_' . $reply['id'] . '_' . $content . '_'; 862 994 // Every submission is its own reply (counted against the limits); 863 // the SMS + WhatsApp rows of one reply share the key.995 // the SMS + WhatsApp rows of one customer reply share the key. 864 996 $dedupe = $prefix . strtolower( wp_generate_password( 6, false, false ) ); 865 997 866 if ( ! empty( $reply['verified_only'] ) && ! Fast2SMS_Forms::phone_verified( $phone ) ) { 867 Fast2SMS_Queue::log_skip( 'form_reply', $log_channel, $phone, __( 'Not sent: the number was not verified with OTP on this form.', 'fast2sms' ), $dedupe ); 998 // "Only OTP-verified numbers" covers the whole submission (customer + admin): 999 // unverified submissions are what bots send. 1000 if ( ! empty( $reply['verified_only'] ) && ( '' === $phone || ! Fast2SMS_Forms::phone_verified( $phone ) ) ) { 1001 if ( '' !== $phone ) { 1002 Fast2SMS_Queue::log_skip( 'form_reply', $log_channel, $phone, __( 'Not sent: the number was not verified with OTP on this form.', 'fast2sms' ), $dedupe ); 1003 } 868 1004 return 0; 869 1005 } … … 876 1012 $wpdb->suppress_errors( $suppressed ); 877 1013 if ( null !== $lock && '1' !== (string) $lock ) { 878 Fast2SMS_Queue::log_skip( 'form_reply', $log_channel, $phone, __( 'Not sent: too many form submissions at the same moment — please check for a bot attack.', 'fast2sms' ), $dedupe );1014 self::log_blocked( $customer, $admin, $admins, $phone, $log_channel, __( 'Not sent: too many form submissions at the same moment — please check for a bot attack.', 'fast2sms' ), $dedupe ); 879 1015 return 0; 880 1016 } 881 1017 try { 882 1018 // The same submission sent again within 10 minutes (double click, 883 // replayed request) gets no second message .1019 // replayed request) gets no second message — customer or admin. 884 1020 $recent = (int) $wpdb->get_var( $wpdb->prepare( 885 "SELECT COUNT(*) FROM {$wpdb->prefix}f2s_queue WHERE event = 'form_reply' AND phone_hash = %s AND dedupe_key LIKE %s AND status <> 'skipped' AND created_at > %s", 886 Fast2SMS_Crypto::hash( $phone ), 1021 "SELECT COUNT(*) FROM {$wpdb->prefix}f2s_queue WHERE dedupe_key LIKE %s AND event IN ('form_reply','form_admin') AND status <> 'skipped' AND created_at > %s", 887 1022 $wpdb->esc_like( $prefix ) . '%', 888 1023 gmdate( 'Y-m-d H:i:s', time() - 10 * MINUTE_IN_SECONDS ) … … 891 1026 return 0; 892 1027 } 893 $channels = array(); 894 foreach ( 'both' === $reply['channel'] ? array( 'whatsapp', 'sms' ) : array( $reply['channel'] ) as $channel ) { 895 $cfg = 'whatsapp' === $channel ? $reply['whatsapp'] : $reply['sms']; 896 if ( ! empty( $cfg['message_id'] ) ) { 897 $channels[ $channel ] = $cfg; // a channel with no template picked is skipped. 898 } 899 } 900 if ( ! $channels ) { 1028 1029 if ( $customer ) { 1030 $limit = self::phone_limit_error( $phone ); 1031 if ( '' !== $limit ) { 1032 Fast2SMS_Queue::log_skip( 'form_reply', $log_channel, $phone, $limit, $dedupe ); 1033 $customer = array(); // the admin is still told about the submission. 1034 } 1035 } 1036 // One form can't flood the admins: at most N alerts per form per hour. 1037 if ( $admin ) { 1038 $alerts = (int) $wpdb->get_var( $wpdb->prepare( 1039 "SELECT COUNT(DISTINCT LEFT(dedupe_key, %d)) FROM {$wpdb->prefix}f2s_queue WHERE dedupe_key LIKE %s AND event = 'form_admin' AND status <> 'skipped' AND created_at > %s", 1040 strlen( $dedupe ), 1041 $wpdb->esc_like( 'form_' . $reply['id'] . '_' ) . '%', 1042 gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS ) 1043 ) ); 1044 if ( $alerts >= self::ADMIN_ALERTS_PER_HOUR ) { 1045 self::log_blocked( array(), $admin, $admins, '', $log_channel, __( 'Not sent: this form already sent its hourly maximum of admin alerts (possible bot attack).', 'fast2sms' ), $dedupe ); 1046 $admin = array(); 1047 } 1048 } 1049 if ( ! $customer && ! $admin ) { 901 1050 return 0; 902 1051 } 903 $limit = self::limit_error( $phone, count( $channels ) ); 904 if ( '' !== $limit ) { 905 Fast2SMS_Queue::log_skip( 'form_reply', $log_channel, $phone, $limit, $dedupe ); 1052 // Site-wide hourly cap counts every message (customer + each admin number). 1053 $messages = count( $customer ) + count( $admin ) * count( $admins ); 1054 $site = self::site_limit_error( $messages ); 1055 if ( '' !== $site ) { 1056 self::log_blocked( $customer, $admin, $admins, $phone, $log_channel, $site, $dedupe ); 906 1057 return 0; 907 1058 } … … 909 1060 $vars = self::vars( $title, $fields, $phone ); 910 1061 $sent = 0; 911 foreach ( $channels as $channel => $cfg ) { 912 unset( $cfg['preview'], $cfg['name'], $cfg['header'], $cfg['button_text'], $cfg['display_name'] ); 1062 foreach ( $customer as $channel => $cfg ) { 913 1063 // Store only what this template uses (form values are personal data). 914 1064 $row = self::used_vars( $vars, $cfg ); … … 918 1068 } 919 1069 } 1070 foreach ( array_values( $admins ) as $i => $admin_phone ) { 1071 foreach ( $admin as $channel => $cfg ) { 1072 $row = self::used_vars( $vars, $cfg ); 1073 $row['_cfg'] = $cfg; 1074 // Own key per admin number: queueing one must not replace another. 1075 if ( Fast2SMS_Queue::enqueue( 'form_admin', $channel, $admin_phone, $row, array( 'dedupe_key' => $dedupe . '_a' . $i ) ) ) { 1076 $sent++; 1077 } 1078 } 1079 } 920 1080 return $sent; 921 1081 } finally { … … 923 1083 $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name ) ); 924 1084 } 1085 } 1086 } 1087 1088 /** Note a blocked submission in Logs (customer row and/or first admin number). */ 1089 private static function log_blocked( array $customer, array $admin, array $admins, $phone, $channel, $reason, $dedupe ) { 1090 if ( $customer && '' !== $phone ) { 1091 Fast2SMS_Queue::log_skip( 'form_reply', $channel, $phone, $reason, $dedupe ); 1092 } 1093 if ( $admin && $admins ) { 1094 Fast2SMS_Queue::log_skip( 'form_admin', (string) key( $admin ), reset( $admins ), $reason, $dedupe . '_a0' ); 925 1095 } 926 1096 } … … 984 1154 return $vars + array( 985 1155 'name' => '' !== $name ? $name : __( 'Customer', 'fast2sms' ), 986 'phone' => $phone,1156 'phone' => '' !== $phone ? $phone : self::raw_phone( $fields ), 987 1157 'email' => $email, 988 1158 'form_name' => (string) $title, … … 994 1164 } 995 1165 996 /** Owner limits: replies per number per day, and per hour for the whole site. */ 997 private static function limit_error( $phone, $messages = 1 ) { 1166 /** The number as typed (for admin alerts when it isn't an Indian mobile). */ 1167 private static function raw_phone( array $fields ) { 1168 foreach ( $fields as $key => $f ) { 1169 if ( preg_match( '/phone|mobile|whatsapp|tel/i', $key . ' ' . ( $f['label'] ?? '' ) . ' ' . ( $f['type'] ?? '' ) ) && '' !== $f['value'] ) { 1170 return mb_substr( preg_replace( '/[^0-9+ \-]/', '', $f['value'] ), 0, 20 ); 1171 } 1172 } 1173 return ''; 1174 } 1175 1176 /** Customer replies to one number in 24 h (owner limit). */ 1177 private static function phone_limit_error( $phone ) { 998 1178 global $wpdb; 999 1179 $per_phone = min( 50, max( 1, (int) Fast2SMS_Settings::get( 'form_reply_max_phone_day', 3 ) ) ); 1000 $per_site = min( 5000, max( 10, (int) Fast2SMS_Settings::get( 'form_reply_max_site_hour', 200 ) ) );1001 1002 1180 // One reply = one dedupe key (shared by its SMS and WhatsApp rows). 1003 1181 $replies_today = (int) $wpdb->get_var( $wpdb->prepare( … … 1006 1184 gmdate( 'Y-m-d H:i:s', time() - DAY_IN_SECONDS ) 1007 1185 ) ); 1008 if ( $replies_today >= $per_phone ) { 1009 return __( 'Not sent: daily auto-reply limit reached for this number.', 'fast2sms' ); 1010 } 1186 return $replies_today >= $per_phone ? __( 'Not sent: daily auto-reply limit reached for this number.', 'fast2sms' ) : ''; 1187 } 1188 1189 /** Messages (customer replies + admin alerts) per hour for the whole site. */ 1190 private static function site_limit_error( $messages ) { 1191 global $wpdb; 1192 $per_site = min( 5000, max( 10, (int) Fast2SMS_Settings::get( 'form_reply_max_site_hour', 200 ) ) ); 1011 1193 $site_count = (int) $wpdb->get_var( $wpdb->prepare( 1012 "SELECT COUNT(*) FROM {$wpdb->prefix}f2s_queue WHERE status IN ('pending','processing','sent','failed') AND send_after > %s AND event = 'form_reply'",1194 "SELECT COUNT(*) FROM {$wpdb->prefix}f2s_queue WHERE status IN ('pending','processing','sent','failed') AND send_after > %s AND event IN ('form_reply','form_admin')", 1013 1195 gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS ) 1014 1196 ) ); 1015 if ( $site_count + $messages > $per_site ) { // counts messages: "both" = 2. 1016 return __( 'Not sent: hourly auto-reply limit for the whole site reached (possible bot attack).', 'fast2sms' ); 1017 } 1018 return ''; 1197 return $site_count + $messages > $per_site ? __( 'Not sent: hourly form message limit for the whole site reached (possible bot attack).', 'fast2sms' ) : ''; 1019 1198 } 1020 1199 -
fast2sms/trunk/includes/class-f2s-forms.php
r3780452 r3783117 111 111 112 112 /** Queue the 'form_submission' admin notification. */ 113 private static function notify_admin( $form_name, array $fields ) {113 private static function notify_admin( $form_name, array $fields, $source = '', $form_id = '', array $aliases = array() ) { 114 114 $rule = Fast2SMS_Settings::rule( 'form_submission' ); 115 115 if ( empty( $rule['enabled'] ) || ! Fast2SMS_Settings::has_key() ) { 116 return; 117 } 118 // This form has its own admin alert (Forms tab) — don't alert twice. 119 if ( '' !== $source && Fast2SMS_Form_Replies::admin_alert_covers( $source, $form_id, $aliases ) ) { 116 120 return; 117 121 } … … 157 161 // error — enforce here for any form that contains the tag, too. 158 162 $has_tag = method_exists( $form, 'scan_form_tags' ) && ! empty( $form->scan_form_tags( array( 'type' => 'fast2sms_otp' ) ) ); 159 if ( ( ! $has_tag && ! in_array( (string) $form->id(), $enforced, true ) ) || self::posted_token_valid() ) { 163 // Listed by post ID (215) or by the short ID newer CF7 shows in its shortcode (743d511). 164 $ids = array( (string) $form->id() ); 165 if ( method_exists( $form, 'hash' ) ) { 166 $ids[] = strtolower( substr( (string) $form->hash(), 0, 7 ) ); 167 } 168 $listed = (bool) array_intersect( $ids, $enforced ); 169 if ( ( ! $has_tag && ! $listed ) || self::posted_token_valid() ) { 160 170 return $result; 161 171 } … … 178 188 $submission = class_exists( 'WPCF7_Submission' ) ? WPCF7_Submission::get_instance() : null; 179 189 $data = $submission ? $submission->get_posted_data() : array(); 180 self::notify_admin( $form->title(), is_array( $data ) ? $data : array() );190 self::notify_admin( $form->title(), is_array( $data ) ? $data : array(), 'cf7', (string) $form->id() ); 181 191 } 182 192 … … 198 208 $flat[ $key ] = isset( $field['value'] ) ? $field['value'] : ''; 199 209 } 200 self::notify_admin( isset( $form_data['settings']['form_title'] ) ? $form_data['settings']['form_title'] : 'WPForms', $flat );210 self::notify_admin( isset( $form_data['settings']['form_title'] ) ? $form_data['settings']['form_title'] : 'WPForms', $flat, 'wpforms', (string) ( $form_data['id'] ?? '' ) ); 201 211 } 202 212 … … 228 238 $fields[ $key ] = isset( $field['value'] ) ? $field['value'] : ''; 229 239 } 230 self::notify_admin( (string) $record->get_form_settings( 'form_name' ), $fields ); 240 $name = (string) $record->get_form_settings( 'form_name' ); 241 self::notify_admin( $name, $fields, 'elementor', (string) $record->get_form_settings( 'id' ), array( $name ) ); 231 242 } 232 243 -
fast2sms/trunk/includes/class-f2s-queue.php
r3780452 r3783117 498 498 private static function dispatch( $row, $phone, array $vars ) { 499 499 // Campaign and form auto-reply rows carry their own template config in vars['_cfg']. 500 if ( 'campaign' === $row->event || 'form_reply' === $row->event) {500 if ( in_array( $row->event, array( 'campaign', 'form_reply', 'form_admin' ), true ) ) { 501 501 $cfg = isset( $vars['_cfg'] ) && is_array( $vars['_cfg'] ) ? $vars['_cfg'] : array(); 502 502 return self::send_with_config( $row->channel, $phone, $cfg, $vars ); -
fast2sms/trunk/includes/class-f2s-settings.php
r3780452 r3783117 369 369 'form_submission' => array( 370 370 'label' => __( 'Form submitted (notify admin)', 'fast2sms' ), 371 'desc' => __( ' Notifies your admin number when Contact Form 7 / WPForms / Elementor forms are submitted. Set the admin number in Settings.', 'fast2sms' ),371 'desc' => __( 'One alert for all Contact Form 7 / WPForms / Elementor forms. For a per-form alert with your own template and any form field — for all 10 supported form plugins — use Forms → Auto-reply → "Also alert the admin". Set the admin number in Settings.', 'fast2sms' ), 372 372 'wc' => false, 373 373 'vars' => array( 'form_name', 'name', 'phone', 'email', 'message', 'site_name', 'site_url', 'date', 'time' ), -
fast2sms/trunk/readme.txt
r3780452 r3783117 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1.1 6.07 Stable tag: 1.17.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 51 51 * Works with Elementor Pro Forms, Contact Form 7, WPForms, Gravity Forms, Ninja Forms, Fluent Forms, Formidable Forms, Forminator, MetForm and Jetpack Forms (plus a developer hook for any other form) 52 52 * Pick the form, the template, and which form field fills each template variable — with a live phone preview and a test send 53 * Also alert the admin: a separate WhatsApp/SMS template with the submission details to your Settings admin numbers or this form's own list of one or more numbers 53 54 * Bot protection: daily limit per number, hourly limit for the whole site, do-not-send list, and an "only OTP-verified numbers" option 54 55 … … 148 149 149 150 == Changelog == 151 152 = 1.17.0 = 153 * New: admin alerts per form — in each form auto-reply, "Also alert the admin" sends its own WhatsApp/SMS template with any form fields (name, mobile, website, message…) to the admin numbers in Settings, or to this form's own list of one or more numbers. Every listed number gets the alert. 154 * The customer message can now be switched off for a form, to send only the admin alert. 155 * When a form has its own admin alert, the older "Form submitted (notify admin)" automation stays quiet for that form, so the admin is not alerted twice. Other forms are unchanged. 156 * Fix: Contact Form 7 forms listed under "OTP verification on forms" by the short ID shown in newer CF7 shortcodes (e.g. 743d511) are now enforced, as well as by number. 150 157 151 158 = 1.16.0 =
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)