Plugin Directory

Changeset 3747021


Ignore:
Timestamp:
10/14/2026 07:21:05 PM (4 weeks ago)
Author:
easywpstuff
Message:

easy optimizer updated to 2.7.3

Location:
easy-optimizer
Files:
736 added
18 edited

Legend:

Unmodified
Added
Removed
  • easy-optimizer/trunk/assets/app.js

    r3697647 r3747021  
    724724        : React.createElement(FixNextCard, { psiData: psiData, dev: psiDev, switchTab: switchTab, settings: settings }), React.createElement("div", { className: "eop-card" }, React.createElement("div", { className: "eop-card__header" }, React.createElement("div", null, React.createElement("div", { className: "eop-card__kicker" }, "Operations"), React.createElement("div", { className: "eop-card__title" }, "Quick actions"))), cacheOn && preloadOn && React.createElement(React.Fragment, null, React.createElement("button", { type: "button", className: "eop-btn eop-btn--primary eop-btn--full", disabled: !!busy, "aria-busy": busy === 'preload', onClick: startPreload }, React.createElement("span", { className: "eop-btn-icon" }, React.createElement(Icon, { id: "refresh" })), React.createElement("span", { className: "eop-btn-label" }, "Preload Cache"), React.createElement("span", { className: "eop-btn-spinner" })), React.createElement("button", { type: "button", className: "eop-btn eop-btn--ghost eop-btn--full", disabled: !!busy, "aria-busy": busy === 'both', onClick: clearAndPreload }, React.createElement("span", { className: "eop-btn-icon" }, React.createElement(Icon, { id: "trash" })), React.createElement("span", { className: "eop-btn-label" }, "Clear Cache & Preload"), React.createElement("span", { className: "eop-btn-spinner" })), cfConnected && React.createElement("button", { type: "button", className: "eop-btn eop-btn--ghost eop-btn--full", disabled: !!busy, "aria-busy": busy === 'cf', onClick: clearCfCache }, React.createElement("span", { className: "eop-btn-icon" }, React.createElement(Icon, { id: "cloud" })), React.createElement("span", { className: "eop-btn-label" }, "Clear Cloudflare Cache"), React.createElement("span", { className: "eop-btn-spinner" })), ps.status === 'paused' && React.createElement("div", { className: "eop-action-status", style: { color: '#d97706' } }, "Paused — server busy", ps.paused_reason ? ` (${ps.paused_reason})` : '', ". Resumes automatically.")), cacheOn && !preloadOn && React.createElement(React.Fragment, null, React.createElement("button", { type: "button", className: "eop-btn eop-btn--primary eop-btn--full", disabled: !!busy, onClick: clearCache }, React.createElement("span", { className: "eop-btn-icon" }, React.createElement(Icon, { id: "trash" })), React.createElement("span", { className: "eop-btn-label" }, "Clear Cache"), React.createElement("span", { className: "eop-btn-spinner" })), cfConnected && React.createElement("button", { type: "button", className: "eop-btn eop-btn--ghost eop-btn--full", disabled: !!busy, "aria-busy": busy === 'cf', onClick: clearCfCache }, React.createElement("span", { className: "eop-btn-icon" }, React.createElement(Icon, { id: "cloud" })), React.createElement("span", { className: "eop-btn-label" }, "Clear Cloudflare Cache"), React.createElement("span", { className: "eop-btn-spinner" }))), !cacheOn && React.createElement("div", { className: "eop-empty-state" }, React.createElement("div", { className: "eop-empty-state__icon" }, React.createElement(Icon, { id: "info" })), React.createElement("div", { className: "eop-empty-state__text" }, "Enable Page Cache in the Caching tab to access quick actions."), React.createElement("a", { href: "#", className: "eop-tab eop-btn eop-btn--soft eop-btn--sm", onClick: (e) => { e.preventDefault(); switchTab('cache'); } }, "Go to Caching")), React.createElement("div", { id: "eop-action-msg", className: "eop-action-msg", style: { color: actionMsg.ok === false ? 'var(--eop-danger)' : actionMsg.ok === true ? 'var(--eop-success)' : 'var(--eop-text-muted)' } }, actionMsg.text),
    725725    // Pro upgrade card — inside the Quick actions card, below the buttons.
    726     (settings.easyopt_cloud_account || Number(settings.easyopt_fluxcdn_verified) === 1) ? null : React.createElement("div", { className: "eop-upsell eop-upsell--dash" }, React.createElement("div", { className: "eop-upsell__tag" }, "⚡ Pro"), React.createElement("div", { className: "eop-upsell__title" }, "Unlock the edge"), React.createElement("div", { className: "eop-upsell__body" }, "Guaranteed 90+ PageSpeed. Images, CSS, JS & fonts served from 119 locations."), React.createElement("button", { type: "button", className: "eop-upsell__btn", onClick: () => switchTab('flux') }, "Unlock 90+ PageSpeed Now")))), (() => {
     726    // Hidden once connected, and once the mobile "after" score is
     727    // already 90+: the card's pitch is a 90+ score.
     728    (settings.easyopt_cloud_account || Number(settings.easyopt_fluxcdn_verified) === 1 || (psiData?.results?.mobile?.after?.score ?? 0) >= 90) ? null : React.createElement("div", { className: "eop-upsell eop-upsell--dash" }, React.createElement("div", { className: "eop-upsell__tag" }, "⚡ Pro"), React.createElement("div", { className: "eop-upsell__title" }, "Unlock the edge"), React.createElement("div", { className: "eop-upsell__body" }, "Guaranteed 90+ PageSpeed. Images, CSS, JS & fonts served from 119 locations."), React.createElement("button", { type: "button", className: "eop-upsell__btn", onClick: () => switchTab('flux') }, "Unlock 90+ PageSpeed Now")))), (() => {
    727729        const sc = liveStats?.server_cache;
    728730        const layers = sc?.layers || [];
    … …  
    10121014    const total = stats ? Number(stats.total) : 0;
    10131015    const done = stats ? Number(stats.optimized) : 0;
     1016    // Live, and on the same clock as `pending`, so processed + pending = total
     1017    // on every poll (skipped and failed images count as processed).
     1018    const processed = stats ? Number(stats.processed || 0) : 0;
    10141019    const pending = stats ? Number(stats.pending || 0) : 0;
     1020    const restoring = stats ? Number(stats.restoring || 0) : 0;
    10151021    const failed = stats ? Number(stats.failed || 0) : 0;
    10161022    const skipped = stats ? Number(stats.skipped || 0) : 0;
     1023    const excluded = stats ? Number(stats.excluded || 0) : 0;
    10171024    const running = !!(stats && stats.running);
    1018     const pct = total > 0 ? Math.round(done / total * 100) : 0;
     1025    const restoringOnly = running && pending === 0 && restoring > 0;
     1026    // floor, not round: 201 of 202 must not read 100%.
     1027    const pct = total > 0 ? Math.floor(processed / total * 100) : 0;
    10191028    return React.createElement("div", null, stats && React.createElement("div", { className: "eop-callout", style: noFormats ? { borderLeft: '3px solid var(--eop-warn, #dba617)', marginBottom: '16px' } : { marginBottom: '16px' } }, React.createElement("p", { style: { margin: 0, fontSize: '13px' } }, React.createElement("strong", null, noFormats ? 'Limited on this host: ' : 'This host supports: '), stats.capability)), React.createElement(Check, { name: "easyopt_images", label: "Optimize images on this server", bold: true, settings: settings, onChange: onChange, disabled: noFormats,
    10201029        desc: noFormats ? "Nothing to enable until this host can encode WebP or AVIF." : "AVIF and WebP copies are made next to your originals, which are never modified." }), on && stats && React.createElement("div", { className: "eop-imgstat", style: { marginTop: '18px' } },
    10211030    // Live headline. During a run it reads "Optimizing N of M"; at
    10221031    // rest it reads how many are done and what was saved.
    1023     React.createElement("div", { className: "eop-imgstat__head" }, React.createElement("div", null, running
    1024         ? React.createElement("span", null, React.createElement("span", { className: "eop-imgstat__spin" }), " Optimizing \u2014 ", React.createElement("strong", null, nf(done) + " of " + nf(total)), " done, ", nf(pending), " to go")
    1025         : React.createElement("span", null, React.createElement("strong", null, nf(done) + " of " + nf(total) + " images"), " optimized", stats.saved > 0 ? React.createElement("span", null, " \u00b7 ", React.createElement("strong", null, stats.saved_human), " saved") : null)), React.createElement("div", { className: "eop-imgstat__pct" }, pct + "%")), React.createElement("div", { className: "eop-imgstat__bar" }, React.createElement("div", { className: "eop-imgstat__fill" + (running ? " is-running" : ""), style: { width: pct + '%' } })), (skipped + failed) > 0 && React.createElement("p", { className: "description", style: { margin: '8px 0 0', color: 'var(--eop-warn-fg, #92400e)' } }, nf(skipped + failed) + " couldn't be optimized (unsupported, animated, too large, or already small) and were skipped."), React.createElement("p", { style: { margin: '14px 0 0' } }, React.createElement("button", { type: "button", className: "button button-primary", disabled: !!busy || running || stats.remaining === 0, onClick: () => act('/images/optimize', 'optimize') }, running ? 'Optimizing\u2026' : (stats.remaining > 0 ? 'Optimize ' + nf(stats.remaining) + ' remaining' : 'All images optimized')), React.createElement("button", { type: "button", className: "button", style: { marginLeft: '8px' }, disabled: !!busy || running || done === 0,
     1032    React.createElement("div", { className: "eop-imgstat__head" }, React.createElement("div", null, restoringOnly
     1033        ? React.createElement("span", null, React.createElement("span", { className: "eop-imgstat__spin" }), " Restoring originals \u2014 ", React.createElement("strong", null, nf(restoring)), " to go")
     1034        : running
     1035            ? React.createElement("span", null, React.createElement("span", { className: "eop-imgstat__spin" }), " Optimizing \u2014 ", React.createElement("strong", null, nf(processed) + " of " + nf(total)), " done, ", nf(pending), " to go")
     1036            : React.createElement("span", null, React.createElement("strong", null, nf(done) + " of " + nf(total) + " images"), " optimized", stats.saved > 0 ? React.createElement("span", null, " \u00b7 ", React.createElement("strong", null, stats.saved_human), " saved") : null)), React.createElement("div", { className: "eop-imgstat__pct" }, pct + "%")), React.createElement("div", { className: "eop-imgstat__bar" }, React.createElement("div", { className: "eop-imgstat__fill" + (running ? " is-running" : ""), style: { width: pct + '%' } })), (skipped + failed) > 0 && React.createElement("p", { className: "description", style: { margin: '8px 0 0', color: 'var(--eop-warn-fg, #92400e)' } }, nf(skipped + failed) + " couldn't be optimized (unsupported, animated, too large, or already small) and were skipped."), excluded > 0 && React.createElement("p", { className: "description", style: { margin: '8px 0 0' } }, nf(excluded) + " restored to the original in the Media Library. Bulk runs leave " + (excluded === 1 ? "it" : "them") + " alone."), React.createElement("p", { style: { margin: '14px 0 0' } }, React.createElement("button", { type: "button", className: "button button-primary", disabled: !!busy || running || stats.remaining === 0, onClick: () => act('/images/optimize', 'optimize') }, running ? (restoringOnly ? 'Restoring\u2026' : 'Optimizing\u2026') : (stats.remaining > 0 ? 'Optimize ' + nf(stats.remaining) + ' remaining' : 'All images optimized')),
     1037    // Drops the queue; the image being encoded finishes, finished
     1038    // images stay optimized, and "Optimize N remaining" resumes.
     1039    running && React.createElement("button", { type: "button", className: "button", style: { marginLeft: '8px' }, disabled: !!busy, onClick: () => act('/images/stop', 'stop') }, busy === 'stop' ? 'Stopping\u2026' : 'Stop'), React.createElement("button", { type: "button", className: "button", style: { marginLeft: '8px' }, disabled: !!busy || running || done === 0,
    10261040        onClick: () => { if (window.confirm('Delete every optimized copy and restore your originals? Your originals were never modified, so nothing is lost.')) {
    10271041            act('/images/restore', 'restore');
    … …  
    18491863        ['easyopt_a11y_progressbar', 'ARIA progressbar elements do not have accessible names'],
    18501864        ['easyopt_a11y_tabindex', 'Some elements have a [tabindex] value greater than 0'],
     1865        ['easyopt_a11y_skip_link', 'Skip links are not focusable'],
     1866        ['easyopt_a11y_main_landmark', 'Document does not have a main landmark'],
    18511867    ];
    18521868    return (React.createElement("div", { style: { display: "block" }, className: "eop-panel", id: "eop-panel-accessibility" }, React.createElement("h2", { className: "eop-section-h" }, "Accessibility"), React.createElement("p", { className: "description", style: { marginBottom: '18px' } }, "Fix common Lighthouse accessibility audits. Enable only the options relevant to your site."), a11y.map(([key, label]) => React.createElement(Check, { key: key, name: key, label: label, settings: settings, onChange: onChange })), React.createElement("h2", { className: "eop-section-h", style: { marginTop: '34px' } }, "SEO"), React.createElement("p", { className: "description", style: { marginBottom: '18px' } }, "Fix common Lighthouse SEO audits."), React.createElement(Check, { name: "easyopt_seo_crawlable_links", label: "Make non-crawlable links crawlable", settings: settings, onChange: onChange, desc: 'Any <a> with missing/empty/javascript:void(0) href is rewritten to "#".' }), React.createElement(Check, { name: "easyopt_seo_image_alts", label: "Add missing image alt attributes", settings: settings, onChange: onChange, desc: "Fills missing alt from image title or filename." })));
  • easy-optimizer/trunk/assets/easyopt-beacon.js

    r3731476 r3747021  
    282282        }
    283283
    284         /* ── Per-session de-dupe ─────────────────────────────────────────── */
     284        /* ── De-dupe ─────────────────────────────────────────────────────── */
    285285        // Once this browser session has reported a given (url_type, viewport) we
    286286        // skip re-sending on later page loads in the same session. The server
    … …  
    288288        // trims redundant in-session reports so the origin isn't hit repeatedly.
    289289        function ssKey() { return 'eo_beacon:' + (cfg.urlType || '') + ':' + viewport; }
     290
     291        // (2.7.3) Per-browser memory of THIS page, across sessions. A page that a
     292        // full-page or CDN cache keeps serving with the beacon in it would
     293        // otherwise be reported again by the same visitor on every visit. Tied to
     294        // the render (cfg.rendered) and to what the page asks for, so a page
     295        // re-rendered after its data was cleared reports again. Kept to the 50
     296        // most recent pages so the stored list cannot grow without bound.
     297        var LS_KEY = 'eo_beacon_pages';
     298        function pageKey() { return (cfg.pageUrl || location.href) + '|' + viewport; }
     299        function pageVal() { return (cfg.rendered || '') + ':' + need.lcp + need.classes + need.fonts; }
     300        function pageMap() {
     301                try { var m = JSON.parse(window.localStorage.getItem(LS_KEY) || '{}'); return m && typeof m === 'object' ? m : {}; } catch (e) { return {}; }
     302        }
     303
    290304        function alreadyReported() {
    291                 try { return !!window.sessionStorage && !!sessionStorage.getItem(ssKey()); } catch (e) { return false; }
     305                try { if (window.sessionStorage && sessionStorage.getItem(ssKey())) { return true; } } catch (e) {}
     306                return pageMap()[pageKey()] === pageVal();
    292307        }
    293308        function markReported() {
    294309                try { if (window.sessionStorage) { sessionStorage.setItem(ssKey(), '1'); } } catch (e) {}
     310                try {
     311                        var m = pageMap(), k = pageKey();
     312                        delete m[k];
     313                        var keys = Object.keys(m);
     314                        for (var i = 0; i < keys.length - 49; i++) { delete m[keys[i]]; }
     315                        m[k] = pageVal();
     316                        window.localStorage.setItem(LS_KEY, JSON.stringify(m));
     317                } catch (e) {}
    295318        }
    296319
  • easy-optimizer/trunk/assets/easyopt-beacon.min.js

    r3731476 r3747021  
    1414 * Build: this is the readable source; assets/easyopt-beacon.min.js is the
    1515 * shipped, minified copy.
    16  */
    17 (function(){if(typeof easyoptBeacon==='undefined'){return}
    18 var cfg=easyoptBeacon;var rawNeed=cfg.need||{};var need={lcp:+rawNeed.lcp||0,classes:+rawNeed.classes||0,fonts:+rawNeed.fonts||0};var viewport=window.innerWidth<(cfg.mobileBreak||768)?'mobile':'desktop';var sent=!1;var lcpEntry=null;var lcpObserver=null;var classSet=Object.create(null);var haveClasses=!1;function lcpApiSupported(){return('PerformanceObserver' in window)&&!!PerformanceObserver.supportedEntryTypes&&PerformanceObserver.supportedEntryTypes.indexOf('largest-contentful-paint')!==-1}
    19 var lcpApiOk=lcpApiSupported();if(need.lcp&&lcpApiOk){try{lcpObserver=new PerformanceObserver(function(list){var entries=list.getEntries();for(var i=0;i<entries.length;i++){lcpEntry=entries[i]}});lcpObserver.observe({type:'largest-contentful-paint',buffered:!0})}catch(e){lcpApiOk=!1}}
    20 function isVisible(el){if(!el||el.nodeType!==1){return!1}
    21 if(el.offsetWidth===0||el.offsetHeight===0){return!1}
    22 var cs=window.getComputedStyle?window.getComputedStyle(el):null;if(!cs){return!0}
    23 if(cs.display==='none'){return!1}
    24 if(cs.visibility==='hidden'||cs.visibility==='collapse'){return!1}
    25 var o=parseFloat(cs.opacity||'1');return!(!isNaN(o)&&o<0.1)}
    26 function bgUrls(value){if(!value||value==='none'){return[]}
    27 var out=[],seen={},re=/url\((?:"([^"]+)"|'([^']+)'|([^)]+))\)/g,m;while((m=re.exec(value))!==null){var u=(m[1]||m[2]||m[3]||'').trim();if(u&&!seen[u]&&u.indexOf('data:')!==0){seen[u]=1;out.push(u)}}
    28 return out}
    29 function bgImageSetSrcset(value){var inner=String(value).match(/image-set\(([\s\S]*)\)/i);if(!inner){return''}
    30 var parts=[],seen={};var re=/url\((?:"([^"]+)"|'([^']+)'|([^)]+))\)\s*([0-9.]+(?:x|w|dppx)?)?/g,m;while((m=re.exec(inner[1]))!==null){var u=(m[1]||m[2]||m[3]||'').trim();if(!u||u.indexOf('data:')===0||seen[u]){continue}
    31 seen[u]=1;var d=(m[4]||'').trim();if(d&&/^[0-9.]+$/.test(d)){d+='x'}
    32 parts.push(d?(u+' '+d):u)}
    33 return parts.length?parts.join(', '):''}
    34 function resolveLcp(el){if(!el){return null}
    35 var tag=(el.tagName||'').toLowerCase();if(tag==='img'){var url=el.currentSrc||el.src||'';if(!url){return null}
    36 var inPicture=el.parentNode&&(el.parentNode.tagName||'').toLowerCase()==='picture';return{tag:'img',kind:inPicture?'picture':(el.srcset?'img-srcset':'img'),url:url,srcset:el.srcset||'',sizes:el.sizes||''}}
    37 if(tag==='video'&&el.poster){return{tag:'video',kind:'video-poster',url:el.poster,srcset:'',sizes:''}}
    38 if(window.getComputedStyle){var bgRaw=window.getComputedStyle(el).backgroundImage||'';var urls=bgUrls(bgRaw);if(urls.length){if(/image-set\(/i.test(bgRaw)){var ss=bgImageSetSrcset(bgRaw);if(ss){return{tag:tag,kind:'background-set',url:urls[0],srcset:ss,sizes:''}}}
    39 return{tag:tag,kind:'background',url:urls[0],srcset:'',sizes:''}}}
    40 return{tag:tag,kind:tag,url:'',srcset:'',sizes:''}}
    41 function findLcpFallback(){var vh=window.innerHeight||(document.documentElement&&document.documentElement.clientHeight)||0;var vw=window.innerWidth||(document.documentElement&&document.documentElement.clientWidth)||0;if(!vh||!vw){return null}
    42 var best=null,bestArea=0,i;function consider(el){if(!isVisible(el)){return}
    43 var rect;try{rect=el.getBoundingClientRect()}catch(e){return}
    44 if(rect.top>=vh||rect.left>=vw||rect.bottom<=0||rect.right<=0){return}
    45 var h=Math.min(rect.bottom,vh)-Math.max(rect.top,0);var w=Math.min(rect.right,vw)-Math.max(rect.left,0);var area=(h>0&&w>0)?(h*w):0;if(area>bestArea){bestArea=area;best=el}}
    46 var imgs=document.getElementsByTagName('img');for(i=0;i<imgs.length&&i<600;i++){consider(imgs[i])}
    47 var vids=document.querySelectorAll('video[poster]');for(i=0;i<vids.length&&i<50;i++){consider(vids[i])}
    48 var bgs=document.querySelectorAll('[style*="background-image"]');for(i=0;i<bgs.length&&i<200;i++){consider(bgs[i])}
    49 if(!best||bestArea<(cfg.minLcpSize||10000)){return null}
    50 return{element:best,size:bestArea,url:''}}
    51 function cssPath(el){if(!el||!el.nodeType){return''}
    52 var parts=[],node=el,depth=0;while(node&&node.nodeType===1&&depth<6){var name=node.nodeName.toLowerCase();if(node.id){parts.unshift(name+'#'+node.id);break}
    53 var cls=typeof node.className==='string'?node.className.trim():'';if(cls){name+='.'+cls.split(/\s+/).slice(0,2).join('.')}
    54 parts.unshift(name);node=node.parentElement;depth++}
    55 return parts.join(' > ')}
    56 function snapshotClasses(){if(!need.classes){return}
    57 try{var els=document.querySelectorAll('[class]');for(var i=0;i<els.length;i++){var list=els[i].classList;if(!list){continue}
    58 for(var j=0;j<list.length;j++){classSet[list[j]]=1}}
    59 haveClasses=!0}catch(e){}}
    60 var GENERIC={'serif':1,'sans-serif':1,'monospace':1,'cursive':1,'fantasy':1,'system-ui':1,'ui-sans-serif':1,'ui-serif':1,'ui-monospace':1,'ui-rounded':1,'math':1,'emoji':1,'fangsong':1,'-apple-system':1,'blinkmacsystemfont':1,'inherit':1,'initial':1,'unset':1,'revert':1};function primaryFamily(ff){var first=(String(ff).split(',')[0]||'').trim().replace(/^['"]+|['"]+$/g,'');if(!first||GENERIC[first.toLowerCase()]){return''}
    61 return first}
    62 function normWeight(w){w=String(w).toLowerCase();if(w==='normal'){return'400'}
    63 if(w==='bold'){return'700'}
    64 var n=parseInt(w,10);return isNaN(n)?'400':String(n)}
    65 function directText(el){var t='';for(var n=el.firstChild;n;n=n.nextSibling){if(n.nodeType===3&&n.nodeValue){t+=n.nodeValue}}
    66 return t}
    67 var fontSigCache=null,fontSigDone=!1;function collectFonts(){if(!need.fonts){return null}
    68 if(fontSigDone){return fontSigCache}
    69 fontSigDone=!0;try{var vh=window.innerHeight||(document.documentElement&&document.documentElement.clientHeight)||0;var vw=window.innerWidth||(document.documentElement&&document.documentElement.clientWidth)||0;if(!vh||!document.body){return null}
    70 var map=Object.create(null);var els=document.body.getElementsByTagName('*');var MAXEL=5000,MAXKEYS=12,MAXCP=300;for(var i=0;i<els.length&&i<MAXEL;i++){var el=els[i];var txt=directText(el);if(!txt||!/\S/.test(txt)){continue}
    71 var rect;try{rect=el.getBoundingClientRect()}catch(e){continue}
    72 if(!rect||rect.width===0||rect.height===0){continue}
    73 if(rect.top>=vh||rect.bottom<=0||rect.left>=vw||rect.right<=0){continue}
    74 var cs=window.getComputedStyle?window.getComputedStyle(el):null;if(!cs||cs.display==='none'||cs.visibility==='hidden'||cs.visibility==='collapse'){continue}
    75 var fam=primaryFamily(cs.fontFamily||'');if(!fam){continue}
    76 var key=fam+'|'+normWeight(cs.fontWeight||'400')+'|'+(String(cs.fontStyle||'').indexOf('italic')===0?'italic':'normal');var rec=map[key];if(!rec){if(Object.keys(map).length>=MAXKEYS){continue}
    77 rec=map[key]={cp:Object.create(null),n:0}}
    78 for(var c=0;c<txt.length&&rec.n<MAXCP;c++){var cp=txt.charCodeAt(c);if(cp>32&&!rec.cp[cp]){rec.cp[cp]=1;rec.n++}}}
    79 var out=[];for(var k in map){var p=k.split('|');var cps=Object.keys(map[k].cp).map(Number);cps.sort(function(a,b){return a-b});out.push({f:p[0],w:p[1],s:p[2],u:cps})}
    80 fontSigCache=out.length?out:null;return fontSigCache}catch(e){fontSigCache=null;return null}}
    81 function ssKey(){return'eo_beacon:'+(cfg.urlType||'')+':'+viewport}
    82 function alreadyReported(){try{return!!window.sessionStorage&&!!sessionStorage.getItem(ssKey())}catch(e){return!1}}
    83 function markReported(){try{if(window.sessionStorage){sessionStorage.setItem(ssKey(),'1')}}catch(e){}}
    84 function send(){if(sent){return}
    85 sent=!0;if(alreadyReported()){return}
    86 if(lcpObserver){try{lcpObserver.disconnect()}catch(e){}}
    87 snapshotClasses();var data=new FormData();data.append('action','easyopt_beacon');data.append('nonce',cfg.nonce);if(cfg.token){data.append('token',cfg.token)}
    88 data.append('url_type',cfg.urlType||'');data.append('page_url',cfg.pageUrl||location.href);data.append('viewport',viewport);var anything=!1;if(need.lcp&&!lcpEntry&&!lcpApiOk){lcpEntry=findLcpFallback()}
    89 if(need.lcp&&lcpEntry){var el=lcpEntry.element;var size=lcpEntry.size||0;if(el&&isVisible(el)&&size>=(cfg.minLcpSize||10000)){var r=resolveLcp(el);var url=r&&r.url?r.url:(lcpEntry.url||'');var kind=r?r.kind:'';if(r&&'video'===r.kind){url=el.currentSrc||url}else if(r&&!url){kind='none'}
    90 if(r&&(url||'none'===kind)){data.append('element_tag',r.tag);data.append('element_kind',kind);data.append('image_url',url);data.append('srcset',r.srcset||'');data.append('sizes',r.sizes||'');data.append('selector',cssPath(el));data.append('lcp_size',String(Math.round(size)));anything=!0}}}
    91 if(need.classes&&haveClasses){var classes=Object.keys(classSet);if(classes.length){if(classes.length>5000){classes=classes.slice(0,5000)}
    92 data.append('classes',classes.join(','));anything=!0}}
    93 if(need.fonts){var fontSig=collectFonts();if(fontSig){data.append('fonts',JSON.stringify(fontSig));anything=!0}}
    94 if(!anything){return}
    95 if(navigator.sendBeacon){try{if(navigator.sendBeacon(cfg.ajaxUrl,data)){markReported()}return}catch(e){}}
    96 try{fetch(cfg.ajaxUrl,{method:'POST',body:data,keepalive:!0,credentials:'same-origin'});markReported()}catch(e){}}
    97 function onInteract(){snapshotClasses()}
    98 function idle(fn,timeout){if(window.requestIdleCallback){window.requestIdleCallback(fn,{timeout:timeout||2000})}else{setTimeout(fn,200)}}
    99 if(document.readyState==='complete'){idle(snapshotClasses)}else{window.addEventListener('load',function(){idle(snapshotClasses)},{once:!0})}
    100 var interactEvents=['pointerdown','keydown','touchstart','scroll'];interactEvents.forEach(function(ev){window.addEventListener(ev,function(){idle(onInteract)},{once:!0,passive:!0})});setTimeout(function(){idle(snapshotClasses)},3500);idle(function(){try{collectFonts()}catch(e){}},4000);document.addEventListener('visibilitychange',function(){if(document.visibilityState==='hidden'){send()}},{once:!0});window.addEventListener('pagehide',send,{once:!0});setTimeout(send,8000)})()
     16 */ !function(){if("undefined"!=typeof easyoptBeacon){var e=easyoptBeacon,t=e.need||{},n={lcp:+t.lcp||0,classes:+t.classes||0,fonts:+t.fonts||0},r=window.innerWidth<(e.mobileBreak||768)?"mobile":"desktop",i=!1,a=null,o=null,l=Object.create(null),s=!1,c="PerformanceObserver"in window&&!!PerformanceObserver.supportedEntryTypes&&-1!==PerformanceObserver.supportedEntryTypes.indexOf("largest-contentful-paint");if(n.lcp&&c)try{(o=new PerformanceObserver(function(e){for(var t=e.getEntries(),n=0;n<t.length;n++)a=t[n]})).observe({type:"largest-contentful-paint",buffered:!0})}catch(u){c=!1}var f={serif:1,"sans-serif":1,monospace:1,cursive:1,fantasy:1,"system-ui":1,"ui-sans-serif":1,"ui-serif":1,"ui-monospace":1,"ui-rounded":1,math:1,emoji:1,fangsong:1,"-apple-system":1,blinkmacsystemfont:1,inherit:1,initial:1,unset:1,revert:1},d=null,p=!1;"complete"===document.readyState?E(m):window.addEventListener("load",function(){E(m)},{once:!0}),["pointerdown","keydown","touchstart","scroll"].forEach(function(e){window.addEventListener(e,function(){E(S)},{once:!0,passive:!0})}),setTimeout(function(){E(m)},3500),E(function(){try{y()}catch(e){}},4e3),document.addEventListener("visibilitychange",function(){"hidden"===document.visibilityState&&k()},{once:!0}),window.addEventListener("pagehide",k,{once:!0}),setTimeout(k,8e3)}function $(e){if(!e||1!==e.nodeType||0===e.offsetWidth||0===e.offsetHeight)return!1;var t=window.getComputedStyle?window.getComputedStyle(e):null;if(!t)return!0;if("none"===t.display||"hidden"===t.visibility||"collapse"===t.visibility)return!1;var n=parseFloat(t.opacity||"1");return!(!isNaN(n)&&n<.1)}function m(){if(n.classes)try{for(var e=document.querySelectorAll("[class]"),t=0;t<e.length;t++){var r=e[t].classList;if(r)for(var i=0;i<r.length;i++)l[r[i]]=1}s=!0}catch(a){}}function g(e){var t=(String(e).split(",")[0]||"").trim().replace(/^['"]+|['"]+$/g,"");return!t||f[t.toLowerCase()]?"":t}function v(e){if("normal"===(e=String(e).toLowerCase()))return"400";if("bold"===e)return"700";var t=parseInt(e,10);return isNaN(t)?"400":String(t)}function h(e){for(var t="",n=e.firstChild;n;n=n.nextSibling)3===n.nodeType&&n.nodeValue&&(t+=n.nodeValue);return t}function y(){if(!n.fonts)return null;if(p)return d;p=!0;try{var e=window.innerHeight||document.documentElement&&document.documentElement.clientHeight||0,t=window.innerWidth||document.documentElement&&document.documentElement.clientWidth||0;if(!e||!document.body)return null;for(var r=Object.create(null),i=document.body.getElementsByTagName("*"),a=0;a<i.length&&a<5e3;a++){var o,l=i[a],s=h(l);if(s&&/\S/.test(s)){try{o=l.getBoundingClientRect()}catch(c){continue}if(o&&0!==o.width&&0!==o.height&&!(o.top>=e)&&!(o.bottom<=0)&&!(o.left>=t)&&!(o.right<=0)){var u=window.getComputedStyle?window.getComputedStyle(l):null;if(u&&"none"!==u.display&&"hidden"!==u.visibility&&"collapse"!==u.visibility){var f=g(u.fontFamily||"");if(f){var $=f+"|"+v(u.fontWeight||"400")+"|"+(0===String(u.fontStyle||"").indexOf("italic")?"italic":"normal"),m=r[$];if(!m){if(Object.keys(r).length>=12)continue;m=r[$]={cp:Object.create(null),n:0}}for(var y=0;y<s.length&&m.n<300;y++){var _=s.charCodeAt(y);_>32&&!m.cp[_]&&(m.cp[_]=1,m.n++)}}}}}}var b=[];for(var k in r){var S=k.split("|"),E=Object.keys(r[k].cp).map(Number);E.sort(function(e,t){return e-t}),b.push({f:S[0],w:S[1],s:S[2],u:E})}return d=b.length?b:null}catch(C){return d=null,null}}function _(){return"eo_beacon:"+(e.urlType||"")+":"+r}function L(){return(e.pageUrl||location.href)+"|"+r}function P(){return(e.rendered||"")+":"+n.lcp+n.classes+n.fonts}function M(){try{var e=JSON.parse(window.localStorage.getItem("eo_beacon_pages")||"{}");return e&&"object"==typeof e?e:{}}catch(t){return{}}}function b(){try{window.sessionStorage&&sessionStorage.setItem(_(),"1")}catch(e){}try{var t=M(),n=L();delete t[n];for(var r=Object.keys(t),i=0;i<r.length-49;i++)delete t[r[i]];t[n]=P(),window.localStorage.setItem("eo_beacon_pages",JSON.stringify(t))}catch(a){}}function k(){if(!(i||(i=!0,function e(){try{if(window.sessionStorage&&sessionStorage.getItem(_()))return!0}catch(t){}return M()[L()]===P()}()))){if(o)try{o.disconnect()}catch(t){}m();var u=new FormData;u.append("action","easyopt_beacon"),u.append("nonce",e.nonce),e.token&&u.append("token",e.token),u.append("url_type",e.urlType||""),u.append("page_url",e.pageUrl||location.href),u.append("viewport",r);var f=!1;if(!n.lcp||a||c||(a=function t(){var n=window.innerHeight||document.documentElement&&document.documentElement.clientHeight||0,r=window.innerWidth||document.documentElement&&document.documentElement.clientWidth||0;if(!n||!r)return null;var i,a=null,o=0;function l(e){if($(e)){try{i=e.getBoundingClientRect()}catch(t){return}if(!(i.top>=n)&&!(i.left>=r)&&!(i.bottom<=0)&&!(i.right<=0)){var i,l=Math.min(i.bottom,n)-Math.max(i.top,0),s=Math.min(i.right,r)-Math.max(i.left,0),c=l>0&&s>0?l*s:0;c>o&&(o=c,a=e)}}}var s=document.getElementsByTagName("img");for(i=0;i<s.length&&i<600;i++)l(s[i]);var c=document.querySelectorAll("video[poster]");for(i=0;i<c.length&&i<50;i++)l(c[i]);var u=document.querySelectorAll('[style*="background-image"]');for(i=0;i<u.length&&i<200;i++)l(u[i]);return!a||o<(e.minLcpSize||1e4)?null:{element:a,size:o,url:""}}()),n.lcp&&a){var d=a.element,p=a.size||0;if(d&&$(d)&&p>=(e.minLcpSize||1e4)){var g=function e(t){if(!t)return null;var n=(t.tagName||"").toLowerCase();if("img"===n){var r=t.currentSrc||t.src||"";return r?{tag:"img",kind:t.parentNode&&"picture"===(t.parentNode.tagName||"").toLowerCase()?"picture":t.srcset?"img-srcset":"img",url:r,srcset:t.srcset||"",sizes:t.sizes||""}:null}if("video"===n&&t.poster)return{tag:"video",kind:"video-poster",url:t.poster,srcset:"",sizes:""};if(window.getComputedStyle){var i=window.getComputedStyle(t).backgroundImage||"",a=function e(t){if(!t||"none"===t)return[];for(var n,r=[],i={},a=/url\((?:"([^"]+)"|'([^']+)'|([^)]+))\)/g;null!==(n=a.exec(t));){var o=(n[1]||n[2]||n[3]||"").trim();o&&!i[o]&&0!==o.indexOf("data:")&&(i[o]=1,r.push(o))}return r}(i);if(a.length){if(/image-set\(/i.test(i)){var o=function e(t){var n=String(t).match(/image-set\(([\s\S]*)\)/i);if(!n)return"";for(var r,i=[],a={},o=/url\((?:"([^"]+)"|'([^']+)'|([^)]+))\)\s*([0-9.]+(?:x|w|dppx)?)?/g;null!==(r=o.exec(n[1]));){var l=(r[1]||r[2]||r[3]||"").trim();if(l&&0!==l.indexOf("data:")&&!a[l]){a[l]=1;var s=(r[4]||"").trim();s&&/^[0-9.]+$/.test(s)&&(s+="x"),i.push(s?l+" "+s:l)}}return i.length?i.join(", "):""}(i);if(o)return{tag:n,kind:"background-set",url:a[0],srcset:o,sizes:""}}return{tag:n,kind:"background",url:a[0],srcset:"",sizes:""}}}return{tag:n,kind:n,url:"",srcset:"",sizes:""}}(d),v=g&&g.url?g.url:a.url||"",h=g?g.kind:"";g&&"video"===g.kind?v=d.currentSrc||v:g&&!v&&(h="none"),g&&(v||"none"===h)&&(u.append("element_tag",g.tag),u.append("element_kind",h),u.append("image_url",v),u.append("srcset",g.srcset||""),u.append("sizes",g.sizes||""),u.append("selector",function e(t){if(!t||!t.nodeType)return"";for(var n=[],r=t,i=0;r&&1===r.nodeType&&i<6;){var a=r.nodeName.toLowerCase();if(r.id){n.unshift(a+"#"+r.id);break}var o="string"==typeof r.className?r.className.trim():"";o&&(a+="."+o.split(/\s+/).slice(0,2).join(".")),n.unshift(a),r=r.parentElement,i++}return n.join(" > ")}(d)),u.append("lcp_size",String(Math.round(p))),f=!0)}}if(n.classes&&s){var k=Object.keys(l);k.length&&(k.length>5e3&&(k=k.slice(0,5e3)),u.append("classes",k.join(",")),f=!0)}if(n.fonts){var S=y();S&&(u.append("fonts",JSON.stringify(S)),f=!0)}if(f){if(navigator.sendBeacon)try{navigator.sendBeacon(e.ajaxUrl,u)&&b();return}catch(E){}try{fetch(e.ajaxUrl,{method:"POST",body:u,keepalive:!0,credentials:"same-origin"}),b()}catch(C){}}}}function S(){m()}function E(e,t){window.requestIdleCallback?window.requestIdleCallback(e,{timeout:t||2e3}):setTimeout(e,200)}}();
  • easy-optimizer/trunk/easy-optimizer.php

    r3731476 r3747021  
    44 * Plugin URI:  https://fluxpress.io
    55 * Description: Page cache, lazy load, used CSS, delay JS, font optimization, image CDN, LCP preload and more — everything you need to ace Core Web Vitals, in one plugin.
    6  * Version:     2.7.2
     6 * Version:     2.7.3
    77 * Author:      FluxPress
    88 * Author URI:  https://fluxpress.io
    … …  
    2525}
    2626if ( ! defined( 'EASYOPT_VERSION' ) ) {
    27     define( 'EASYOPT_VERSION', '2.7.2' );
     27    define( 'EASYOPT_VERSION', '2.7.3' );
    2828}
    2929if ( ! defined( 'EASYOPT_CSS_LOGIC_VERSION' ) ) {
    … …  
    466466$a11y_keys = array( 'easyopt_a11y_inputs', 'easyopt_a11y_links', 'easyopt_a11y_buttons',
    467467    'easyopt_a11y_viewport', 'easyopt_a11y_role_elements', 'easyopt_a11y_iframes',
    468     'easyopt_a11y_progressbar', 'easyopt_a11y_tabindex' );
     468    'easyopt_a11y_progressbar', 'easyopt_a11y_tabindex',
     469    'easyopt_a11y_skip_link', 'easyopt_a11y_main_landmark' );
    469470foreach ( $a11y_keys as $ak ) {
    470471    if ( ! empty( $easyopt_opts[ $ak ] ) ) {
    … …  
    10421043        $a11y = array( 'a11y_inputs', 'a11y_links', 'a11y_buttons', 'a11y_viewport',
    10431044                       'a11y_role_elements', 'a11y_iframes', 'a11y_progressbar', 'a11y_tabindex',
     1045                       'a11y_skip_link', 'a11y_main_landmark',
    10441046                       'seo_crawlable_links', 'seo_image_alts' );
    10451047        $checks = array_merge( $checks, $a11y );
  • easy-optimizer/trunk/includes/class-easyopt-accessibility.php

    r3617492 r3747021  
    8282        }
    8383
     84        // (2.7.3) Skip-link target + main landmark — one shared pass, a no-op
     85        // unless the page actually fails one of the two audits.
     86        if ( self::$enabled['skip_link'] || self::$enabled['main_landmark'] ) {
     87            $html = self::fix_landmarks( $html );
     88        }
     89
    8490        return $html;
    8591    }
    … …  
    100106            'progressbar'   => (int) EasyOpt_Config::get( 'a11y_progressbar', 0 ),
    101107            'tabindex'      => (int) EasyOpt_Config::get( 'a11y_tabindex', 0 ),
     108            'skip_link'     => (int) EasyOpt_Config::get( 'a11y_skip_link', 0 ),
     109            'main_landmark' => (int) EasyOpt_Config::get( 'a11y_main_landmark', 0 ),
    102110        );
    103111
    … …  
    355363            $p->set_attribute( 'tabindex', '0' );
    356364        }
     365    }
     366
     367    /* ─────────────────────────────────────────────
     368     *  (2.7.3) Skip-link target + main landmark
     369     * ───────────────────────────────────────────── */
     370
     371    /** data-elementor-type values that hold the page's OWN content — never a header, footer, popup or embedded template. */
     372    const CONTENT_DOC_TYPES = array( 'wp-page', 'wp-post', 'single', 'single-post', 'single-page', 'archive', 'search-results', 'error-404', 'product', 'product-archive' );
     373
     374    /** Landmarks the main content is never inside. Mega menus embed whole Elementor posts in the header. */
     375    const OUTSIDE_MAIN_TAGS = array( 'HEADER', 'FOOTER', 'NAV', 'ASIDE' );
     376
     377    /**
     378     * Lighthouse: "Skip links are not focusable" and "Document does not have a
     379     * main landmark".
     380     *
     381     * Page-builder layouts (Elementor full width / canvas / Theme Builder,
     382     * Divi) print no <main>, while the theme still prints its "Skip to
     383     * content" link to #content — a link to nothing. Both are fixed on one
     384     * element, the page's main content block:
     385     *
     386     *   - the existing <main> / [role=main], else
     387     *   - the first Elementor page/post/single/archive document, or Divi's
     388     *     #main-content, that is NOT inside a header, footer, nav or aside.
     389     *
     390     * Only attributes change: role="main" on that block, an id on it when it
     391     * has none (easyopt-main — never "content", which themes style), and the
     392     * skip link's href pointed at it. Nothing is moved, wrapped or removed; a
     393     * page that already passes, or has no recognisable content block, is
     394     * returned unchanged.
     395     */
     396    private static function fix_landmarks( $html ) {
     397
     398        $has_main  = self::has_main_landmark( $html );
     399        $want_main = self::$enabled['main_landmark'] && ! $has_main;
     400        $want_skip = self::$enabled['skip_link'] && self::has_broken_skip_link( $html );
     401
     402        if ( ! $want_main && ! $want_skip ) {
     403            return $html;
     404        }
     405
     406        // Nothing to point at → nothing to walk for.
     407        if ( ! $has_main && ! preg_match( '#data-elementor-type=["\'](?:' . implode( '|', array_map( 'preg_quote', self::CONTENT_DOC_TYPES ) ) . ')["\']|\sid\s*=\s*["\']?main-content["\'\s>]#', $html ) ) {
     408            return $html;
     409        }
     410
     411        $p        = new WP_HTML_Tag_Processor( $html );
     412        $outside  = 0;    // Depth inside header / footer / nav / aside.
     413        $in_links = true; // Still before the first ordinary page link (where skip links live).
     414        $skips    = 0;
     415        $found    = false;
     416
     417        while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
     418            $tag = $p->get_tag();
     419
     420            if ( in_array( $tag, self::OUTSIDE_MAIN_TAGS, true ) ) {
     421                $outside = $p->is_tag_closer() ? max( 0, $outside - 1 ) : $outside + 1;
     422                continue;
     423            }
     424
     425            if ( $p->is_tag_closer() ) {
     426                continue;
     427            }
     428
     429            if ( 'A' === $tag ) {
     430                if ( $in_links ) {
     431                    $href = $p->get_attribute( 'href' );
     432                    $href = is_string( $href ) ? trim( $href ) : '';
     433
     434                    if ( $want_skip && $skips < 5 && self::is_skip_link( $p, $href ) && ! self::target_exists( $html, substr( $href, 1 ) ) ) {
     435                        $p->set_bookmark( 'easyopt_skip_' . $skips );
     436                        $skips++;
     437                    } elseif ( '' !== $href && '#' !== $href[0] && 0 !== stripos( $href, 'javascript:' ) ) {
     438                        $in_links = false;
     439                    }
     440                }
     441                continue;
     442            }
     443
     444            if ( self::is_main_block( $p, $tag, $outside ) ) {
     445                $found = true;
     446                break;
     447            }
     448        }
     449
     450        if ( ! $found ) {
     451            return $html;
     452        }
     453
     454        if ( $want_main && null === $p->get_attribute( 'role' ) ) {
     455            $p->set_attribute( 'role', 'main' );
     456        }
     457
     458        if ( $skips > 0 ) {
     459            $id = $p->get_attribute( 'id' );
     460            $id = is_string( $id ) ? trim( $id ) : '';
     461
     462            if ( '' === $id ) {
     463                $id = self::unique_id( $html, 'easyopt-main' );
     464                $p->set_attribute( 'id', $id );
     465            }
     466
     467            if ( ! preg_match( '/\s/', $id ) ) {
     468                for ( $i = 0; $i < $skips; $i++ ) {
     469                    if ( $p->seek( 'easyopt_skip_' . $i ) ) {
     470                        $p->set_attribute( 'href', '#' . $id );
     471                    }
     472                }
     473            }
     474        }
     475
     476        return $p->get_updated_html();
     477    }
     478
     479    /** A <main> element or role="main" anywhere in the document. */
     480    private static function has_main_landmark( $html ) {
     481        return (bool) preg_match( '#<main[\s>/]|\srole\s*=\s*["\']?main["\'\s/>]#i', $html );
     482    }
     483
     484    /** Any skip link whose #target does not exist. Cheap gate before the tag walk. */
     485    private static function has_broken_skip_link( $html ) {
     486        if ( false === stripos( $html, 'skip' )
     487            || ! preg_match_all( '#<a\s(?=[^>]*skip)[^>]*?\bhref\s*=\s*["\']?\#([^"\'\s>]+)#i', $html, $m ) ) {
     488            return false;
     489        }
     490
     491        foreach ( $m[1] as $fragment ) {
     492            if ( '/' !== $fragment[0] && '!' !== $fragment[0] && ! self::target_exists( $html, $fragment ) ) {
     493                return true;
     494            }
     495        }
     496        return false;
     497    }
     498
     499    /**
     500     * A skip link the way the audit sees one: an in-page link (#name, not #/
     501     * or #!) — and, so a menu toggle or "back to top" is never repointed, one
     502     * that names itself a skip link in its class or id.
     503     */
     504    private static function is_skip_link( $p, $href ) {
     505        if ( strlen( $href ) < 2 || '#' !== $href[0] || '/' === $href[1] || '!' === $href[1] ) {
     506            return false;
     507        }
     508
     509        $class = $p->get_attribute( 'class' );
     510        $id    = $p->get_attribute( 'id' );
     511
     512        return ( is_string( $class ) && false !== stripos( $class, 'skip' ) )
     513            || ( is_string( $id ) && false !== stripos( $id, 'skip' ) );
     514    }
     515
     516    /** An element with this id (or name, as the audit also accepts) exists. */
     517    private static function target_exists( $html, $fragment ) {
     518        return (bool) preg_match( '#\s(?:[iI][dD]|[nN][aA][mM][eE])\s*=\s*(["\']?)' . preg_quote( $fragment, '#' ) . '\1(?=[\s/>])#', $html );
     519    }
     520
     521    /** The page's main content block, at the processor's current tag. */
     522    private static function is_main_block( $p, $tag, $outside ) {
     523        $role = $p->get_attribute( 'role' );
     524
     525        if ( 'MAIN' === $tag || ( is_string( $role ) && 'main' === strtolower( trim( $role ) ) ) ) {
     526            return true;
     527        }
     528
     529        if ( $outside > 0 ) {
     530            return false;
     531        }
     532
     533        $type = $p->get_attribute( 'data-elementor-type' );
     534
     535        if ( is_string( $type ) && in_array( $type, self::CONTENT_DOC_TYPES, true ) ) {
     536            return true;
     537        }
     538
     539        return 'main-content' === $p->get_attribute( 'id' );
     540    }
     541
     542    /** $base, or $base-2, -3 … when the document already uses it. */
     543    private static function unique_id( $html, $base ) {
     544        $id = $base;
     545        for ( $n = 2; self::target_exists( $html, $id ) && $n < 50; $n++ ) {
     546            $id = $base . '-' . $n;
     547        }
     548        return $id;
    357549    }
    358550
  • easy-optimizer/trunk/includes/class-easyopt-autoloader.php

    r3697647 r3747021  
    5959            'EasyOpt_Images_Delivery'      => 'includes/class-easyopt-images-delivery.php',
    6060            'EasyOpt_Images_Encoder'       => 'includes/class-easyopt-images-encoder.php',
     61            'EasyOpt_Images_Media'         => 'includes/class-easyopt-images-media.php',
    6162            'EasyOpt_Config'               => 'includes/class-easyopt-config.php',
    6263            'EasyOpt_CSS_Tokenizer'        => 'includes/class-easyopt-css-tokenizer.php',
  • easy-optimizer/trunk/includes/class-easyopt-cache-preload.php

    r3633323 r3747021  
    10901090        // per minute; logs only when the queue stalls or grows abnormally.
    10911091        add_action( 'easyopt_queue_watchdog', array( __CLASS__, 'watchdog_health_check' ) );
     1092        // (2.7.3) The watchdog is only scheduled while there is work now; keep
     1093        // it ticking while the check above still has something to watch.
     1094        add_filter( 'easyopt_queue_watchdog_keep', array( __CLASS__, 'watchdog_keep' ) );
    10921095
    10931096        // Phase 7 — traffic-aware retention. Opt-in stale page-cache pruning on
    … …  
    11381141     * Cost: one option read + one option write per minute. No filesystem work.
    11391142     */
     1143    /**
     1144     * (2.7.3) Keep the watchdog scheduled while watchdog_health_check() can
     1145     * still act: a running or paused run, or the 30 min post-done reconcile.
     1146     * Same conditions it checks before taking its fast exit.
     1147     */
     1148    public static function watchdog_keep( $keep ) {
     1149        if ( $keep ) {
     1150            return true;
     1151        }
     1152        $status = (string) get_option( 'easyopt_cache_preload_status', 'idle' );
     1153        return 'running' === $status || 'paused' === $status
     1154            || ( 'done' === $status && false !== get_transient( 'easyopt_preload_post_done_reconcile' ) );
     1155    }
     1156
    11401157    public static function watchdog_health_check() {
    11411158        if ( ! class_exists( 'EasyOpt_Debug_Log' ) ) {
  • easy-optimizer/trunk/includes/class-easyopt-cdn-cloud.php

    r3731476 r3747021  
    936936                // EasyOpt_REST_Cloud::arm_maintenance().
    937937                set_transient( 'easyopt_cloud_maint', 1, 10 * MINUTE_IN_SECONDS );
     938                update_option( self::CHECKED_OPTION, time(), true );
    938939
    939940                self::usage( true );
    … …  
    942943                        self::map_refresh( self::collect_originals() );
    943944                }
     945        }
     946
     947        /**
     948         * (2.7.3) Last time the status was asked for. Autoloaded: it is read on
     949         * front-end requests by EasyOpt_REST_Cloud::arm_frontend_check().
     950         */
     951        const CHECKED_OPTION = 'easyopt_cloud_checked_at';
     952
     953        /**
     954         * (2.7.3) The status check for sites whose WP-Cron never runs — see
     955         * EasyOpt_REST_Cloud::arm_frontend_check(). Same throttle as maintain();
     956         * only the usage call, which is what learns of a revoked licence.
     957         */
     958        public static function check_now() {
     959                if ( ! self::is_connected() || get_transient( 'easyopt_cloud_maint' ) ) {
     960                        return;
     961                }
     962                set_transient( 'easyopt_cloud_maint', 1, 10 * MINUTE_IN_SECONDS );
     963                update_option( self::CHECKED_OPTION, time(), true );
     964                self::usage( true );
    944965        }
    945966
  • easy-optimizer/trunk/includes/class-easyopt-images.php

    r3697647 r3747021  
    5555    const META_CLIENTSIDE = '_easyopt_img_clientside';
    5656
     57    /**
     58     * Set when the user restores ONE image from the Media Library. Bulk runs
     59     * leave it alone until they optimize it again — otherwise the next
     60     * "Optimize all" would simply redo what they just undid. Kept apart from
     61     * META so restore (which deletes META) never clears it.
     62     */
     63    const META_EXCLUDE = '_easyopt_img_exclude';
     64
    5765    /** Queue wiring. */
    5866    const QUEUE_GROUP    = 'images';
    … …  
    6674    /** Sources we will process. */
    6775    const MIME = array( 'image/jpeg', 'image/png', 'image/gif' );
     76
     77    /** True while restore regenerates core's sizes — not a new upload. */
     78    private static $restoring = false;
    6879
    6980    /**
    … …  
    101112            add_filter( 'wp_generate_attachment_metadata', array( __CLASS__, 'on_generate_metadata' ), 20, 3 );
    102113        }
     114
     115        // Per-image status, Restore and Optimize in the Media Library.
     116        if ( is_admin() ) {
     117            EasyOpt_Images_Media::init();
     118        }
    103119    }
    104120
    … …  
    176192     */
    177193    public static function on_generate_metadata( $metadata, $attachment_id, $context = 'create' ) {
    178         if ( 'create' !== $context ) {
     194        // Restore regenerates sizes through this same filter with 'create'.
     195        // Queueing then would re-optimize (and re-shrink) what was just restored.
     196        if ( 'create' !== $context || self::$restoring ) {
    179197            return $metadata;
    180198        }
    … …  
    225243        }
    226244        if ( $n ) {
     245            // Queued images leave the cached "untouched" count (see stats()).
     246            delete_transient( self::STATS_CACHE );
    227247            $q->start_queue();
    228248        }
    … …  
    243263        }
    244264        if ( $n ) {
     265            delete_transient( self::STATS_CACHE );
    245266            $q->start_queue();
    246267        }
    247268        return $n;
     269    }
     270
     271    /**
     272     * Stop a bulk run (or a restore-all). Drops every queued task; the image
     273     * being encoded right now finishes, and everything already done stays
     274     * done. "Optimize N remaining" picks up where it stopped.
     275     */
     276    public static function stop() {
     277        self::queue( self::HOOK_OPTIMIZE )->clear_queue();
     278        self::queue( self::HOOK_RESTORE )->clear_queue();
     279        delete_transient( self::STATS_CACHE );
    248280    }
    249281
    … …  
    310342    public static function run_optimize( $task ) {
    311343        $id = isset( $task['id'] ) ? (int) $task['id'] : 0;
    312         if ( $id <= 0 ) {
     344        if ( $id <= 0 || self::is_excluded( $id ) ) {
    313345            return;
    314346        }
    … …  
    396428        $meta['sig']         = self::settings_signature();
    397429        update_post_meta( $id, self::META, $meta );
     430
     431        // Restored from the Media Library while this encode ran: undo the run
     432        // (variants and any shrink) so the user's restore holds. The meta cache
     433        // was filled when this run started, so drop it to see the restore.
     434        wp_cache_delete( $id, 'post_meta' );
     435        if ( self::is_excluded( $id ) ) {
     436            self::run_restore( array( 'id' => $id ) );
     437        }
    398438    }
    399439
    … …  
    498538                        require_once ABSPATH . 'wp-admin/includes/image.php';
    499539                    }
    500                     $regenerated = wp_generate_attachment_metadata( $id, $main );
     540                    self::$restoring = true;
     541                    $regenerated     = wp_generate_attachment_metadata( $id, $main );
     542                    self::$restoring = false;
    501543                    if ( is_array( $regenerated ) ) {
    502544                        wp_update_attachment_metadata( $id, $regenerated );
    … …  
    509551        delete_post_meta( $id, self::META );
    510552        delete_post_meta( $id, self::META_CLIENTSIDE );
     553    }
     554
     555    /**
     556     * Restore ONE image now (Media Library) and keep bulk runs off it.
     557     * Exclusion first, so an encode of this image already in flight sees it
     558     * when it finishes and undoes itself (see run_optimize()).
     559     */
     560    public static function restore_one( $id ) {
     561        $id = (int) $id;
     562        update_post_meta( $id, self::META_EXCLUDE, 1 );
     563        self::run_restore( array( 'id' => $id ) );
     564        delete_transient( self::STATS_CACHE );
     565    }
     566
     567    /**
     568     * Optimize ONE image (Media Library): lift a restore's exclusion, queue it.
     569     *
     570     * @return bool Queued.
     571     */
     572    public static function optimize_one( $id ) {
     573        $id = (int) $id;
     574        delete_post_meta( $id, self::META_EXCLUDE );
     575        delete_transient( self::STATS_CACHE );
     576        return self::enqueue( array( $id ) ) > 0;
    511577    }
    512578
    … …  
    622688    }
    623689
     690    /** Restored by hand from the Media Library (see META_EXCLUDE). */
     691    public static function is_excluded( $id ) {
     692        return (bool) get_post_meta( (int) $id, self::META_EXCLUDE, true );
     693    }
     694
    624695    /**
    625696     * Has this attachment already been through the optimizer at all — including
    … …  
    630701     */
    631702    public static function has_been_attempted( $id ) {
     703        if ( self::is_excluded( $id ) ) {
     704            return true; // Restored by hand: bulk runs skip it.
     705        }
    632706        $meta = get_post_meta( (int) $id, self::META, true );
    633707        // Sig-aware: an attempt made under DIFFERENT settings is stale, so the
    … …  
    662736     */
    663737    public static function stats() {
    664         // The expensive half — a scan of every _easyopt_img row — is cached
    665         // briefly and keyed on the settings signature, so a 2s progress poll on
    666         // a large library does not re-scan every tick (and a settings change
    667         // invalidates it at once). Queue depth is always live below.
    668         $agg = get_transient( self::STATS_CACHE );
    669         if ( ! is_array( $agg ) || ( $agg['sig'] ?? '' ) !== self::settings_signature() ) {
    670             $agg = self::compute_aggregate();
    671             set_transient( self::STATS_CACHE, $agg, 20 );
    672         }
    673 
    674738        // Live queue depth — cheap, and must stay real-time for the poll.
    675739        $oq        = self::queue( self::HOOK_OPTIMIZE );
    … …  
    678742        $failed    = $oq->get_failed_count();
    679743        $restoring = $rq->get_pending_count();
     744        $busy      = ( $pending + $restoring ) > 0;
     745
     746        // The expensive half — a scan of every _easyopt_img row — is cached
     747        // briefly and keyed on the settings signature, so a 2s progress poll on
     748        // a large library does not re-scan every tick (and a settings change
     749        // invalidates it at once). Also re-scanned once when the queue goes
     750        // idle, so the resting numbers are exact rather than up to 20s old.
     751        $agg = get_transient( self::STATS_CACHE );
     752        if ( ! is_array( $agg ) || ( $agg['sig'] ?? '' ) !== self::settings_signature()
     753            || ( ! empty( $agg['busy'] ) && ! $busy ) ) {
     754            $agg         = self::compute_aggregate();
     755            $agg['busy'] = $busy;
     756            // Images neither processed nor queued. Unlike `current`, this does
     757            // not move while the queue works (it only moves when something is
     758            // queued or restored, and both clear this cache), so the live
     759            // `processed` below stays exact on every poll with no re-scan.
     760            $agg['untouched'] = max( 0, $agg['total'] - $agg['current'] - $pending - $failed );
     761            set_transient( self::STATS_CACHE, $agg, 20 );
     762        }
    680763
    681764        return array(
    682765            'total'       => $agg['total'],
     766            // Live. Every image that is neither queued nor untouched has been
     767            // processed: optimized, skipped, failed or restored by hand. So
     768            // processed + pending = total for a full run, on every poll.
     769            'processed'   => max( 0, min( $agg['total'], $agg['total'] - $pending - $agg['untouched'] ) ),
    683770            'optimized'   => $agg['optimized'],
    684771            // Reaches 100% even when some images can't be shrunk, and re-opens
    … …  
    687774            // Attempted at the current settings but produced no servable variant.
    688775            'skipped'     => $agg['skipped'],
     776            // Restored one by one in the Media Library; bulk runs skip them.
     777            'excluded'    => $agg['excluded'],
    689778            'saved'       => $agg['saved'],
    690779            'saved_human' => size_format( $agg['saved'] ),
    … …  
    743832        }
    744833
     834        // Restored by hand: done as far as bulk runs are concerned, so they
     835        // count as current or "Optimize N remaining" could never reach zero.
     836        // Restore deleted their _easyopt_img row, so no double count.
     837        $excluded = (int) $wpdb->get_var(
     838            $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = %s", self::META_EXCLUDE )
     839        );
     840
    745841        return array(
    746842            'total'     => $total,
    747843            'optimized' => $optimized,
    748             'current'   => $current,
     844            'current'   => min( $total, $current + $excluded ),
    749845            'skipped'   => $skipped,
     846            'excluded'  => $excluded,
    750847            'saved'     => $saved,
    751848            'sig'       => $sig,
  • easy-optimizer/trunk/includes/class-easyopt-lcp.php

    r3731476 r3747021  
    303303            'mobileBreak' => self::MOBILE_BREAK,
    304304            'minLcpSize'  => self::MIN_LCP_SIZE,
     305            // (2.7.3) Render time: the beacon's per-page memory is keyed to
     306            // it, so a cached copy is reported once per browser, while a
     307            // re-render (e.g. after the data was cleared) reports again.
     308            'rendered'    => time(),
    305309            'need'        => array(
    306310                'lcp'     => $need_lcp ? 1 : 0,
  • easy-optimizer/trunk/includes/class-easyopt-queue.php

    r3621746 r3747021  
    216216        add_action( 'plugins_loaded', array( __CLASS__, 'maybe_create_table' ), 5 );
    217217
    218         // Watchdog: schedule on init, run on its hook.
     218        // Watchdog: run on its hook. (2.7.3) Scheduled only while there is
     219        // work: add_task() arms it, run_watchdog() drops it once the queue is
     220        // empty, and admin + cron requests re-arm it as a safety net. It used
     221        // to be a permanent every-minute event, which on WP-Cron-driven hosts
     222        // spawned wp-cron.php about once a minute forever to find nothing.
     223        // Front-end and admin-ajax requests (beacons, heartbeat) never check:
     224        // the flag it needs is not autoloaded, so checking there would cost a
     225        // query per request. Priority 99 so modules that keep it alive
     226        // (preload, on init:10) have registered.
    219227        add_filter( 'cron_schedules',          array( __CLASS__, 'register_schedule' ) );
    220         add_action( 'init',                    array( __CLASS__, 'setup_watchdog'    ) );
     228        if ( ( is_admin() && ! wp_doing_ajax() ) || wp_doing_cron() ) {
     229            add_action( 'init',                array( __CLASS__, 'maybe_setup_watchdog' ), 99 );
     230        }
    221231        add_action( self::WATCHDOG_HOOK,       array( __CLASS__, 'run_watchdog'      ) );
    222232
    … …  
    323333                update_option( 'easyopt_queue_active', '1', false );
    324334            }
     335            // (2.7.3) Work exists: make sure the watchdog is ticking.
     336            self::setup_watchdog();
    325337        }
    326338
    … …  
    535547    }
    536548
     549    /** (2.7.3) Re-arm while there is work, drop the event when idle. */
     550    public static function maybe_setup_watchdog() {
     551        if ( self::watchdog_needed() ) {
     552            self::setup_watchdog();
     553        } elseif ( wp_next_scheduled( self::WATCHDOG_HOOK ) ) {
     554            wp_clear_scheduled_hook( self::WATCHDOG_HOOK );
     555        }
     556    }
     557
     558    /**
     559     * Does the per-minute tick have anything to do? Queue work (the flag
     560     * add_task() raises; missing counts as yes, as in run_watchdog()), or a
     561     * module still watching on the tick (preload's health check).
     562     */
     563    private static function watchdog_needed() {
     564        return '0' !== (string) get_option( 'easyopt_queue_active', 'unset' )
     565            || (bool) apply_filters( 'easyopt_queue_watchdog_keep', false );
     566    }
     567
    537568    public static function clear_watchdog() {
    538569        wp_clear_scheduled_hook( self::WATCHDOG_HOOK );
    … …  
    737768        $easyopt_active_flag = get_option( 'easyopt_queue_active', 'unset' );
    738769        if ( '0' === (string) $easyopt_active_flag ) {
     770            self::maybe_setup_watchdog(); // (2.7.3) idle: stop ticking
    739771            return; // one option read and done.
    740772        }
    … …  
    781813            if ( null === $easyopt_any ) {
    782814                update_option( 'easyopt_queue_active', '0', false );
     815                // (2.7.3) About to stop ticking, so close the gap first: a task
     816                // queued between the check above and the flag write saw the
     817                // flag still up and did not raise it again.
     818                if ( null !== $wpdb->get_var( "SELECT 1 FROM $table WHERE status IN ('pending','processing') LIMIT 1" ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
     819                    update_option( 'easyopt_queue_active', '1', false );
     820                } else {
     821                    self::maybe_setup_watchdog();
     822                }
    783823            }
    784824            return;
  • easy-optimizer/trunk/includes/class-easyopt-rest-cloud.php

    r3731476 r3747021  
    5656                // the trigger, which is exactly when freshness matters.
    5757                add_action( 'admin_init', array( __CLASS__, 'arm_maintenance' ), 30 );
     58                add_action( 'template_redirect', array( __CLASS__, 'arm_frontend_check' ) );
    5859                add_action( 'easyopt_cloud_maintain', array( 'EasyOpt_CDN_Cloud', 'maintain' ) );
    5960                if ( ! wp_next_scheduled( 'easyopt_cloud_maintain' ) ) {
    6061                        wp_schedule_event( time() + 900, 'hourly', 'easyopt_cloud_maintain' );
    6162                }
     63        }
     64
     65        /** (2.7.3) Status-check age after which a front-end request steps in. */
     66        const FRONTEND_CHECK_AFTER = 3 * HOUR_IN_SECONDS;
     67
     68        /**
     69         * (2.7.3) Status check for sites whose WP-Cron never runs.
     70         *
     71         * The hourly check is how a site learns its licence was revoked; with no
     72         * cron and no admin visits it never runs, so the site keeps emitting CDN
     73         * URLs and the edge 302s every asset back to origin — an extra round trip
     74         * per file, for good. When the last check is over 3 hours old, the next
     75         * front-end page view runs it AFTER the response has been sent, so the
     76         * visitor never waits. Hosts that cannot finish a response early (no
     77         * PHP-FPM or LiteSpeed) are left exactly as before.
     78         *
     79         * Cost on every other page view: two autoloaded option reads.
     80         */
     81        public static function arm_frontend_check() {
     82                // Settings read first: sites that never connected must not autoload
     83                // the 40 KB cloud class on every page view for this.
     84                if ( '' === (string) EasyOpt_Config::get( 'cloud_account', '' ) || ! EasyOpt_CDN_Cloud::is_connected() ) {
     85                        return;
     86                }
     87                if ( time() - (int) get_option( EasyOpt_CDN_Cloud::CHECKED_OPTION, 0 ) < self::FRONTEND_CHECK_AFTER ) {
     88                        return;
     89                }
     90                if ( ! function_exists( 'fastcgi_finish_request' ) && ! function_exists( 'litespeed_finish_request' ) ) {
     91                        return;
     92                }
     93                // Last on shutdown: the page (and Easy Optimizer's cache write, on
     94                // wp_ob_end_flush_all at priority 1) is already done.
     95                add_action( 'shutdown', array( __CLASS__, 'frontend_check' ), PHP_INT_MAX );
     96        }
     97
     98        /** Hand the page to the visitor, then ask for the status. */
     99        public static function frontend_check() {
     100                if ( function_exists( 'fastcgi_finish_request' ) ) {
     101                        fastcgi_finish_request();
     102                } elseif ( function_exists( 'litespeed_finish_request' ) ) {
     103                        litespeed_finish_request();
     104                } else {
     105                        return;
     106                }
     107                EasyOpt_CDN_Cloud::check_now();
    62108        }
    63109
  • easy-optimizer/trunk/includes/class-easyopt-rest-dashboard.php

    r3700847 r3747021  
    133133        ) + $admin );
    134134
     135        register_rest_route( self::NS, '/images/stop', array(
     136            'methods'  => 'POST',
     137            'callback' => array( __CLASS__, 'images_stop' ),
     138        ) + $admin );
     139
     140        // One image, from the Media Library: { id, do: restore|optimize }.
     141        register_rest_route( self::NS, '/images/item', array(
     142            'methods'  => 'POST',
     143            'callback' => array( __CLASS__, 'images_item' ),
     144        ) + $admin );
     145
    135146        // ── Cloudflare ───────────────────────────────────────────────
    136147        register_rest_route( self::NS, '/cloudflare/test', array(
    … …  
    11311142    /** Cached bandwidth usage for the connected license (panel usage bar). */
    11321143    public static function fluxcdn_usage( $request = null ) {
    1133         if ( class_exists( 'EasyOpt_Tracker' ) ) {
    1134             EasyOpt_Tracker::smartimg_tab_viewed(); // daily-throttled inside
    1135         }
    11361144        $force = $request && (bool) $request->get_param( 'force' );
    11371145        $u = class_exists( 'EasyOpt_CDN' ) ? EasyOpt_CDN::usage_status( $force ) : null;
    … …  
    11481156        $endpoint   = ''; // filled from provisioning (per-customer zone hostname)
    11491157        $skip_probe = (bool) $request->get_param( 'skip_probe' );
    1150 
    1151         if ( class_exists( 'EasyOpt_Tracker' ) ) {
    1152             EasyOpt_Tracker::smartimg_connect_attempted(
    1153                 null === EasyOpt_CDN::parse_key( $input ) ? 'license' : 'key'
    1154             );
    1155         }
    11561158
    11571159        $license_key   = '';
    … …  
    15291531        ) );
    15301532    }
     1533
     1534    public static function images_stop() {
     1535        if ( ! class_exists( 'EasyOpt_Images' ) ) {
     1536            return new WP_Error( 'easyopt_no_images', 'Image module not loaded.', array( 'status' => 500 ) );
     1537        }
     1538        EasyOpt_Images::stop();
     1539        return rest_ensure_response( array(
     1540            'success' => true,
     1541            'message' => __( 'Stopped. Images already done stay optimized.', 'easy-optimizer' ),
     1542        ) );
     1543    }
     1544
     1545    public static function images_item( $request ) {
     1546        if ( ! class_exists( 'EasyOpt_Images' ) ) {
     1547            return new WP_Error( 'easyopt_no_images', 'Image module not loaded.', array( 'status' => 500 ) );
     1548        }
     1549        $id = (int) $request->get_param( 'id' );
     1550        $do = (string) $request->get_param( 'do' );
     1551        if ( $id <= 0 || 'attachment' !== get_post_type( $id )
     1552            || ! in_array( (string) get_post_mime_type( $id ), EasyOpt_Images::MIME, true )
     1553            || ! in_array( $do, array( 'restore', 'optimize' ), true ) ) {
     1554            return new WP_Error( 'easyopt_bad_image', __( 'Not an image this optimizer handles.', 'easy-optimizer' ), array( 'status' => 400 ) );
     1555        }
     1556        $queued = false;
     1557        if ( 'restore' === $do ) {
     1558            EasyOpt_Images::restore_one( $id );
     1559        } else {
     1560            if ( ! EasyOpt_Images::target_formats() ) {
     1561                return rest_ensure_response( array(
     1562                    'success' => false,
     1563                    'message' => EasyOpt_Images_Capability::summary(),
     1564                ) );
     1565            }
     1566            $queued = EasyOpt_Images::optimize_one( $id );
     1567        }
     1568        return rest_ensure_response( array(
     1569            'success' => true,
     1570            'html'    => EasyOpt_Images_Media::status_html( $id, $queued ),
     1571        ) );
     1572    }
    15311573}
  • easy-optimizer/trunk/includes/class-easyopt-save-coordinator.php

    r3731476 r3747021  
    105105        'easyopt_a11y_viewport', 'easyopt_a11y_role_elements',
    106106        'easyopt_a11y_iframes', 'easyopt_a11y_progressbar', 'easyopt_a11y_tabindex',
     107        'easyopt_a11y_skip_link', 'easyopt_a11y_main_landmark',
    107108        'easyopt_seo_crawlable_links', 'easyopt_seo_image_alts',
    108109    );
  • easy-optimizer/trunk/includes/class-easyopt-settings-registry.php

    r3700847 r3747021  
    291291            'easyopt_a11y_viewport', 'easyopt_a11y_role_elements', 'easyopt_a11y_iframes',
    292292            'easyopt_a11y_progressbar', 'easyopt_a11y_tabindex',
     293            'easyopt_a11y_skip_link', 'easyopt_a11y_main_landmark',
    293294        ) as $k ) {
    294295            $s[ $k ] = array( 'default' => 0 ) + $int_bool;
  • easy-optimizer/trunk/includes/class-easyopt-settings.php

    r3697647 r3747021  
    6868        wp_enqueue_style( 'easyopt-admin', EASYOPT_URL . 'assets/style.css', array(), EASYOPT_VERSION );
    6969
    70         // Smart Images tab-view ping — top-of-funnel measurement. Fires when
    71         // the imgopt tab is clicked OR is already the restored tab on load.
    72         // Once per page load client-side; the tracker throttles to once per
    73         // day server-side, so this is at most one tiny request daily.
    74         wp_add_inline_script( 'easyopt-dashboard', "
    75 (function(){
    76     var sent = false;
    77     function ping(){
    78         if (sent || !window.__EASYOPT__ || !window.__EASYOPT__.restUrl) { return; }
    79         sent = true;
    80         fetch(window.__EASYOPT__.restUrl.replace(/\\/$/, '') + '/track/si-view', {
    81             method: 'POST',
    82             headers: { 'X-WP-Nonce': window.__EASYOPT__.restNonce },
    83             credentials: 'same-origin'
    84         }).catch(function(){});
    85     }
    86     document.addEventListener('click', function(e){
    87         var el = e.target && e.target.closest ? e.target.closest('[data-tab=\"imgopt\"]') : null;
    88         if (el) { ping(); }
    89     });
    90     // Restored-tab case: panel already mounted shortly after load.
    91     var tries = 0;
    92     var t = setInterval(function(){
    93         if (document.getElementById('eop-panel-imgopt')) { ping(); clearInterval(t); }
    94         if (++tries > 20) { clearInterval(t); }
    95     }, 500);
    96 })();
    97 " );
     70        // (2.7.3) The Smart Images tab-view ping is gone: it logged a row per
     71        // admin per hour and measured nothing analyze_started does not.
    9872    }
    9973
  • easy-optimizer/trunk/includes/class-easyopt-tracker.php

    r3700847 r3747021  
    88 * Events sent (only after opt-in):
    99 *  Lifecycle:    activation, deactivation (+reason), heartbeat, uninstall
    10  *  Smart Images: smartimg_tab_viewed, smartimg_connect_attempted,
    11  *                smartimg_delivering, smartimg_fallback (+reason),
    12  *                smartimg_disconnected
     10 *  Smart Images: smartimg_delivering, smartimg_fallback (+reason),
     11 *                smartimg_disconnected (tab-view and connect-attempt
     12 *                events retired in 2.7.3)
    1313 *  Core health:  wizard_completed (+preset), safe_mode_triggered (+trigger),
    1414 *                error_occurred (+code, throttled 1/code/day)
    … …  
    7272        // Loose-coupling entry point: do_action( 'easyopt_track_error', 'EO_CODE' )
    7373        add_action( 'easyopt_track_error', array( __CLASS__, 'error' ), 10, 2 );
    74 
    75         // Smart Images tab-view ping (fired by a tiny admin script when the
    76         // Smart Images tab is opened — connection state irrelevant, so the
    77         // TOP of the funnel is measured, not just already-connected sites).
    78         add_action( 'rest_api_init', function() {
    79             register_rest_route( 'easyopt/v1', '/track/si-view', array(
    80                 'methods'             => 'POST',
    81                 'callback'            => function() {
    82                     EasyOpt_Tracker::smartimg_tab_viewed();
    83                     return rest_ensure_response( array( 'ok' => true ) );
    84                 },
    85                 'permission_callback' => function() { return current_user_can( 'manage_options' ); },
    86             ) );
    87         } );
    8874    }
    8975
    … …  
    244230    /* ── Smart Images funnel ────────────────────────────────────────────── */
    245231
    246     /** Smart Images tab opened (throttled to once per day). */
    247     public static function smartimg_tab_viewed() {
    248         if ( get_transient( 'easyopt_trk_si_tab' ) ) {
    249             return;
    250         }
    251         // Was DAY_IN_SECONDS. Every funnel event downstream of this one is
    252         // per-action, and a per-action numerator over a per-site-per-DAY
    253         // denominator is not a conversion rate — it made the first row of
    254         // the funnel uncomputable.
    255         set_transient( 'easyopt_trk_si_tab', 1, HOUR_IN_SECONDS );
    256         self::send( 'smartimg_tab_viewed' );
    257     }
    258 
    259     /** User submitted a license / key — {mode: license|key}. */
    260     public static function smartimg_connect_attempted( $mode = 'license' ) {
    261         self::send( 'smartimg_connect_attempted', array( 'mode' => sanitize_key( $mode ) ) );
    262     }
    263232
    264233    /**
  • easy-optimizer/trunk/readme.txt

    r3731476 r3747021  
    44Tags: performance, cache, pagespeed, core web vitals, speed
    55Requires at least: 6.4
    6 Tested up to: 7.1.3
     6Tested up to: 7.1
    77Requires PHP: 7.4
    8 Stable tag: 2.7.2
     8Stable tag: 2.7.3
    99License: GNU General Public License v2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    7070* Some elements have a [tabindex] value greater than 0
    7171* Image elements do not have [alt] attributes
     72* Skip links are not focusable
     73* Document does not have a main landmark
    7274
    7375**SEO**
    … …  
    210212
    211213== Changelog ==
     214
     215= 2.7.3 =
     216* New: Restore original or Optimize a single image from the Media Library. A status column in the list view and a row in the image details panel show whether each image is optimized, skipped or restored. An image you restore yourself is left alone by later "Optimize all" runs until you choose Optimize for it again.
     217* New: a Stop button for a running image optimization. Images already done stay optimized, and "Optimize remaining" continues from where it stopped.
     218* New: two Accessibility fixes, off by default. "Skip links are not focusable" points a "Skip to content" link whose target is missing (common with Elementor full-width layouts) at the page's main content. "Document does not have a main landmark" marks the main content area for screen readers on layouts that have none (Elementor, Divi). Both only add attributes; nothing on the page is moved or removed, and pages that already pass are left unchanged.
     219* Fixed: the image optimization progress ("8 of 202 done, 192 to go") now adds up on every refresh. The done count lagged up to 20 seconds behind the to-go count and left out images that were skipped because no smaller copy was possible.
     220* Fixed: restoring an image whose original had been resized no longer queues it to be optimized and resized again straight away.
     221* Improved: less background load. The every-minute background check now runs only while there is work queued (preload, image optimization), instead of all the time. On hosts where WP-Cron runs off visitor traffic, an idle site no longer starts a background request about once a minute.
     222* Improved: a visitor's browser now remembers that it already reported a page's measurements, so a page served from a page cache or CDN is not reported again by the same visitor on later visits. A page rendered again, for example after its data was cleared, still reports.
     223* Removed: two usage-statistics events (Smart Images tab viewed, connect attempted) that only opted-in sites sent. They added noise without measuring anything new.
     224* Fixed: Cloud Optimization now notices an ended licence even on sites where WP-Cron does not run. Previously such a site kept pointing its files at the CDN, which redirected every file back to the site (an extra round trip each). An overdue status check now runs after a page has been sent to the visitor, so nobody waits for it; on servers that cannot do that, nothing changes.
    212225
    213226= 2.7.2 =
    … …  
    420433== Upgrade Notice ==
    421434
     435= 2.7.3 =
     436Adds Restore and Optimize for single images in the Media Library, a Stop button for image optimization, and two optional accessibility fixes (skip links, main landmark). Background checks now run only while work is queued.
     437
    422438= 2.7.2 =
    423439Fixes Remove Unused CSS dropping rules on Tailwind sites, lazy-loaded <noscript> iframes, and several Cloud Optimization issues (LCP preload, edge-served fonts, the Images toggle). Used CSS is rebuilt once after updating, so expect a short regeneration period.
Note: See TracChangeset for help on using the changeset viewer.