Plugin Directory

Changeset 3783117


Ignore:
Timestamp:
11/04/2026 05:26:12 AM (3 hours ago)
Author:
fast2smsdev
Message:

1.17.0: Admin alerts per form - send the form submission details to one or more admin numbers with your own WhatsApp/SMS template; Contact Form 7 short form IDs supported for OTP.

Location:
fast2sms
Files:
51 added
8 edited

Legend:

Unmodified
Added
Removed
  • fast2sms/trunk/assets/js/admin.js

    r3780452 r3783117  
    13751375        }
    13761376
     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
    13771384        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;
    13821390        }
    13831391
    … …  
    13921400                                toggleHtml('f2s-fr-on-' + i, Number(r.enabled)) +
    13931401                                '<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>' +
    13951403                                '<button type="button" class="f2s-btn f2s-btn-ghost f2s-btn-sm f2s-fr-edit">Edit</button>' +
    13961404                                '<button type="button" class="f2s-btn f2s-btn-ghost f2s-btn-sm f2s-fr-del" title="Delete">🗑</button>' +
    … …  
    14021410                return '<div class="f2s-card" id="f2s-fr-card">' +
    14031411                        '<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>' +
    14051413                        '<div id="f2s-fr-sources" class="f2s-muted" style="font-size:12px;margin-bottom:10px">Checking your form plugins…</div>' +
    14061414                        '<div id="f2s-fr-listbox">' + frListHtml() + '</div>' +
    … …  
    14391447                        '<div class="f2s-field"><label>2. Which field has the customer’s mobile number?</label>' +
    14401448                                '<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>' +
    14431451                                '<div id="f2s-fr-wa" class="f2s-subcard"><div class="f2s-subcard-head"><span class="ch-dot ch-wa"></span>WhatsApp template</div>' +
    14441452                                        '<div style="margin-bottom:10px">' + comboHtml('whatsapp') + '</div><div id="f2s-fr-wa-map"></div>' +
    … …  
    14511459                                '</div></div>' +
    14521460                        '<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>' +
    14531483                        '<div class="f2s-row" style="gap:8px;margin-top:6px"><button type="button" class="f2s-btn" id="f2s-fr-save">Save auto-reply</button>' +
    14541484                                '<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);
    14561498        }
    14571499
    … …  
    14661508                        id: '', enabled: 1, source: '', form: '*', form_title: '', phone_field: '', channel: 'whatsapp', verified_only: 0,
    14671509                        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: '' } }
    14691514                };
     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;
    14701540        }
    14711541
    … …  
    14881558        function frOpenEditor(p, index) {
    14891559                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; });
    14901563                var box = $('#f2s-fr-editor', p);
    14911564                box.innerHTML = frEditorHtml(editing);
    … …  
    14961569
    14971570                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)
    15081575                };
    15091576
    … …  
    15281595                }
    15291596
     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
    15301604                function renderMaps(waMap, smsMap) {
    15311605                        var opts = frVarOptions(fields());
    … …  
    15461620                function preview() {
    15471621                        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' : '';
    15481625                        $('#f2s-fr-wa', box).style.display = ch === 'sms' ? 'none' : '';
    15491626                        $('#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;
    15501637                        paintPreview($('.f2s-split', box), {
    1551                                 channel: ch,
     1638                                channel: showAdmin ? ach : (ch === 'none' ? 'whatsapp' : ch),
    15521639                                route: 'quick',
    1553                                 smsSender: st.smsTpl ? st.smsTpl.sender_id : '',
    1554                                 smsPreview: st.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')),
    15561643                                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'))
    15621649                        });
    15631650                }
    15641651
    15651652                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
    15661667                renderMaps(editing.whatsapp.var_map, editing.sms.var_map);
     1668                renderAdminMaps(editing.admin.whatsapp.var_map, editing.admin.sms.var_map);
    15671669                if (st.waTpl && st.waTpl.header) { applyWaHeader($('#f2s-fr-wa', box), st.waTpl); }
    15681670                preview();
    … …  
    15701672                formSel.addEventListener('change', function () {
    15711673                        var waMap = collectVarMap(box, 'frwa'), smsMap = collectVarMap(box, 'frsms');
     1674                        var aWaMap = collectVarMap(box, 'fraw'), aSmsMap = collectVarMap(box, 'fras');
    15721675                        renderPhone('');
    15731676                        renderMaps(waMap, smsMap);
     1677                        renderAdminMaps(aWaMap, aSmsMap);
    15741678                        preview();
    15751679                });
    … …  
    16001704                });
    16011705                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                });
    16021736
    16031737                function build() {
    … …  
    16161750                                channel: ch,
    16171751                                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                                }
    16281763                        };
    16291764                }
    … …  
    16311766                function problems(r) {
    16321767                        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                        }
    16351787                        return '';
    16361788                }
    … …  
    16751827                                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));
    16761828                        });
    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);
    16781833                });
    16791834        }
    … …  
    17651920                                '<p class="f2s-sub">Stop fake submissions — visitors must verify their mobile number with OTP before the form goes through.</p>' +
    17661921                                '<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>' +
    17681923                                        '<input class="f2s-input" id="f2s-forms-cf7" value="' + esc(s.otp_forms_cf7) + '" placeholder="e.g. 123, 456" /></div>' +
    17691924                                '<div class="f2s-field"><label>WPForms ' + pill(FAST2SMS.forms.wpforms) + '</label>' +
  • fast2sms/trunk/fast2sms.php

    r3780452 r3783117  
    44 * Plugin URI:        https://www.fast2sms.com/help/fast2sms-wordpress-plugin-setup-guide/
    55 * 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.16.0
     6 * Version:           1.17.0
    77 * Requires at least: 5.8
    88 * Requires PHP:      7.4
    … …  
    2121}
    2222
    23 define( 'FAST2SMS_VERSION', '1.16.0' );
     23define( 'FAST2SMS_VERSION', '1.17.0' );
    2424define( 'FAST2SMS_PLUGIN_FILE', __FILE__ );
    2525define( 'FAST2SMS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • fast2sms/trunk/includes/class-f2s-admin.php

    r3780452 r3783117  
    228228                        wp_send_json_error( array( 'message' => __( 'Bad request.', 'fast2sms' ) ) );
    229229                }
    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 ) ) ) );
    231231        }
    232232
    … …  
    276276                        'hasKey'       => Fast2SMS_Settings::has_key(),
    277277                        'isAdmin'      => current_user_can( 'manage_options' ),
    278                         'formReplies'  => Fast2SMS_Form_Replies::all(),
     278                        'formReplies'  => Fast2SMS_Form_Replies::for_admin_ui(),
    279279                        'keyLast4'     => (string) Fast2SMS_Settings::get( 'key_last4' ),
    280280                        'wcActive'     => fast2sms_wc_active(),
    … …  
    737737                $labels['campaign']   = __( 'Campaign', 'fast2sms' );
    738738                $labels['form_reply'] = __( 'Form auto-reply', 'fast2sms' );
     739                $labels['form_admin'] = __( 'Form alert (admin)', 'fast2sms' );
    739740
    740741                $out = array();
  • fast2sms/trunk/includes/class-f2s-form-replies.php

    r3780452 r3783117  
    2525        const SEEN_OPTION = 'fast2sms_form_fields_seen';
    2626        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;
    2730
    2831        /** Submissions already handled in this request (some plugins fire twice). */
    … …  
    100103         * @return array|null Clean reply, or null when unusable.
    101104         */
    102         public static function sanitize( $raw ) {
     105        public static function sanitize( $raw, $prev = null ) {
    103106                if ( ! is_array( $raw ) ) {
    104107                        return null;
    … …  
    109112                        return null;
    110113                }
    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();
    114116                $id      = preg_replace( '/[^a-z0-9]/', '', strtolower( (string) ( $raw['id'] ?? '' ) ) );
    115117
    … …  
    121123                        'form_title'    => substr( sanitize_text_field( (string) ( $raw['form_title'] ?? '' ) ), 0, 150 ),
    122124                        '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.
    124126                        '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() ),
    145136                        ),
    146137                );
     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;
    147247        }
    148248
    … …  
    165265
    166266        public static function save_all( array $list ) {
     267                $prev = array();
     268                foreach ( self::all() as $old ) {
     269                        $prev[ $old['id'] ] = $old;
     270                }
    167271                $clean = array();
    168272                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 );
    170275                        if ( $reply ) {
    171276                                $clean[] = $reply;
    … …  
    851956        }
    852957
     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         */
    853979        private static function send_reply( array $reply, $title, array $fields ) {
    854980                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).
    858990                }
    859991                $log_channel = 'whatsapp' === $reply['channel'] ? 'whatsapp' : 'sms';
    … …  
    861993                $prefix      = 'form_' . $reply['id'] . '_' . $content . '_';
    862994                // 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.
    864996                $dedupe = $prefix . strtolower( wp_generate_password( 6, false, false ) );
    865997
    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                        }
    8681004                        return 0;
    8691005                }
    … …  
    8761012                $wpdb->suppress_errors( $suppressed );
    8771013                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 );
    8791015                        return 0;
    8801016                }
    8811017                try {
    8821018                        // 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.
    8841020                        $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",
    8871022                                $wpdb->esc_like( $prefix ) . '%',
    8881023                                gmdate( 'Y-m-d H:i:s', time() - 10 * MINUTE_IN_SECONDS )
    … …  
    8911026                                return 0;
    8921027                        }
    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 ) {
    9011050                                return 0;
    9021051                        }
    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 );
    9061057                                return 0;
    9071058                        }
    … …  
    9091060                        $vars = self::vars( $title, $fields, $phone );
    9101061                        $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 ) {
    9131063                                // Store only what this template uses (form values are personal data).
    9141064                                $row         = self::used_vars( $vars, $cfg );
    … …  
    9181068                                }
    9191069                        }
     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                        }
    9201080                        return $sent;
    9211081                } finally {
    … …  
    9231083                                $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name ) );
    9241084                        }
     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' );
    9251095                }
    9261096        }
    … …  
    9841154                return $vars + array(
    9851155                        'name'      => '' !== $name ? $name : __( 'Customer', 'fast2sms' ),
    986                         'phone'     => $phone,
     1156                        'phone'     => '' !== $phone ? $phone : self::raw_phone( $fields ),
    9871157                        'email'     => $email,
    9881158                        'form_name' => (string) $title,
    … …  
    9941164        }
    9951165
    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 ) {
    9981178                global $wpdb;
    9991179                $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 
    10021180                // One reply = one dedupe key (shared by its SMS and WhatsApp rows).
    10031181                $replies_today = (int) $wpdb->get_var( $wpdb->prepare(
    … …  
    10061184                        gmdate( 'Y-m-d H:i:s', time() - DAY_IN_SECONDS )
    10071185                ) );
    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 ) ) );
    10111193                $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')",
    10131195                        gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS )
    10141196                ) );
    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' ) : '';
    10191198        }
    10201199
  • fast2sms/trunk/includes/class-f2s-forms.php

    r3780452 r3783117  
    111111
    112112        /** 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() ) {
    114114                $rule = Fast2SMS_Settings::rule( 'form_submission' );
    115115                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 ) ) {
    116120                        return;
    117121                }
    … …  
    157161                // error — enforce here for any form that contains the tag, too.
    158162                $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() ) {
    160170                        return $result;
    161171                }
    … …  
    178188                $submission = class_exists( 'WPCF7_Submission' ) ? WPCF7_Submission::get_instance() : null;
    179189                $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() );
    181191        }
    182192
    … …  
    198208                        $flat[ $key ] = isset( $field['value'] ) ? $field['value'] : '';
    199209                }
    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'] ?? '' ) );
    201211        }
    202212
    … …  
    228238                        $fields[ $key ] = isset( $field['value'] ) ? $field['value'] : '';
    229239                }
    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 ) );
    231242        }
    232243
  • fast2sms/trunk/includes/class-f2s-queue.php

    r3780452 r3783117  
    498498        private static function dispatch( $row, $phone, array $vars ) {
    499499                // 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 ) ) {
    501501                        $cfg = isset( $vars['_cfg'] ) && is_array( $vars['_cfg'] ) ? $vars['_cfg'] : array();
    502502                        return self::send_with_config( $row->channel, $phone, $cfg, $vars );
  • fast2sms/trunk/includes/class-f2s-settings.php

    r3780452 r3783117  
    369369                        'form_submission' => array(
    370370                                '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' ),
    372372                                'wc'    => false,
    373373                                'vars'  => array( 'form_name', 'name', 'phone', 'email', 'message', 'site_name', 'site_url', 'date', 'time' ),
  • fast2sms/trunk/readme.txt

    r3780452 r3783117  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.16.0
     7Stable tag: 1.17.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    5151* 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)
    5252* 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
    5354* Bot protection: daily limit per number, hourly limit for the whole site, do-not-send list, and an "only OTP-verified numbers" option
    5455
    … …  
    148149
    149150== 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.
    150157
    151158= 1.16.0 =
Note: See TracChangeset for help on using the changeset viewer.