PageSourceSearch

https://dapacheckerpro.com/wp-content/plugins/dapachecker/assets/js/tooldapa.js?ver=1788434456

js dapacheckerpro.com collected 2026-09-26 05:33:57 UTC 82,334 bytes, 1,893 lines download raw bytes

1/* Remove legacy ?single_login= from URL ASAP (some themes/plugins append it after forced logout — old JS showed a misleading popup). */
2(function () {
3    try {
4        if (!window.location.search || window.location.search.indexOf('single_login') === -1) {
5            return;
6        }
7        var url = new URL(window.location.href);
8        url.searchParams.delete('single_login');
9        var q = url.searchParams.toString();
10        var path = url.pathname + (q ? '?' + q : '') + url.hash;
11        window.history.replaceState({}, '', path);
12    } catch (ignore) {}
13})();
14
15var $checker_limit = params['limit'];
16/* `$chunk_size` is the MAX URLs per upstream POST (premium=500 / free=10).
17 * The INPUT cap (`$checker_limit`) may be larger (default 700) — the server
18 * splits into chunk-sized batches. Default premium: whole Check = 1 request.
19 * Custom URL-cap premium (>700): requests = ceil(urls / billingUnit). */
20var $chunk_size    = parseInt(params['chunkSize'] || 0, 10) || (params['isPremium'] ? 500 : 10);
21var $is_premium    = !!params['isPremium'];
22var $is_logged_in  = !!params['isLoggedIn'];
23var $request_cap        = parseInt(params['requestCap'] || 0, 10) || 0;
24var $request_used       = parseInt(params['requestUsed'] || 0, 10) || 0;
25var $request_remaining  = parseInt(params['requestRemaining'] || 0, 10) || 0;
26var $lifetime_urls      = parseInt(params['lifetimeUrls'] || 0, 10) || 0;
27var $upgrade_url        = params['upgradeUrl'] || 'https://dapacheckerpro.com/pricing/';
28var $login_url          = params['loginUrl'] || '/wp-login.php';
29var $register_url       = params['registerUrl'] || '';
30var $guest_cap          = parseInt(params['guestCap'] || 0, 10) || 0;
31var $guest_used         = parseInt(params['guestUsed'] || 0, 10) || 0;
32var $guest_remaining    = parseInt(params['guestRemaining'] || 0, 10) || 0;
33/* Daily cap for logged-in accounts without premium. 0 = admin set no cap. */
34var $free_daily_cap       = parseInt(params['freeDailyCap'] || 0, 10) || 0;
35var $free_daily_used      = parseInt(params['freeDailyUsed'] || 0, 10) || 0;
36var $free_daily_remaining = parseInt(params['freeDailyRemaining'] || 0, 10) || 0;
37var $uses_custom_url_billing = !!params['usesCustomUrlBilling'];
38var $billing_unit            = parseInt(params['billingUnit'] || 0, 10) || 700;
39var $requires_turnstile      = !!params['requiresTurnstile'] && !$is_premium;
40var $turnstile_sitekey       = params['turnstileSiteKey'] || '';
41
42var dpcTurnstileToken    = '';
43var dpcTurnstileTicket   = '';
44var dpcTurnstileWidgetId = null;
45var dpcPendingDomains    = null;
46var dpcTurnstileBusy     = false;
47
48function dpcTurnstileRequired() {
49    return $requires_turnstile && !!$turnstile_sitekey;
50}
51
52function dpcTurnstileWrap() {
53    return document.getElementById('dapa-turnstile-wrap');
54}
55
56function dpcShowTurnstile() {
57    var wrap = dpcTurnstileWrap();
58    if (wrap) {
59        wrap.classList.add('is-visible');
60    }
61}
62
63function dpcHideTurnstile() {
64    var wrap = dpcTurnstileWrap();
65    if (wrap) {
66        wrap.classList.remove('is-visible');
67    }
68}
69
70function dpcTurnstileMessage(text) {
71    var el = document.getElementById('dapa-turnstile');
72    if (!el) {
73        return;
74    }
75    el.innerHTML = text
76        ? '<div class="dapa-turnstile-msg">' + text + '</div>'
77        : '';
78}
79
80window.dpcOnTurnstileSuccess = function (token) {
81    dpcTurnstileToken = token || '';
82    if (dpcPendingDomains && dpcTurnstileToken) {
83        var domains = dpcPendingDomains;
84        dpcPendingDomains = null;
85        startChecking(domains);
86    }
87};
88window.dpcOnTurnstileExpired = function () {
89    dpcTurnstileToken = '';
90};
91window.dpcOnTurnstileError = function () {
92    dpcTurnstileToken = '';
93};
94
95function dpcRenderTurnstile() {
96    if (!dpcTurnstileRequired()) {
97        return;
98    }
99    var el = document.getElementById('dapa-turnstile');
100    if (!el) {
101        return;
102    }
103    if (dpcTurnstileWidgetId !== null) {
104        return;
105    }
106    if (typeof turnstile === 'undefined' || typeof turnstile.render !== 'function') {
107        dpcTurnstileMessage('Captcha failed to load. Please refresh and try again.');
108        return;
109    }
110    dpcTurnstileMessage('');
111    dpcTurnstileWidgetId = turnstile.render(el, {
112        sitekey: $turnstile_sitekey,
113        action: 'dapa-check',
114        appearance: 'always',
115        size: 'normal',
116        theme: 'light',
117        callback: window.dpcOnTurnstileSuccess,
118        'expired-callback': window.dpcOnTurnstileExpired,
119        'error-callback': function () {
120            dpcTurnstileToken = '';
121            dpcTurnstileMessage('Captcha could not display. In Cloudflare use Managed widget, and add this site domain.');
122        }
123    });
124}
125
126function dpcLoadTurnstileApi(done) {
127    if (typeof turnstile !== 'undefined' && typeof turnstile.render === 'function') {
128        done();
129        return;
130    }
131    var existing = document.getElementById('cf-turnstile-api');
132    if (!existing) {
133        existing = document.createElement('script');
134        existing.id = 'cf-turnstile-api';
135        existing.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
136        existing.async = true;
137        existing.defer = true;
138        existing.onerror = function () {
139            dpcTurnstileBusy = false;
140            dpcTurnstileMessage('Captcha script blocked. Disable adblock and try again.');
141        };
142        document.head.appendChild(existing);
143    }
144    var tries = 0;
145    var timer = setInterval(function () {
146        tries += 1;
147        if (typeof turnstile !== 'undefined' && typeof turnstile.render === 'function') {
148            clearInterval(timer);
149            dpcTurnstileBusy = false;
150            done();
151            return;
152        }
153        if (tries > 50) {
154            clearInterval(timer);
155            dpcTurnstileBusy = false;
156            dpcTurnstileMessage('Captcha failed to load. Please refresh and try again.');
157        }
158    }, 100);
159}
160
161function dpcRequestTurnstile() {
162    if (dpcTurnstileWidgetId !== null) {
163        return;
164    }
165    if (dpcTurnstileBusy) {
166        return;
167    }
168    dpcTurnstileBusy = true;
169    dpcLoadTurnstileApi(function () {
170        dpcTurnstileBusy = false;
171        var run = function () {
172            window.setTimeout(function () {
173                dpcRenderTurnstile();
174            }, 50);
175        };
176        if (typeof turnstile.ready === 'function') {
177            turnstile.ready(run);
178        } else {
179            run();
180        }
181    });
182}
183
184function dpcResetTurnstile() {
185    dpcTurnstileToken = '';
186    dpcTurnstileTicket = '';
187    dpcPendingDomains = null;
188    if (typeof turnstile !== 'undefined') {
189        try {
190            if (dpcTurnstileWidgetId !== null && typeof turnstile.remove === 'function') {
191                turnstile.remove(dpcTurnstileWidgetId);
192            } else if (dpcTurnstileWidgetId !== null && typeof turnstile.reset === 'function') {
193                turnstile.reset(dpcTurnstileWidgetId);
194            }
195        } catch (ignore) {}
196    }
197    dpcTurnstileWidgetId = null;
198    var el = document.getElementById('dapa-turnstile');
199    if (el) {
200        el.innerHTML = '';
201    }
202    dpcHideTurnstile();
203}
204
205function dpcShowCaptchaRequired(doReset) {
206    if (doReset !== false) {
207        dpcResetTurnstile();
208    }
209    if (typeof Swal !== 'undefined' && Swal.fire) {
210        Swal.fire({
211            title: 'Please complete the captcha',
212            text: 'Solve the captcha above the Check button, then try again.',
213            icon: 'info'
214        });
215    } else if (typeof swal !== 'undefined' && swal.fire) {
216        swal.fire('Please complete the captcha.');
217    }
218}
219
220function dpcEnsureTurnstileThenStart(uniqueDomains) {
221    if (!dpcTurnstileRequired()) {
222        startChecking(uniqueDomains);
223        return;
224    }
225    if (dpcTurnstileToken) {
226        startChecking(uniqueDomains);
227        return;
228    }
229    dpcPendingDomains = uniqueDomains;
230    dpcShowTurnstile();
231    dpcRequestTurnstile();
232}
233
234/**
235 * Period-request units this Check will consume.
236 * Custom URL-cap premium: ceil(urls / 700); otherwise 1.
237 */
238function dpcRequestsNeeded(urlCount) {
239    urlCount = parseInt(urlCount, 10) || 0;
240    if (urlCount < 1) return 0;
241    if ($uses_custom_url_billing && $billing_unit > 0) {
242        return Math.max(1, Math.ceil(urlCount / $billing_unit));
243    }
244    return 1;
245}
246
247/* Virtual-scroll + sort/filter/export state.
248 *
249 * `dpcAllRowsData` is the canonical row store; HTML strings are derived on
250 * demand. This lets us drop them into Clusterize.js for rendering, while
251 * still being able to sort/filter/export without scanning the (potentially
252 * truncated) live DOM. */
253var dpcAllRowsData = [];
254var dpcClusterize  = null;
255
256var submit_button = document.getElementById('urls-submit');
257
258var focusInput = document.getElementById('urls-input');
259
260submit_button.addEventListener('click', handleInputURLs);
261
262focusInput.addEventListener("input", countURLs);
263
264/* ------------------------------------------------------------------
265 * Free-tier upgrade popup (shown on hover of blurred cells)
266 * ----------------------------------------------------------------- */
267(function () {
268    var popup = document.getElementById('dpc-upgrade-popup');
269    if (!popup) return;
270    popup.innerHTML = ''
271        + '<div class="dpc-upgrade-popup__title">Premium feature</div>'
272        + '<div class="dpc-upgrade-popup__body">The free plan hides some results. Unlock every DA, PA, Spam Score, Linking Domains, Inbound Links, IP and Domain Age with the Premium plan.</div>'
273        + '<a class="dpc-upgrade-popup__cta" href="' + $upgrade_url + '" target="_blank" rel="noopener">Upgrade to Premium</a>';
274
275    var hideTimer = null;
276    function positionFor(el) {
277        var rect = el.getBoundingClientRect();
278        var pw   = popup.offsetWidth;
279        var ph   = popup.offsetHeight;
280        var top  = rect.bottom + 8;
281        var left = rect.left + (rect.width / 2) - (pw / 2);
282        if (left < 8) left = 8;
283        if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
284        if (top + ph >
284 window.innerHeight - 8) top = rect.top - ph - 8;
285        popup.style.top = top + 'px';
286        popup.style.left = left + 'px';
287    }
288    function show(el) {
289        if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
290        popup.classList.add('is-visible');
291        popup.setAttribute('aria-hidden', 'false');
292        positionFor(el);
293    }
294    function hideSoon() {
295        if (hideTimer) clearTimeout(hideTimer);
296        hideTimer = setTimeout(function () {
297            popup.classList.remove('is-visible');
298            popup.setAttribute('aria-hidden', 'true');
299        }, 180);
300    }
301    document.addEventListener('mouseover', function (ev) {
302        var t = ev.target;
303        if (t && t.classList && t.classList.contains('dpc-blur')) show(t);
304        else if (t === popup || (popup.contains && popup.contains(t))) {
305            if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
306        }
307    });
308    document.addEventListener('mouseout', function (ev) {
309        var t = ev.target;
310        if (t && t.classList && t.classList.contains('dpc-blur')) hideSoon();
311        else if (t === popup || (popup.contains && popup.contains(t))) hideSoon();
312    });
313    document.addEventListener('click', function (ev) {
314        var t = ev.target;
315        if (t && t.classList && t.classList.contains('dpc-blur')) {
316            window.open($upgrade_url, '_blank', 'noopener');
317        }
318    });
319    window.addEventListener('scroll', function () {
320        popup.classList.remove('is-visible');
321    }, { passive: true });
322})();
323
324function dpcBlurredCell() {
325    return '<span class="dpc-blur" data-upgrade="1" title="Upgrade to Premium">●●●</span>';
326}
327
328function dpcUpdateUsage(usage) {
329    if (!usage || typeof usage !== 'object') return;
330    if (typeof usage.request_used === 'number')      $request_used      = usage.request_used;
331    if (typeof usage.request_cap === 'number')       $request_cap       = usage.request_cap;
332    if (typeof usage.request_remaining === 'number') $request_remaining = usage.request_remaining;
333    if (typeof usage.is_premium === 'boolean')       $is_premium        = usage.is_premium;
334    if (typeof usage.guest_used === 'number')        $guest_used        = usage.guest_used;
335    if (typeof usage.guest_cap === 'number')         $guest_cap         = usage.guest_cap;
336    if (typeof usage.guest_remaining === 'number')   $guest_remaining   = usage.guest_remaining;
337    if (typeof usage.free_daily_cap === 'number')       $free_daily_cap       = usage.free_daily_cap;
338    if (typeof usage.free_daily_used === 'number')      $free_daily_used      = usage.free_daily_used;
339    if (typeof usage.free_daily_remaining === 'number') $free_daily_remaining = usage.free_daily_remaining;
340    if (typeof usage.chunk_size === 'number' && usage.chunk_size > 0) $chunk_size = usage.chunk_size;
341    /* Keeps an already-open tab on the current input cap when an admin changes it. */
342    if (typeof usage.url_cap === 'number' && usage.url_cap > 0) $checker_limit = usage.url_cap;
343    if (typeof usage.uses_custom_url_billing === 'boolean') {
344        $uses_custom_url_billing = usage.uses_custom_url_billing;
345    }
346    if (typeof usage.billing_unit === 'number' && usage.billing_unit > 0) {
347        $billing_unit = usage.billing_unit;
348    }
349}
350
351/* Open the XooTiX "Login / Signup Popup". We NEVER fall back to
352 * wp-login.php so the site's hidden admin URL stays hidden.
353 *
354 * IMPORTANT — how XooTiX trigger classes actually work on most sites:
355 *   - `.xoo-el-action-sc.xoo-el-login-tgr` is the visible PAGE Login button.
356 *     Clicking it OPENS the popup (Login tab).
357 *   - `.xoo-el-reg-tgr` (with `data-tab="register"`) is the Sign Up TAB
358 *     RENDERED INSIDE THE POPUP. It only switches tabs while the popup
359 *     is already open — clicking it before the popup opens does nothing.
360 *   - Most themes (including dapacheckerpro.com) only render the Login
361 *     page button. So the correct flow for register is:
362 *        1) Click the page-level Login trigger to OPEN the popup.
363 *        2) Wait for the popup container to appear.
364 *        3) Click the internal `.xoo-el-reg-tgr` tab to switch form.
365 *
366 * @param {'login'|'register'} which Which form to open.
367 * @return {boolean} True if we triggered the popup (or attempted to).
368 */
369function dpcOpenXootixLogin(which) {
370    which = which === 'register' ? 'register' : 'login';
371
372    function $$(sel) { return document.querySelectorAll(sel); }
373    function $first(sel) { return document.querySelector(sel); }
374
375    function fireClick(el) {
376        if (!el) return false;
377        try {
378            if (window.jQuery) {
379                jQuery(el).trigger('click');
380            } else {
381                el.click();
382            }
383            return true;
384        } catch (e) { return false; }
385    }
386
387    /* Find the page-level XooTiX trigger that OPENS the popup. We prefer
388     * `.xoo-el-action-sc` (the shortcode-rendered link/button) and only
389     * accept triggers that live OUTSIDE the popup container, so we don't
390     * accidentally click an internal tab. */
391    function findPageTrigger(form) {
392        var nodes = $$('.xoo-el-action-sc, .xoo-el-tgr, .xoo-el-login-tgr, .xoo-el-reg-tgr');
393        var fallback = null;
394        for (var i = 0; i < nodes.length; i++) {
395            var n = nodes[i];
396            if (n.closest && n.closest('.xoo-el-container')) continue;
397            var cls = n.className || '';
398            if (form === 'register') {
399                if (/(^|\s)(xoo-el-reg-tgr|xoo-el-register-tgr)(\s|$)/.test(cls)) return n;
400            } else {
401                if (/(^|\s)xoo-el-login-tgr(\s|$)/.test(cls)) return n;
402            }
403            if (!fallback) fallback = n;
404        }
405        return fallback;
406    }
407
408    function isPopupVisible() {
409        var c = $first('.xoo-el-container');
410        if (!c) return false;
411        var cs = window.getComputedStyle(c);
412        if (!cs || cs.display === 'none' || cs.visibility === 'hidden') return false;
413        return c.offsetWidth > 0 || c.offsetHeight > 0;
414    }
415
416    function clickInternalRegTab() {
417        var tab = $first('.xoo-el-container .xoo-el-reg-tgr, .xoo-el-form-nav .xoo-el-reg-tgr, .xoo-el-reg-tgr[data-tab="register"]');
418        if (!tab) tab = $first('.xoo-el-reg-tgr');
419        return fireClick(tab);
420    }
421
422    function tryPublicApi() {
423        try {
424            if (window.xoo_el_data && window.xoo_el_data.popup && typeof window.xoo_el_data.popup.open === 'function') {
425                window.xoo_el_data.popup.open(which);
426                return true;
427            }
428        } catch (e) {}
429        return false;
430    }
431
432    /* Inject a hidden trigger with the right class so XooTiX's delegated
433     * handler picks it up. Used as last resort when no page trigger exists. */
434    function injectAndClick(klass) {
435        try {
436            var a = document.createElement('a');
437            a.className = klass + ' xoo-el-action-sc';
438            a.setAttribute('href', '#');
439            a.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;opacity:0;pointer-events:auto;';
440            document.body.appendChild(a);
441            fireClick(a);
442            setTimeout(function () { if (a && a.parentNode) a.parentNode.removeChild(a); }, 1500);
443            return true;
444        } catch (e) { return false; }
445    }
446
447    function showUnavailableFallback() {
448        if (typeof Swal !== 'undefined' && Swal.fire) {
449            Swal.fire({
450                title: 'Sign in unavailable',
451                text: 'The sign-in window could not be opened. Please refresh the page and try again.',
452                icon: 'warning',
453                customClass: { popup: 'dpc-premium-confirm' },
454                confirmButtonText: 'OK',
455            });
456        }
457    }
458
459    /* Switch the popup to the requested tab if needed. Retries a few times
460     * because the popup container may animate in. */
461    function switchTabWhenReady(form, attempts) {
462        attempts = attempts || 0;
463        if (form !== 'register') return;
464        if (isPopupVisible()) {
465            clickInternalRegTab();
466            return;
467        }
468        if (attempts >= 20) return;
469        setTimeout(function () { switchTabWhenReady(form, attempts + 1); }, 100);
470    }
471
472    if (tryPublicApi()) {
473        switchTabWhenReady(which);
474        return true;
475    }
476
477    var pageTrigger = findPageTrigger(which);
478
479    if (which === 'register') {
480        /* If a real `.xoo-el-reg-tgr` page trigger exists outside the popup,
481         * a single click opens the popup directly on the register tab. */
482        if (pageTrigger && /(^|\s)(xoo-el-reg-tgr|xoo-el-register-tgr)(\s|$)/.test(pageTrigger.className || '')) {
483            fireClick(pageTrigger);
484            switchTabWhenReady('register');
485            return true;
486        }
487        /* Otherwise open the popup via the Login trigger and then flip
488         * to the Sign Up tab inside the popup. */
489        var loginTrigger = pageTrigger || findPageTrigger('login');
490        if (loginTrigger) {
491            fireClick(loginTrigger);
492            switchTabWhenReady('register');
493            return true;
494        }
495        /* No on-page trigger at all — inject one. */
496        injectAndClick('xoo-el-login-tgr');
497        switchTabWhenReady('register');
498        setTimeout(function () { if (!isPopupVisible()) showUnavailableFallback(); }, 900);
499        return true;
500    }
501
502    /* Login branch. */
503    if (pageTrigger) {
504        fireClick(pageTrigger);
505        return true;
506    }
507    injectAndClick('xoo-el-login-tgr');
508    setTimeout(function () { if (!isPopupVisible()) showUnavailableFallback(); }, 900);
509    return true;
510}
511
512/* Guest hard-cap reached — force login via XooTiX popup only. */
513function dpcShowLoginRequiredPopup(payload) {
514    var cap  = (payload && payload.cap)  ? payload.cap  : $guest_cap;
515    var used = (payload && payload.used) ? payload.used : $guest_used;
516
517    var html = ''
518        + '<div class="dpc-confirm-stats">'
519        +   '<div class="dpc-confirm-stat">'
520        +     '<div class="dpc-confirm-stat__label">Free uses today</div>'
521        +     '<div class="dpc-confirm-stat__value">' + used + ' / ' + cap + '</div>'
522        +   '</div>'
523        +   '<div class="dpc-confirm-stat">'
524        +     '<div class="dpc-confirm-stat__label">Resets at</div>'
525        +     '<div class="dpc-confirm-stat__value" style="font-size:14px;">Midnight</div>'
526        +   '</div>'
527        + '</div>'
528        + '<p style="margin:14px 0 0;color:#4a5568;font-size:13px;line-height:1.55;">'
529        +   'You have used your <strong>' + cap + '</strong> free checks for today. '
530        +   'Please <strong>sign in</strong> to keep checking for free — logged-in users have no daily limit.'
531        + '</p>';
532
533    Swal.fire({
534        title: 'Sign in to continue',
535        html: html,
536        icon: null,
537        showCancelButton: !!$register_url,
538        confirmButtonText: 'Sign in',
539        cancelButtonText: 'Create an account',
540        reverseButtons: true,
541        focusConfirm: true,
542        allowOutsideClick: false,
543        customClass: { popup: 'dpc-premium-confirm' },
544    }).then(function (res) {
545        if (res && res.isConfirmed) {
546            dpcOpenXootixLogin('login');
547        } else if (res && res.dismiss === Swal.DismissReason.cancel && $register_url) {
548            dpcOpenXootixLogin('register');
549        }
550    });
551}
552
553/* Premium pre-submit confirmation popup.
554 *
555 * Default premium: One Check click = 1 request against the monthly quota.
556 * Custom URL-cap (>700): requests = ceil(urls / billing unit). */
557function dpcPremiumConfirmIfNeeded(domainCount, fullCap) {
558    return new Promise(function (resolve) {
559        if (!$is_premium) return resolve(true);
560
561        var needed = dpcRequestsNeeded(domainCount);
562        var remaining   = $request_cap > 0 ? Math.max(0, $request_cap - $request_used) : Infinity;
563        var willExceed  = remaining !== Infinity && remaining < needed;
564
565        /* Hard block when not enough remaining requests (server also enforces). */
566        if (willExceed) {
567            var exceedHtml;
568            if ($uses_custom_url_billing) {
569                exceedHtml = 'This check needs <strong>' + needed + '</strong> request'
570                    + (needed === 1 ? '' : 's')
571                    + ' (<strong>' + $billing_unit + '</strong> URLs = 1 request), but you only have <strong>'
572                    + remaining + '</strong> left.';
573            } else {
574                exceedHtml = 'You have used <strong>' + $request_used + ' / ' + $request_cap
575                    + '</strong> requests this period. This check cannot be sent.';
576            }
577            Swal.fire({
578                title: 'Not enough requests',
579                html: exceedHtml,
580                icon: 'warning',
581                customClass: { popup: 'dpc-premium-confirm' },
582                confirmButtonText: 'OK',
583            }).then(function () { resolve(false); });
584            return;
585        }
586
587        try {
588            if (window.sessionStorage && sessionStorage.getItem('dpc_premium_confirm_seen') === '1') {
589                return resolve(true);
590            }
591        } catch (e) {}
592
593        var capLabel    = $request_cap > 0 ? $request_cap : '∞';
594        var remLabel    = remaining === Infinity ? '∞' : remaining;
595        var afterLabel  = remaining === Infinity ? '∞' : Math.max(0, remaining - needed);
596        var title       = $uses_custom_url_billing
597            ? ('Tip: ' + $billing_unit + ' URLs = 1 request')
598            : 'Tip: 1 run = 1 request';
599
600        var body;
601        if ($uses_custom_url_billing) {
602            body = 'Checking <strong>' + domainCount + '</strong>
602 URL' + (domainCount === 1 ? '' : 's')
603                 + ' counts as <strong>' + needed + '</strong> request' + (needed === 1 ? '' : 's')
604                 + ' (<strong>' + $billing_unit + '</strong> URLs = 1 request) from your monthly quota.'
605                 + ' Submit up to <strong>' + fullCap + '</strong> URLs per check.';
606        } else {
607            body = 'Checking <strong>' + domainCount + '</strong> URL' + (domainCount === 1 ? '' : 's')
608                 + ' counts as <strong>1 request</strong> from your monthly quota.'
609                 + ' Submit up to <strong>' + fullCap + '</strong> URLs per check.';
610        }
611
612        var html = ''
613            + '<div class="dpc-confirm-stats">'
614            +   '<div class="dpc-confirm-stat">'
615            +     '<div class="dpc-confirm-stat__label">Requests left</div>'
616            +     '<div class="dpc-confirm-stat__value">' + remLabel + ' / ' + capLabel + '</div>'
617            +   '</div>'
618            +   '<div class="dpc-confirm-stat">'
619            +     '<div class="dpc-confirm-stat__label">After this run</div>'
620            +     '<div class="dpc-confirm-stat__value">' + afterLabel + '</div>'
621            +   '</div>'
622            + '</div>'
623            + '<p style="margin:14px 0 0;color:#4a5568;font-size:13px;line-height:1.55;">' + body + '</p>';
624
625        Swal.fire({
626            title: title,
627            html: html,
628            icon: null,
629            showCancelButton: true,
630            confirmButtonText: 'Continue',
631            cancelButtonText: 'Cancel',
632            reverseButtons: true,
633            focusConfirm: true,
634            customClass: { popup: 'dpc-premium-confirm' },
635            buttonsStyling: true,
636        }).then(function (res) {
637            if (res && res.isConfirmed) {
638                try { sessionStorage.setItem('dpc_premium_confirm_seen', '1'); } catch (e) {}
639                resolve(true);
640            } else {
641                resolve(false);
642            }
643        });
644    });
645}
646
647function dpcHandleLimitReached(payload, ctx) {
648    var reason = (payload && payload.reason) || '';
649    var msg, title;
650    var partial = ctx && ctx.totalChunks > 1 && ctx.chunkIndex > 0
651        ? '<br><br><span style="color:#5b6b7d;font-size:12px;">Partial results are shown below — you can download them as CSV.</span>'
652        : '';
653
654    if (reason === 'request_cap_exceeded') {
655        title = 'Monthly limit reached';
656        var neededNote = (payload && payload.requests_needed)
657            ? ' This check needed <strong>' + payload.requests_needed + '</strong> request'
658                + (payload.requests_needed === 1 ? '' : 's') + '.'
659            : '';
660        msg = 'You have used <strong>' + (payload.used || 0) + ' / ' + (payload.cap || 0) + '</strong> requests this period.'
661            + neededNote
662            + ' Please contact admin to reset your counter or request a higher cap.' + partial;
663    } else if (reason === 'free_daily_cap_exceeded') {
664        title = 'Daily limit reached';
665        msg = 'You have used <strong>' + (payload.used || 0) + ' / ' + (payload.cap || 0) + '</strong> checks today. '
666            + 'Your free allowance resets tomorrow.'
667            + '<br><br>Upgrade to Premium for a much higher limit and no daily reset wait.' + partial;
668    } else if (reason === 'url_cap_exceeded') {
669        title = 'Limit reached';
670        msg = 'This request exceeds the allowed URL limit. '
671            + 'Please refresh the page and try again.' + ($is_premium ? '' : '<br><br>Upgrade to Premium to check more URLs at once.') + partial;
672    } else {
673        title = 'Limit reached';
674        msg = ((payload && payload.message) ? payload.message : 'Request limit reached. Please try again later.') + partial;
675    }
676    Swal.fire({
677        title: title,
678        html: msg,
679        icon: 'warning',
680        customClass: { popup: 'dpc-premium-confirm' },
681        confirmButtonText: $is_premium ? 'OK' : 'See plans',
682    }).then(function (res) {
683        if (res && res.isConfirmed && !$is_premium) {
684            window.open($upgrade_url, '_blank', 'noopener');
685        }
686    });
687}
688
689function countURLs(event) {
690    let textareaValue = event.target.value;
691    if (textareaValue.length != 0) {
692        let urls = textareaValue
693        let lines = urls.split('\n');
694        const filteredArray = lines.filter((element) => {
695            return element !== '';
696        });
697        let filteredBucket = [];
698        filteredArray.forEach(function (ingredient) {
699            const tempIngredient = ingredient.split(' ');
700            const filteredIngredient = tempIngredient.filter((element) => {
701                return element !== '';
702            });
703            filteredBucket.push(...filteredIngredient);
704        });
705
706        let filteredDomains = [];
707        filteredBucket.forEach(function (instance) {
708            if (isValidDomain(instance) || isValidUrl(instance)) {
709                filteredDomains.push(instance);
710            }
711        });
712
713        if (filteredDomains.length > 0) {
714            filteredDomains = [...new Set(filteredDomains)];
715        }
716
717
718        if (filteredDomains.length > $checker_limit) {
719            jQuery(".url_counter").css("color", "red");
720        } else {
721            jQuery(".url_counter").css("color", "#000");
722        }
723
724        jQuery(".url_counter").text(filteredDomains.length);
725    }
726}
727
728function chunkArray(array, chunkSize) {
729    const chunkedArray = [];
730
731    for (let i = 0; i < array.length; i += chunkSize) {
732        const chunk = array.slice(i, i + chunkSize).join(', ');
733        // const chunk = array.slice(i, i + chunkSize);
734        chunkedArray.push(chunk);
735    }
736
737    return chunkedArray;
738}
739
740
741var facebook_icon = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 14.4 14.4"><path fill="#039be5" d="M7.2 1.5a5.7 5.7 0 1 0 0 11.4 5.7 5.7 0 1 0 0-11.4Z"/><path fill="#fff" d="M7.972 8.711h1.475l.232-1.498H7.972v-.819c0-.623.203-1.174.786-1.174h.936V3.912
741c-.164-.022-.512-.071-1.169-.071-1.372 0-2.176.725-2.176 2.375v.997h-1.41v1.498h1.41v4.119c.279.042.562.07.853.07.263 0 .519-.024.772-.058V8.711z"/></svg>`;
742var instagram_icon = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 14.4 14.4"><path fill="#FF3D00" d="M12.96 10.17c-.12.63-.63 1.11-1.26 1.2-.99.15-2.64.33-4.5.33-1.83 0-3.48-.18-4.5-.33-.63-.09-1.14-.57-1.26-1.2-.12-.69-.24-1.71-.24-2.97s.12-2.28.24-2.97c.12-.63.63-1.11 1.26-1.2.99-.15 2.64-.33 4.5-.33 1.86 0 3.48.18 4.5.33.63.09 1.14.57 1.26 1.2.12.69.27 1.71.27 2.97-.03 1.26-.15 2.28-.27 2.97z"/><path fill="#FFF" d="M6 9.3V5.1l3.6 2.1z"/></svg>`;
743var linkedin_icon = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 14.4 14.4"><path fill="#0288D1" d="M12.6 11.1a1.5 1.5 0 0 1-1.5 1.5H3.3a1.5 1.5 0 0 1-1.5-1.5V3.3a1.5 1.5 0 0 1 1.5-1.5h7.8a1.5 1.5 0 0 1 1.5 1.5v7.8z"/><path fill="#FFF" d="M3.6 5.7h1.5v5.1H3.6zm.745-.6h-.008c-.448 0-.737-.334-.737-.75 0-.426.298-.75.754-.75s.737.324.746.75c0 .416-.289.75-.755.75zm6.455 5.7H9.3V8.07c0-.659-.367-1.109-.958-1.109-.45 0-.694.304-.812.597-.043.105-.03.395-.03.542v2.7H6V5.7h1.5v.785c.216-.335.555-.785 1.421-.785 1.073 0 1.878.675 1.878 2.182L10.8 10.8z"/></svg>`;
744var twitter_icon = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 14.4 14.4"><path fill="#212121" fill-rule="evenodd" d="M11.4 12.6H3a1.2 1.2 0 0 1-1.2-1.2V3A1.2 1.2 0 0 1 3 1.8h8.4A1.2 1.2 0 0 1 12.6 3v8.4a1.2 1.2 0 0 1-1.2 1.2z" clip-rule="evenodd"/><path fill="#fff" d="M10.277 10.2H8.346l-4.197-6H6.08l4.197 6zm-1.701-.509h.769L5.85 4.709h-.769l3.495 4.982z"/><path fill="#fff" d="m4.76 10.2 2.161-2.503-.283-.375L4.147 10.2Zm2.575-3.684.271.387L9.941 4.2h-.6Z"/></svg>`;
745var pinterest_icon = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 14.4 14.4"><path fill="#E60023" d="M13.2 7.2a6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 12 0z"/><path fill="#FFF" d="M7.333 3.425c-2.59 0-3.964 1.738-3.964 3.631 0 .88.468 1.976 1.218 2.325.113.053.174.03.2-.08l.167-.681c.015-.06.007-.112-.041-.172-.248-.301-.447-.854-.447-1.369 0-1.324 1.002-2.604 2.709-2.604 1.474 0 2.506 1.004 2.506 2.441 0 1.623-.82 2.747-1.886 2.747-.589 0-1.03-.487-.889-1.085.169-.713.497-1.483.497-1.998 0-.46-.247-.845-.759-.845-.602 0-1.085.623-1.085 1.457 0 .531.179.891.179.891s-.595 2.514-.704 2.982c-.121.517-.074 1.243-.022 1.717.135.053.271.106.411.15.245-.398.611-1.052.746-1.573l.374-1.426c.196.373.767.689 1.375.689 1.81 0 3.114-1.664 3.114-3.732 0-1.983-1.618-3.466-3.699-3.466z"/></svg>`;
746var youtube_icon = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 14.4 14.4"><path fill="#FF3D00" d="M12.96 10.17c-.12.63-.63 1.11-1.26 1.2-.99.15-2.64.33-4.5.33-1.83 0-3.48-.18-4.5-.33-.63-.09-1.14-.57-1.26-1.2-.12-.69-.24-1.71-.24-2.97s.12-2.28.24-2.97c.12-.63.63-1.11 1.26-1.2.99-.15 2.64-.33 4.5-.33 1.86 0 3.48.18 4.5.33.63.09 1.14.57 1.26 1.2.12.69.27 1.71.27 2.97-.03 1.26-.15 2.28-.27 2.97z"/><path fill="#FFF" d="M6 9.3V5.1l3.6 2.1z"/></svg>`;
747
748function formatNumberAuto(num) {
749    if (num >= 1_000_000) {
750        return (num / 1_000_000).toFixed(2) + 'M';
751    } else if (num >= 1_000) {
752        return (num / 1_000).toFixed(2) + 'K';
753    } else {
754        return num.toString();
755    }
756}
757
758
759async function processStringsSequentially(strings) {
760    var totalChunks = strings.length;
761    /* Results already delivered in this run, so the free spam-score
762     * allowance is counted once per run rather than once per chunk. */
763    var delivered = 0;
764    for (let i = 0; i < strings.length; i++) {
765        dpcUpdateProgressLabel(i, totalChunks);
766        const stringToProcess = strings[i];
767        const status = await processChunkWithMainFallback(stringToProcess, {
768            offset: delivered,
769            onLimitReached: function ($parsed) {
770                dpcHandleLimitReached($parsed, { chunkIndex: i, totalChunks: totalChunks });
771            }
772        });
773
774        if (status === 'stop' || status === 'limit') {
775            return;
776        }
777
778        delivered += stringToProcess.split(',').filter(function (s) {
779            return s.trim() !== '';
780        }).length;
781    }
782    dpcUpdateProgressLabel(totalChunks, totalChunks);
783}
784
785function dpcApplySuccessResponse($parsed) {
786    dpcUpdateUsage($parsed.usage);
787    var sorted = Object.entries($parsed.data).sort((a, b) => a[1].seq - b[1].seq);
788    var batch    = [];
789    var chunkUrls = [];
790    for (var k = 0; k < sorted.length; k++) {
791        var key  = sorted[k][0];
792        var val  = sorted[k][1];
793        var rowD = dpcRowDataFromUpstream(key, val);
794        dpcAllRowsData.push(rowD);
795        batch.push(dpcRowDataToHtml(rowD));
796        chunkUrls.push(key);
797    }
798    dpcAppendRowsToTable(batch);
799    dpcEnrichDomainAge(chunkUrls);
800}
801
802function dpcHandleChunkResponse($parsed, progressCtx) {
803    progressCtx = progressCtx || {};
804
805    if ($parsed.hasOwnProperty('status') && $parsed.status === 'login_required') {
806        dpcShowLoginRequiredPopup($parsed);
807        return 'stop';
808    }
809
810    if ($parsed.hasOwnProperty('status') && $parsed.status === 'captcha_required') {
811        dpcShowCaptchaRequired();
812        return 'stop';
813    }
814
815    if ($parsed.hasOwnProperty('status') && $parsed.status === 'limit_reached') {
816        if (typeof progressCtx.onLimitReached === 'function') {
817            progressCtx.onLimitReached($parsed);
818        }
819        return 'limit';
820    }
821
822    if ($parsed.hasOwnProperty('status') && $parsed.status == "error") {
823        const msg = (typeof $parsed.message === 'string' && $parsed.message.indexOf('Too many') !== -1)
824            ? 'Too many requests. Please wait a few minutes.'
825            : 'Unable to complete this request. Please try again.';
826        swal.fire(msg);
827        return 'stop';
828    }
829
830    if ($parsed.hasOwnProperty('success') && $parsed.success === false) {
831        swal.fire('Unable to complete this request. Please try again.');
832        return 'stop';
833    }
834
835    if ($parsed.hasOwnProperty('success') && $parsed.success === true) {
836        dpcApplySuccessResponse($parsed);
837        return 'ok';
838    }
839
840    swal.fire('Unable to complete this request. Please try again.');
841    return 'stop';
842}
843
844async function processChunkWithMainFallback(stringToProcess, progressCtx) {
845    progressCtx = progressCtx || {};
846    const result = await makeAjaxRequest(stringToProcess, { offset: progressCtx.offset || 0 });
847    let $parsed = result;
848
849    if ($parsed.hasOwnProperty('status') && $parsed.status === 'retry_split') {
850        if ($parsed.usage) {
851            dpcUpdateUsage($parsed.usage);
852        }
853        var maxChunk = parseInt($parsed.max_chunk || 500, 10) || 500;
854        var splitToken = (typeof $parsed.split_token === 'string') ? $parsed.split_token : '';
855        var domainList = stringToProcess.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
856        var subChunks = chunkArray(domainList, maxChunk);
857        var totalSub = subChunks.length;
858
859        for (var j = 0; j < subChunks.length; j++) {
860            if (totalSub > 1 && typeof progressCtx.onSubProgress === 'function') {
861                progressCtx.onSubProgress(j, totalSub);
862            }
863            var subResult = await makeAjaxRequest(subChunks[j], { splitToken: splitToken });
864            var handled = dpcHandleChunkResponse(subResult, progressCtx);
865            if (handled !== 'ok') {
866                return handled;
867            }
868        }
869        return 'ok';
870    }
871
872    return dpcHandleChunkResponse($parsed, progressCtx);
873}
874
875/* Normalise one upstream metric row into a plain JS object so that
876 * sorting, filtering, CSV/Excel export and Clusterize.js rendering all
877 * use the SAME source of truth (the live DOM may only hold the visible
878 * window of rows when Clusterize is active). */
879function dpcRowDataFromUpstream(url, val) {
880    function pick(obj, key, fallback) {
881        if (obj && Object.prototype.hasOwnProperty.call(obj, key) && obj[key] != null) {
882            return obj[key];
883        }
884        return fallback;
885    }
886    return {
887        url:              url,
888        domain_authority: pick(val, 'domain_authority', null),
889        page_authority:   pick(val, 'page_authority', null),
890        spam_score:       pick(val, 'spam_score', null),
891        linking_domains:  pick(val, 'plds_to_pld', pick(val, 'linking_domains', null)),
892        inbound_links:    pick(val, 'external_urls_to_pld', pick(val, 'inbound_links', null)),
893        full_metrics:     !!(val && val.full_metrics),
894        spam_locked:      !!(val && val.spam_locked),
895        da_locked:        !!(val && val.da_locked),
896        age_locked:       !!(val && val.age_locked),
897        ip:               pick(val, 'ip', null),
898        age:              pick(val, 'age', null),
899    };
900}
901
902/* HTML for ONE table row. Renders the blurred-upgrade-prompt cells for
903 * free users (PA/LD/IL always; DA/SS when upstream returned no value; plus
904 * the server-picked free allowance for SS, DA and Domain Age). */
905function dpcRowDataToHtml(d) {
906    var $da = d.domain_authority == null ? 'N/A' : d.domain_authority;
907    var $pa = d.page_authority   == null ? 'N/A' : d.page_authority;
908    var $ss;
909    if (d.spam_score == null) {
910        $ss = 'N/A';
911    } else if (d.spam_score > 5) {
912        $ss = '<span style="color:red; font-weight: bold;">' + d.spam_score + '</span>';
913    } else {
914        $ss = d.spam_score;
915    }
916    var $ld = d.linking_domains == null ? 'N/A' : formatNumberAuto(d.linking_domains);
917    var $il = d.inbound_links   == null ? 'N/A' : formatNumberAuto(d.inbound_links);
918    /* IP and Domain Age are enriched asynchronously from the domain-age
919     * bridge (see dpcEnrichDomainAge). Until the response lands the cells
920     * show a small spinner so the user knows a value is on the way. Once
921     * enriched, dpcSyncClusterizeFromData keeps Clusterize's row cache in
922     * sync so virtual scroll never shows stale spinners. */
923    /* null = still waiting for domain-age bridge; any string (incl. N/A)
924     * means enrichment finished for that cell. */
925    var $ip = d.ip == null ? dpcAgePendingHtml() : d.ip;
926    var $age = d.age == null ? dpcAgePendingHtml() : d.age;
927
928    if (!$is_premium && !d.full_metrics) {
929        $pa = $ld = $il = dpcBlurredCell();
930        if (d.domain_authority == null) {
931            $da = dpcBlurredCell();
932        }
933        if (d.spam_score == null) {
934            $ss = dpcBlurredCell();
935        }
936    }
937
938    /* Free tier hides a slice of the results: the spam score past the per-run
939     * allowance, plus a stable pseudo-random subset of DA and Domain Age.
940     * The server already stripped those values, so prompt instead of N/A. */
941    if (!$is_premium) {
942        if (d.spam_locked) $ss  = dpcBlurredCell();
943        if (d.da_locked)   $da  = dpcBlurredCell();
944        if (d.age_locked)  $age = dpcBlurredCell();
945    }
946
947    return ''
948        + '<tr>'
949        +   '<td style="text-align: left !important;">' + d.url + '</td>'
950        +   '<td><span class="domain-score">' + $da + '</span></td>'
951        +   '<td><span class="page-score">' + $pa + '</span></td>'
952        +   '<td><span class="spam-score">' + $ss + '</span></td>'
953        +   '<td><span class="linking-domains">' + $ld + '</span></td>'
954        +   '<td><span class="inbound-links">' + $il + '</span></td>'
955        +   '<td><span class="d-ip">' + $ip + '</span></td>'
956        +   '<td><span class="d-age">' + $age + '</span></td>'
957        + '</tr>';
958}
959
960/* Append a batch of pre-rendered <tr> strings to the results table.
961 * Prefers Clusterize.js when the row total justifies virtual scrolling
962 * (>100 rows) so the page stays responsive with thousands of results. */
963function dpcAppendRowsToTable(htmlRows) {
964    if (!htmlRows || !htmlRows.length) return;
965
966    var useCluster = (typeof Clusterize !== 'undefined')
967        && (dpcAllRowsData.length > 100 || dpcClusterize);
968
969    if (useCluster) {
970        if (!dpcClusterize) {
971            /* First time we cross the virtual-scroll threshold: hand the
972             * FULL row set to Clusterize so the rows already in the DOM
973             * are replaced with the windowed render. */
974            var existing = [];
975            for (var i = 0; i < dpcAllRowsData.length - htmlRows.length; i++) {
976                existing.push(dpcRowDataToHtml(dpcAllRowsData[i]));
977            }
978            try {
979                dpcClusterize = new Clusterize({
980                    rows: existing.concat(htmlRows),
981                    scrollId: 'dpc-scroll-area',
982                    contentId: 'dpc-tbody',
983                    no_data_text: '',
984                    rows_in_block: 50,
985                    blocks_in_cluster: 4,
986                });
987                return;
988            } catch (e) {
989                dpcClusterize = null;
990            }
991        } else {
992            dpcClusterize.append(htmlRows);
993            return;
994        }
995    }
996
997    /* Fallback path: small result sets or Clusterize unavailable. */
998    var $tbody = jQuery('#dapa-stats > tbody');
999    $tbody.append(htmlRows.join(''));
1000}
1001
1002/* Clear table + JS row store, ready for a fresh run. */
1003function dpcResetResults() {
1004    dpcAllRowsData = [];
1005    dpcAgeRunId++; /* invalidate any in-flight enrich responses */
1006    if (dpcClusterSyncTimer) {
1007        clearTimeout(dpcClusterSyncTimer);
1008        dpcClusterSyncTimer = null;
1009    }
1010    if (dpcAgeFinalizeTimer) {
1011        clearTimeout(dpcAgeFinalizeTimer);
1012        dpcAgeFinalizeTimer = null;
1013    }
1014    if (dpcClusterize) {
1015        try { dpcClusterize.destroy(true); } catch (e) {}
1016        dpcClusterize = null;
1017    }
1018    jQuery('#dapa-stats > tbody').html('');
1019}
1020
1021/* ------------------------------------------------------------------
1022 * Domain Age / IP enrichment (bridge to the domain-age plugin's API).
1023 *
1024 * DA PA cells render instantly. IP + Domain Age cells start with a
1025 * spinner; this code fetches the values for each DA PA chunk in the
1026 * background and patches the rows in place. Sub-chunked to 100 so users
1027 * see rows fill progressively for large premium batches.
1028 * ----------------------------------------------------------------- */
1029var dpcAgeRunId = 0;
1030var dpcAgeFinalizeTimer = null;
1031var DPC_AGE_SUB_CHUNK = 100;
1032var DPC_AGE_FETCH_MS  = 120000;
1033
1034function dpcNormalizeHost(url) {
1035    var v = String(url || '').toLowerCase().trim();
1036    v = v.replace(/^https?:\/\//, '').replace(/^www\./, '');
1037    v = v.split('/')[0].split(':')[0].replace(/\.$/, '');
1038    return v;
1039}
1040
1041function dpcAgePendingHtml() {
1042    return '<span class="dpc-age-pending" title="Checking..."><span class="dpc-spin" aria-hidden="true"></span></span>';
1043}
1044
1045function dpcAjaxUrl() {
1046    if (params && typeof params.ajaxUrl === 'string' && params.ajaxUrl) {
1047        return params.ajaxUrl;
1048    }
1049    if (typeof window.ajaxurl === 'string' && window.ajaxurl) {
1050        return window.ajaxurl;
1051    }
1052    return '/wp-admin/admin-ajax.php';
1053}
1054
1055function dpcEnrichDomainAge(urls) {
1056    if (!urls || !urls.length) return;
1057    var runId = dpcAgeRunId;
1058    for (var i = 0; i < urls.length; i += DPC_AGE_SUB_CHUNK) {
1059        dpcFetchDomainAgeChunk(urls.slice(i, i + DPC_AGE_SUB_CHUNK), runId);
1060    }
1061}
1062
1063function dpcFetchDomainAgeChunk(urls, runId) {
1064    var fd = new FormData();
1065    fd.append('action', 'dapa_check_domain_age');
1066    fd.append('nonce', (params && params.dapa_nonce) ? params.dapa_nonce : '');
1067    for (var i = 0; i < urls.length; i++) {
1068        fd.append('domains[]', urls[i]);
1069    }
1070
1071    var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
1072    var timeoutId  = null;
1073    if (controller) {
1074        timeoutId = setTimeout(function () {
1075            try { controller.abort(); } catch (e) {}
1076        }, DPC_AGE_FETCH_MS);
1077    }
1078
1079    var fetchOpts = {
1080        method: 'POST',
1081        credentials: 'same-origin',
1082        body: fd,
1083    };
1084    if (controller) fetchOpts.signal = controller.signal;
1085
1086    fetch(dpcAjaxUrl(), fetchOpts).then(function (r) {
1087        return r.json().catch(function () { return null; });
1088    }).then(function (resp) {
1089        if (runId !== dpcAgeRunId) return; /* user cleared / reran */
1090        if (!resp || !resp.success || !Array.isArray(resp.data)) {
1091            for (var i = 0; i < urls.length; i++) {
1092                dpcApplyAgeRow(urls[i], null, null);
1093            }
1094            dpcFlushClusterizeSync();
1095            return;
1096        }
1097        var map = {};
1098        for (var j = 0; j < resp.data.length; j++) {
1099            var row = resp.data[j];
1100            if (row && typeof row.url === 'string') {
1101                map[row.url] = row;
1102            }
1103        }
1104        for (var k = 0; k < urls.length; k++) {
1105            var u   = urls[k];
1106            var hit = map[u];
1107            dpcApplyAgeRow(u, hit ? hit.ip : null, hit ? hit.age : null);
1108        }
1109        dpcFlushClusterizeSync();
1110    }).catch(function () {
1111        if (runId !== dpcAgeRunId) return;
1112        for (var i = 0; i < urls.length; i++) {
1113            dpcApplyAgeRow(urls[i], null, null);
1114        }
1115        dpcFlushClusterizeSync();
1116    }).finally(function () {
1117        if (timeoutId) clearTimeout(timeoutId);
1118    });
1119}
1120
1121/* Update one row's IP / Age cell + sync the canonical data store so
1122 * sort / filter / CSV / XLSX export pick up the new values.
1123 *
1124 * Clusterize.js keeps its own copy of pre-rendered <tr> HTML strings.
1125 * Patching only the live DOM is not enough — on scroll it re-mounts the
1126 * stale cached HTML (spinners). dpcScheduleClusterizeSync() rebuilds
1127 * Clusterize's row cache from dpcAllRowsData after each enrichment. */
1128var dpcClusterSyncTimer = null;
1129
1130function dpcScheduleClusterizeSync() {
1131    if (!dpcClusterize) return;
1132    if (dpcClusterSyncTimer) clearTimeout(dpcClusterSyncTimer);
1133    dpcClusterSyncTimer = setTimeout(function () {
1134        dpcClusterSyncTimer = null;
1135        dpcSyncClusterizeFromData();
1136    }, 40);
1137}
1138
1139function dpcSyncClusterizeFromData() {
1140    if (!dpcClusterize || !dpcAllRowsData.length) return;
1141    try {
1142        dpcClusterize.update(dpcAllRowsData.map(dpcRowDataToHtml));
1143    } catch (e) {}
1144}
1145
1146function dpcFlushClusterizeSync() {
1147    if (dpcClusterSyncTimer) {
1148        clearTimeout(dpcClusterSyncTimer);
1149        dpcClusterSyncTimer = null;
1150    }
1151    dpcSyncClusterizeFromData();
1152}
1153
1154/* Any row still pending after all enrich requests had time to finish
1155 * (including 120s timeout per sub-chunk) is marked N/A so spinners
1156 * never stay forever. */
1157function dpcFinalizeStaleAgePending() {
1158    var changed = false;
1159    for (var i = 0; i < dpcAllRowsData.length; i++) {
1160        if (dpcAllRowsData[i].ip == null) {
1161            dpcAllRowsData[i].ip = 'N/A';
1162            changed = true;
1163        }
1164        if (dpcAllRowsData[i].age == null) {
1165            dpcAllRowsData[i].age = 'N/A';
1166            changed = true;
1167        }
1168    }
1169    if (changed) dpcFlushClusterizeSync();
1170}
1171
1172function dpcScheduleAgeFinalize() {
1173    if (dpcAgeFinalizeTimer) clearTimeout(dpcAgeFinalizeTimer);
1174    dpcAgeFinalizeTimer = setTimeout(function () {
1175        dpcAgeFinalizeTimer = null;
1176        dpcFinalizeStaleAgePending();
1177    }, DPC_AGE_FETCH_MS + 5000);
1178}
1179
1180function dpcApplyAgeRow(url, ip, age) {
1181    var ipText  = (ip  != null && String(ip).trim()  !== '') ? String(ip).trim()  : 'N/A';
1182    var ageText = (age != null && String(age).trim() !== '') ? String(age).trim() : 'N/A';
1183    var host    = dpcNormalizeHost(url);
1184    var found   = false;
1185    /* Rows whose Domain Age is withheld from free users keep the upgrade
1186     * badge — enrichment fills their IP cell only. */
1187    var ageLocked = {};
1188
1189    /* Apply to every row for this URL and any other URL that resolves to
1190     * the same host (http vs https, with/without www, duplicate paste). */
1191    for (var i = 0; i < dpcAllRowsData.length; i++) {
1192        var rowUrl = dpcAllRowsData[i].url;
1193        if (rowUrl === url || (host && dpcNormalizeHost(rowUrl) === host)) {
1194            dpcAllRowsData[i].ip = ipText;
1195            if (!$is_premium && dpcAllRowsData[i].age_locked) {
1196                ageLocked[rowUrl] = true;
1197            } else {
1198                dpcAllRowsData[i].age = ageText;
1199            }
1200            found = true;
1201        }
1202    }
1203    if (!found) return;
1204
1205    /* Patch every visible DOM row that matches URL or host. */
1206    var rows = document.querySelectorAll('#dapa-stats > tbody > tr');
1207    for (var j = 0; j < rows.length; j++) {
1208        var tds = rows[j].cells;
1209        if (!tds || tds.length < 8) continue;
1210        var cellUrl  = (tds[0].textContent || '').trim();
1211        var cellHost = dpcNormalizeHost(cellUrl);
1212        if (cellUrl !== url && (!host || cellHost !== host)) continue;
1213        var ipSpan  = tds[6].querySelector('.d-ip');
1214        var ageSpan = tds[7].querySelector('.d-age');
1215        if (ipSpan)  ipSpan.textContent  = ipText;
1216        if (ageSpan && !ageLocked[cellUrl]) ageSpan.textContent = ageText;
1217    }
1218
1219    dpcScheduleClusterizeSync();
1220}
1221
1222/* Button label while a check is in progress (no internal batch details). */
1223function dpcUpdateProgressLabel(done, total) {
1224    var $txt = jQuery('#urls-submit > .text');
1225    if (!$txt.length) return;
1226    $txt.text(done >= total ? 'Check DA PA' : 'Checking');
1227}
1228
1229function makeAjaxRequest(stringToProcess, opts) {
1230    opts = opts || {};
1231    return new Promise((resolve, reject) => {
1232        const url = params.port + "checkdapa.php";
1233
1234        testFetch().then(function (key) {
1235            let $payload = JSON.parse(key);
1236            $payload['domains'] = stringToProcess;
1237            if (opts.splitToken) {
1238                $payload['split_token'] = opts.splitToken;
1239            }
1240            if (opts.offset > 0) {
1241                $payload['offset'] = opts.offset;
1242            }
1243            if (dpcTurnstileRequired()) {
1244                if (dpcTurnstileTicket) {
1245                    $payload['turnstile_ticket'] = dpcTurnstileTicket;
1246                } else if (dpcTurnstileToken) {
1247                    $payload['cf-turnstile-response'] = dpcTurnstileToken;
1248                }
1249            }
1250            return fetch(url, {
1251                method: 'POST',
1252                credentials: 'same-origin',
1253                body: JSON.stringify($payload),
1254                headers: {
1255                    'Content-Type': 'application/json',
1256                    'X-Requested-With': 'XMLHttpRequest',
1257                    'X-WP-Nonce': (typeof params !== 'undefined' && params.dapa_nonce) ? params.dapa_nonce : '',
1258                },
1259            }).then(async function (response) {
1260                let data;
1261                try {
1262                    data = JSON.parse(await response.text());
1263                } catch (e) {
1264                    data = null;
1265                }
1266                if (data && typeof data.turnstile_ticket === 'string' && data.turnstile_ticket) {
1267                    dpcTurnstileTicket = data.turnstile_ticket;
1268                }
1269                if (response.status === 401 && data && data.status === 'login_required') {
1270                    return data;
1271                }
1272                if (data && data.status === 'captcha_required') {
1273                    return data;
1274                }
1275                if (response.status === 429 && data && data.status === 'limit_reached') {
1276                    return data;
1277                }
1278                if (response.status === 429 && data && data.status === 'error' && typeof data.message === 'string') {
1279                    return {
1280                        status: 'error',
1281                        message: 'Too many requests. Please wait a few minutes.',
1282                    };
1283                }
1284                if (!response.ok || data === null) {
1285                    throw new Error('request_failed');
1286                }
1287                return data;
1288            });
1289        }).then(function (data) {
1290            resolve(data);
1291        }).catch(function () {
1292            reject(new Error('request_failed'));
1293        });
1294    });
1295}
1296
1297function nullHandler(variable) {
1298    if (variable == null) {
1299        return '<span style="color:red;">
1299N/A</span>'
1300    } else {
1301        return variable
1302    }
1303}
1304
1305
1306
1307function sendAjaxRequestsRecursive(outerArray, index = 0) {
1308    if (index === outerArray.length) {
1309        jQuery("#urls-submit").prop("disabled", false);
1310        jQuery("#urls-input").prop("disabled", false);
1311        jQuery("#urls-input").css("opacity", 1);
1312        jQuery("#urls-submit > .text").text('Find Data');
1313        // jQuery("#download-stats").prop("disabled", false);
1314        jQuery(".loader-line").css("visibility", "hidden");
1315        jQuery("#download-stats").prop("disabled", false);
1316        jQuery("#tool-clear").prop("disabled", false);
1317        jQuery(".loader_wmf").css("visibility", "hidden");
1318        return;
1319    }
1320
1321    testFetch().then(function (key) {
1322        let $payload = JSON.parse(key);
1323        let tempDomainsArray = outerArray[index];
1324        let ajaxRequests = [];
1325        tempDomainsArray.forEach(function ($url) {
1326            $payload['domain'] = $url;
1327
1328            let singleAjaXRequest = jQuery.ajax({
1329                url: params.port + "webmailfinder.php",
1330                type: 'POST',
1331                dataType: 'json',
1332                contentType: 'application/json',
1333                data: JSON.stringify($payload),
1334                beforeSend: function () {
1335                },
1336                success: function (data) {
1337                    let $parsedData = data;
1338                    let $domain_authority = $page_authority = $spam_score = $business_name = $email_address = $phone_number = $social_profiles = $web_category = $technology = null;
1339                    let $status = "failed";
1340                    if ($parsedData.hasOwnProperty('success')) {
1341                        if ($parsedData.success == true) {
1342                            $status = "success";
1343                            // for dapa
1344                            if ($parsedData.dapa_error == false) {
1345                                let $tempDapaData = $parsedData.dapa_data[0];
1346                                if ($tempDapaData.hasOwnProperty('domain_authority')) {
1347                                    $domain_authority = $tempDapaData.domain_authority;
1348                                }
1349                                if ($tempDapaData.hasOwnProperty('page_authority')) {
1350                                    $page_authority = $tempDapaData.page_authority;
1351                                }
1352                                if ($tempDapaData.hasOwnProperty('spam_score')) {
1353                                    $spam_score = $tempDapaData.spam_score;
1354                                }
1355                            }
1356                            // for wmf
1357                            if ($parsedData.webm_error == false) {
1358                                let $tempWmfdata = $parsedData.webm_data;
1359                                if ($tempWmfdata.hasOwnProperty('successful')) {
1360                                    if ($tempWmfdata.successful == true) {
1361                                        if ($tempWmfdata.hasOwnProperty('data')) {
1362                                            let $worigindata = $tempWmfdata.data;
1363                                            // business name
1364                                            if ($worigindata.hasOwnProperty('siteName')) {
1365                                                $business_name = $worigindata.siteName;
1366                                                if ($business_name.trim().length == 0) {
1367                                                    $business_name = null;
1368                                                }
1369                                            }
1370                                            // emails
1371                                            if ($worigindata.hasOwnProperty('emails')) {
1372                                                let $email_address_temp = $worigindata.emails;
1373                                                if ($email_address_temp.length != 0) {
1374                                                    let $emails = [];
1375                                                    $email_address_temp.forEach(function ($email_data) {
1376                                                        $emails.push($email_data.email);
1377                                                    });
1378                                                    $email_address = $emails.join(', ');
1379                                                }
1380                                            }
1381                                            // phones
1382                                            if ($worigindata.hasOwnProperty("phones")) {
1383                                                let $phones_temp = $worigindata.phones;
1384                                                if ($phones_temp.length != 0) {
1385                                                    $phone_number = $phones_temp.join(', ')
1386                                                }
1387                                            }
1388
1389                                            // category
1390                                            if ($worigindata.hasOwnProperty('category')) {
1391                                                $web_category = $worigindata.category;
1392                                                if ($web_category.trim().length == 0) {
1393                                                    $web_category = null;
1394                                                }
1395                                            }
1396
1397                                            // technology
1398                                            if ($worigindata.hasOwnProperty("technology")) {
1399                                                let $technology_temp = $worigindata.technology;
1400                                                if ($technology_temp.length != 0) {
1401                                                    $technology = $technology_temp.join(', ');
1402                                                }
1403                                            }
1404
1405                                            // social media HTML
1406                                            let $social_temp_data = "";
1407                                            if ($worigindata.hasOwnProperty("social")) {
1408                                                let $socialMedia = $worigindata.social;
1409                                                let $facebook = $socialMedia.facebook;
1410                                                if ($facebook.trim().length != 0) {
1411                                                    $social_temp_data += `<span><a target="_blank" href="${$facebook}">${facebook_icon}</a></span>`;
1412                                                }
1413                                                let $instagram = $socialMedia.instagram;
1414                                                if ($instagram.trim().length != 0) {
1415                                                    $social_temp_data += `<span><a target="_blank" href="${$instagram}">${instagram_icon}</a></span>`;
1416                                                }
1417                                                let $linkedin = $socialMedia.linkedin;
1418                                                if ($linkedin.trim().length != 0) {
1419                                                    $social_temp_data += `<span><a target="_blank" href="${$linkedin}">${linkedin_icon}</a></span>`;
1420                                                }
1421                                                let $pinterest = $socialMedia.pinterest;
1422                                                if ($pinterest.trim().length != 0) {
1423                                                    $social_temp_data += `<span><a target="_blank" href="${$pinterest}">${pinterest_icon}</a></span>`;
1424                                                }
1425                                                let $twitter = $socialMedia.twitter;
1426                                                if ($twitter.trim().length != 0) {
1427                                                    $social_temp_data += `<span><a target="_blank" href="${$twitter}">${twitter_icon}</a></span>`;
1428                                                }
1429                                                let $youtube = $socialMedia.youtube;
1430                                                if ($youtube.trim().length != 0) {
1431                                                    $social_temp_data += `<span><a target="_blank" href="${$youtube}">${youtube_icon}</a></span>`;
1432                                                }
1433                                            }
1434
1435                                            if ($social_temp_data.trim().length != 0) {
1436                                                $social_profiles = '<div class="sm_profiles">' + $social_temp_data + '</div>';
1437                                            }
1438                                        }
1439                                    }
1440                                }
1441                            }
1442                        } else {
1443
1444                        }
1445                    } else {
1446
1447                    }
1448                    // console.log(JSON.stringify({"da":$domain_authority, "pa":$page_authority, "ss":$spam_score, "bn": $business_name, "em":$email_address, "ph":$phone_number, "cat":$web_category, "tech":$technology,  "sm": $social_profiles,  "status":$status}));
1449                    let $before = jQuery('#dapa-stats > tbody').html();
1450
1451                    let $after = $before + `<tr>
1452                        <td>${$url}</td>
1453                        <td><span class="business_name">${nullHandler($business_name)}</span></td>
1454                        <td><span class="domain-score">${nullHandler($domain_authority)}</span></td>
1455                        <td><span class="page-score">${nullHandler($page_authority)}</span></td>
1456                        <td><span class="spam-score">${nullHandler($spam_score)}</span></td>
1457                        <td><span class="email_address">${nullHandler($email_address)}</span></td>
1458                        <td><span class="phone_no">${nullHandler($phone_number)}</span></td>
1459                        <td><span class="social_p">${nullHandler($social_profiles)}</span></td>
1460                        <td><span class="web_cat">${nullHandler($web_category)}</span></td>
1461                        <td><span class="technology">${nullHandler($technology)}</span></td>
1462                        <td><span class="rstat">${nullHandler($status)}</span></td>
1463                    </tr>`;
1464                    jQuery('#dapa-stats > tbody').html($after);
1465                },
1466                error: function (jqXHR, textStatus, errorThrown) {
1467                    //   console.log('Error: AJAX request failed.');
1468                    //   console.log('Status:', textStatus);
1469                    //   console.log('Error Thrown:', errorThrown);
1470                    let $domain_authority = $page_authority = $spam_score = $business_name = $email_address = $phone_number = $social_profiles = $web_category = $technology = null;
1471                    let $status = "failed";
1472                    // console.log(JSON.stringify({"da":$domain_authority, "pa":$page_authority, "ss":$spam_score, "bn": $business_name, "em":$email_address, "ph":$phone_number, "cat":$web_category, "tech":$technology,  "sm": $social_profiles,  "status":$status}));
1473                    let $before = jQuery('#dapa-stats > tbody').html();
1474
1475                    let $after = $before + `<tr>
1476                        <td>${$url}</td>
1477                        <td><span class="business_name">${nullHandler($business_name)}</span></td>
1478                        <td><span class="domain-score">${nullHandler($domain_authority)}</span></td>
1479                        <td><span class="page-score">${nullHandler($page_authority)}</span></td>
1480                        <td><span class="spam-score">${nullHandler($spam_score)}</span></td>
1481                        <td><span class="email_address">${nullHandler($email_address)}</span></td>
1482                        <td><span class="phone_no">${nullHandler($phone_number)}</span></td>
1483                        <td><span class="social_p">${nullHandler($social_profiles)}</span></td>
1484                        <td><span class="web_cat">${nullHandler($web_category)}</span></td>
1485                        <td><span class="technology">${nullHandler($technology)}</span></td>
1486                        <td><span class="rstat">${nullHandler($status)}</span></td>
1487                    </tr>`;
1488                    jQuery('#dapa-stats > tbody').html($after);
1489                },
1490                complete: function () {
1491                }
1492            });
1493            ajaxRequests.push(singleAjaXRequest);
1494        });
1495        jQuery.when.apply(jQuery, ajaxRequests).then(function () {
1496            sendAjaxRequestsRecursive(outerArray, index + 1);
1497        });
1498    });
1499}
1500
1501function isValidUrl(input) {
1502    const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w.-]*)*\/?$/;
1503    return urlPattern.test(input);
1504}
1505
1506function isValidDomain(input) {
1507    const domainPattern = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i;
1508    return domainPattern.test(input);
1509}
1510
1511function handleInputURLs(event) {
1512    let urls = document.getElementById('urls-input').value;
1513    if (urls.trim().length == 0) {
1514        swal.fire("Please enter urls in the textarea.");
1515    } else {
1516        let lines = urls.split('\n');
1517        const filteredArray = lines.filter((element) => {
1518            return element !== '';
1519        });
1520        let filteredBucket = [];
1521        filteredArray.forEach(function (ingredient) {
1522            const tempIngredient = ingredient.split(' ');
1523            const filteredIngredient = tempIngredient.filter((element) => {
1524                return element !== '';
1525            });
1526            filteredBucket.push(...filteredIngredient);
1527        });
1528
1529        let filteredDomains = [];
1530        filteredBucket.forEach(function (instance) {
1531            if (isValidDomain(instance) || isValidUrl(instance)) {
1532                filteredDomains.push(instance);
1533            }
1534        });
1535
1536        if (filteredDomains.length == 0) {
1537            swal.fire("No valid domain found in the data provided.");
1538            return;
1539        }
1540
1541        if (!$is_logged_in && $guest_cap > 0 && $guest_used >= $guest_cap) {
1542            dpcShowLoginRequiredPopup({
1543                used: $guest_used,
1544                cap: $guest_cap,
1545                login_url: $login_url,
1546            });
1547            return;
1548        }
1549
1550        let uniqueDomains = [...new Set(filteredDomains)];
1551
1552        if (uniqueDomains.length > $checker_limit) {
1553            if (!$is_premium) {
1554                Swal.fire({
1555                    title: 'Free plan limit',
1556                    html: 'Free plan allows up to <strong>' + $checker_limit + '</strong> URLs per check. '
1557                        + 'You submitted ' + uniqueDomains.length + '. '
1558                        + '<br><br>Upgrade to Premium to check up to ' + $checker_limit + ' URLs at a time.',
1559                    icon: 'info',
1560                    customClass: { popup: 'dpc-premium-confirm' },
1561                    showCancelButton: true,
1562                    confirmButtonText: 'See plans',
1563                    cancelButtonText: 'Check first ' + $checker_limit + ' only',
1564                    reverseButtons: true,
1565                }).then(function (res) {
1566                    if (res && res.isConfirmed) {
1567                        window.open($upgrade_url, '_blank', 'noopener');
1568                    } else {
1569                        uniqueDomains = uniqueDomains.slice(0, $checker_limit);
1570                        dpcEnsureTurnstileThenStart(uniqueDomains);
1571                    }
1572                });
1573                return;
1574            }
1575            swal.fire(`First ${$checker_limit} domains/urls will be checked.`);
1576            uniqueDomains = uniqueDomains.slice(0, $checker_limit);
1577        }
1578
1579        if (!$is_logged_in && $guest_cap > 0) {
1580            var remaining = Math.max(0, $guest_cap - $guest_used);
1581            if (remaining <= 3 && remaining > 0) {
1582                try {
1583                    var seenKey = 'dpc_guest_low_warning_' + new Date().toISOString().slice(0, 10);
1584                    if (!sessionStorage.getItem(seenKey)) {
1585                        sessionStorage.setItem(seenKey, '1');
1586                        Swal.fire({
1587                            title: 'Almost out of free uses',
1588                            html: 'Only <strong>' + remaining + '</strong> free check' + (remaining === 1 ? '' : 's') + ' left today. '
1589                                + '<br>' + ($free_daily_cap > 0
1590                                    ? 'Sign in for ' + $free_daily_cap + ' checks a day.'
1591                                    : 'Sign in for unlimited free checks.'),
1592                            icon: 'info',
1593                            customClass: { popup: 'dpc-premium-confirm' },
1594                            showCancelButton: true,
1595                            confirmButtonText: 'Sign in',
1596                            cancelButtonText: 'Continue',
1597                            reverseButtons: true,
1598                        }).then(function (res) {
1599                            if (res && res.isConfirmed) {
1600                                dpcOpenXootixLogin('login');
1601                                return;
1602                            }
1603                            dpcPremiumConfirmIfNeeded(uniqueDomains.length, $checker_limit).then(function (proceed) {
1604                                if (!proceed) return;
1605                                dpcEnsureTurnstileThenStart(uniqueDomains);
1606                            });
1607                        });
1608                        return;
1609                    }
1610                } catch (e) {}
1611            }
1612        }
1613
1614        /* Same courtesy warning for logged-in free accounts, when the admin
1615         * has set a daily cap. Upgrade is the way out here, not signing in. */
1616        if ($is_logged_in && !$is_premium && $free_daily_cap > 0) {
1617            var freeLeft = Math.max(0, $free_daily_cap - $free_daily_used);
1618            if (freeLeft <= 3 && freeLeft > 0) {
1619                try {
1620                    var freeSeenKey = 'dpc_free_low_warning_' + new Date().toISOString().slice(0, 10);
1621                    if (!sessionStorage.getItem(freeSeenKey)) {
1622                        sessionStorage.setItem(freeSeenKey, '1');
1623                        Swal.fire({
1624                            title: 'Almost out of checks today',
1625                            html: 'Only <strong>' + freeLeft + '</strong> check' + (freeLeft === 1 ? '' : 's') + ' left today. '
1626                                + '<br>Your allowance resets tomorrow, or upgrade for a much higher limit.',
1627                            icon: 'info',
1628                            customClass: { popup: 'dpc-premium-confirm' },
1629                            showCancelButton: true,
1630                            confirmButtonText: 'See plans',
1631                            cancelButtonText: 'Continue',
1632                            reverseButtons: true,
1633                        }).then(function (res) {
1634                            if (res && res.isConfirmed) {
1635                                window.open($upgrade_url, '_blank', 'noopener');
1636                                return;
1637                            }
1638                            dpcPremiumConfirmIfNeeded(uniqueDomains.length, $checker_limit).then(function (proceed) {
1639                                if (!proceed) return;
1640                                dpcEnsureTurnstileThenStart(uniqueDomains);
1641                            });
1642                        });
1643                        return;
1644                    }
1645                } catch (e) {}
1646            }
1647        }
1648
1649        dpcPremiumConfirmIfNeeded(uniqueDomains.length, $checker_limit).then(function (proceed) {
1650            if (!proceed) return;
1651            dpcEnsureTurnstileThenStart(uniqueDomains);
1652        });
1653    }
1654}
1655
1656function startChecking(uniqueDomains) {
1657    dpcResetResults();
1658    jQuery("#urls-submit").prop("disabled", true);
1659    jQuery("#urls-input").prop("disabled", true);
1660    jQuery("#urls-input").css("opacity", 0.5);
1661    jQuery("#urls-submit > .text").text('Checking');
1662    jQuery(".output_conatiner").css('display', 'block');
1663    jQuery(".filters-parent").css('display', 'flex');
1664    jQuery(".loader-line").css("visibility", "visible");
1665    jQuery("#download-stats").prop("disabled", true);
1666    jQuery("#tool-clear").prop("disabled", true);
1667    jQuery(".loader_wmf").css("visibility", "visible");
1668
1669    /* Premium: send the full URL list in one run; server retry_split handles
1670     * chunk-sized upstream batches (500 + remainder). Default premium bills 1
1671     * request; custom URL-cap (>700) bills ceil(urls / billingUnit).
1672     * Free: chunk at 10 per AJAX request. */
1673    var perChunk      = $chunk_size > 0 ? $chunk_size : ($is_premium ? 500 : 10);
1674    var bucketChunks  = $is_premium
1675        ? [uniqueDomains.join(', ')]
1676        : chunkArray(uniqueDomains, perChunk);
1677    dpcUpdateProgressLabel(0, bucketChunks.length);
1678
1679    processStringsSequentially(bucketChunks)
1680            .then(() => {
1681                jQuery("#urls-submit").prop("disabled", false);
1682                jQuery("#urls-input").prop("disabled", false);
1683                jQuery("#urls-input").css("opacity", 1);
1684                jQuery("#urls-submit > .text").text('Check DA PA');
1685                // jQuery("#download-stats").prop("disabled", false);
1686                jQuery(".loader-line").css("visibility", "hidden");
1687                jQuery("#download-stats").prop("disabled", false);
1688                jQuery("#tool-clear").prop("disabled", false);
1689                jQuery(".loader_wmf").css("visibility", "hidden");
1690                dpcResetTurnstile();
1691                dpcScheduleAgeFinalize();
1692            })
1693            .catch(() => {
1694                dpcResetTurnstile();
1695                Swal.fire({
1696                    text: 'Something went wrong. Please refresh the page and try again.',
1697                    icon: 'error',
1698                    confirmButtonText: 'OK'
1699                }).then(() => {
1700                    location.reload(true);
1701                });
1702            });
1703}
1704
1705var clear_button = document.getElementById("tool-clear");
1706clear_button.addEventListener("click", clearInstalls);
1707
1708function clearInstalls() {
1709    jQuery("#urls-input").val('');
1710    jQuery(".output_conatiner").css("display", "none");
1711    jQuery(".filters-parent").css('display', 'none');
1712    dpcResetResults();
1713    jQuery("#download-stats").prop("disabled", true);
1714    jQuery("#tool-clear").prop("disabled", true);
1715    jQuery(".url_counter").text("0");
1716    jQuery('html, body').animate({
1717        scrollTop: jQuery(".dapa_tool_container").offset().top
1718    }, 1000);
1719}
1720
1721
1722// ================ filter code ======================
1723
1724// References to filter controls
1725// const toggleFiltersBtn = document.getElementById("toggle-filters");
1726const filterControls = document.getElementById("filter-controls");
1727const daRange = document.getElementById("da-range");
1728const daNumber = document.getElementById("da-number");
1729const paRange = document.getElementById("pa-range");
1730const paNumber = document.getElementById("pa-number");
1731const ssRange = document.getElementById("ss-range");
1732const ssNumber = document.getElementById("ss-number");
1733
1734const daValue = document.getElementById("da-value");
1735const paValue = document.getElementById("pa-value");
1736const ssValue = document.getElementById("ss-value");
1737const toggleFiltersDiv = document.getElementById("toggle-filters");
1738
1739let filtersEnabled = false;
1740
1741// Toggle filters enable/disable
1742toggleFiltersDiv.addEventListener("click", () => {
1743    if ($checker_limit < 100) {
1744        swal.fire("Upgrade your plan!");
1745        return;
1746    }
1747    filtersEnabled = !filtersEnabled;
1748
1749    // Update the enable/disable status of filter inputs
1750    const status = !filtersEnabled;
1751    [daRange, daNumber, paRange, paNumber, ssRange, ssNumber].forEach((el) => {
1752        el.disabled = status;
1753    });
1754
1755    // Update div state and text
1756    toggleFiltersDiv.textContent = filtersEnabled ? "Disable Filters" : "Enable Filters";
1757    toggleFiltersDiv.classList.toggle("enabled", filtersEnabled);
1758
1759    // Show/hide filter values
1760    const displayStyle = filtersEnabled ? "inline" : "none";
1761    [daValue, paValue, ssValue].forEach((el) => {
1762        el.style.display = displayStyle;
1763    });
1764
1765    // Reset filters if disabled
1766    if (!filtersEnabled) {
1767        resetFilters();
1768    }
1769});
1770
1771
1772/* Reset filters: re-render the FULL data set. We rebuild from
1773 * `dpcAllRowsData` (not DOM) because Clusterize.js only ever keeps a small
1774 * window of rows mounted, so DOM-based "show all" wouldn't bring back
1775 * rows that were filtered out earlier. */
1776function resetFilters() {
1777    dpcRenderRows(dpcAllRowsData);
1778}
1779
1780/* Re-render the results table from a (possibly filtered/sorted) array of
1781 * row data objects. Uses Clusterize.js when appropriate. */
1782function dpcRenderRows(rows) {
1783    var html = (rows || []).map(dpcRowDataToHtml);
1784    if (dpcClusterize) {
1785        try { dpcClusterize.update(html); return; } catch (e) {}
1786    }
1787    if (typeof Clusterize !== 'undefined' && html.length > 100) {
1788        try { if (dpcClusterize) dpcClusterize.destroy(true); } catch (e) {}
1789        try {
1790            dpcClusterize = new Clusterize({
1791                rows: html,
1792                scrollId: 'dpc-scroll-area',
1793                contentId: 'dpc-tbody',
1794                no_data_text: '',
1795                rows_in_block: 50,
1796                blocks_in_cluster: 4,
1797            });
1798            return;
1799        } catch (e) { dpcClusterize = null; }
1800    }
1801    jQuery('#dapa-stats > tbody').html(html.join(''));
1802}
1803
1804/* Sort `dpcAllRowsData` in place by a numeric column key and re-render.
1805 * Called by the inline header-click handler in dapa.php. */
1806window.dpcSortByColumn = function (columnIndex, ascending) {
1807    if (!Array.isArray(dpcAllRowsData) || dpcAllRowsData.length === 0) return;
1808    var keyMap = {
1809        1: 'domain_authority',
1810        2: 'page_authority',
1811        3: 'spam_score',
1812        4: 'linking_domains',
1813        5: 'inbound_links',
1814    };
1815    var key = keyMap[columnIndex];
1816    if (!key) return;
1817    dpcAllRowsData.sort(function (a, b) {
1818        var va = parseFloat(a[key]); if (isNaN(va)) va = 0;
1819        var vb = parseFloat(b[key]); if (isNaN(vb)) vb = 0;
1820        return ascending ? (va - vb) : (vb - va);
1821    });
1822    dpcRenderRows(dpcAllRowsData);
1823};
1824
1825/* Hand the FULL row HTML to a callback (used by CSV / XLSX exporters that
1826 * walk the DOM). With Clusterize active the DOM only holds the visible
1827 * window; we materialise everything for the duration of `cb`, then snap
1828 * back to the windowed render. */
1829window.dpcWithFullRowsRendered = function (cb) {
1830    var hadCluster = !!dpcClusterize;
1831    var allHtml    = (dpcAllRowsData || []).map(dpcRowDataToHtml);
1832    if (hadCluster) {
1833        try { dpcClusterize.destroy(true); } catch (e) {}
1834        dpcClusterize = null;
1835    }
1836    if (allHtml.length) {
1837        jQuery('#dapa-stats > tbody').html(allHtml.join(''));
1838    }
1839    try { cb && cb(); }
1840    finally {
1841        if (hadCluster && typeof Clusterize !== 'undefined' && allHtml.length > 100) {
1842            try {
1843                dpcClusterize = new Clusterize({
1844                    rows: allHtml,
1845                    scrollId: 'dpc-scroll-area',
1846                    contentId: 'dpc-tbody',
1847                    no_data_text: '',
1848                    rows_in_block: 50,
1849                    blocks_in_cluster: 4,
1850                });
1851            } catch (e) {}
1852        }
1853    }
1854};
1855
1856// Synchronize range and number inputs
1857function syncInputs(range, number, valueDisplay) {
1858    range.addEventListener("input", () => {
1859        number.value = range.value;
1860        valueDisplay.textContent = range.value;
1861        if (filtersEnabled) filterTable();
1862    });
1863
1864    number.addEventListener("input", () => {
1865        range.value = number.value;
1866        valueDisplay.textContent = number.value;
1867        if (filtersEnabled) filterTable();
1868    });
1869}
1870
1871syncInputs(daRange, daNumber, daValue);
1872syncInputs(paRange, paNumber, paValue);
1873syncInputs(ssRange, ssNumber, ssValue);
1874
1875/* Filter against the data store (not the DOM) so it works correctly with
1876 * Clusterize.js, which only keeps the visible window mounted. */
1877function filterTable() {
1878    if ($checker_limit < 100) {
1879        swal.fire("Upgrade your plan!");
1880        return;
1881    }
1882    var daThreshold = parseInt(daRange.value, 10);
1883    var paThreshold = parseInt(paRange.value, 10);
1884    var ssThreshold = parseInt(ssRange.value, 10);
1885
1886    var matched = (dpcAllRowsData || []).filter(function (r) {
1887        var da = parseInt(r.domain_authority, 10); if (isNaN(da)) da = 0;
1888        var pa = parseInt(r.page_authority, 10);   if (isNaN(pa)) pa = 0;
1889        var ss = parseInt(r.spam_score, 10);       if (isNaN(ss)) ss = 0;
1890        return da >= daThreshold && pa >= paThreshold && ss <= ssThreshold;
1891    });
1892    dpcRenderRows(matched);
1893}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.