Plugin Directory

Changeset 3448585


Ignore:
Timestamp:
01/28/2026 10:30:37 AM (2 months ago)
Author:
quentinldd
Message:

Update to version 2.2.0 from GitHub

Location:
zenpress
Files:
32 added
104 edited
1 copied

Legend:

Unmodified
Added
Removed
  • zenpress/tags/2.2.0/assets/build/index.asset.php

    r3412245 r3448585  
    1 <?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-notices'), 'version' => '980bfa58281a120e84d0');
     1<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-notices'), 'version' => '253355e0f124eff32114');
  • zenpress/tags/2.2.0/assets/build/index.js

    r3412245 r3448585  
    1 (()=>{"use strict";var e={n:s=>{var t=s&&s.__esModule?()=>s.default:()=>s;return e.d(t,{a:t}),t},d:(s,t)=>{for(var n in t)e.o(t,n)&&!e.o(s,n)&&Object.defineProperty(s,n,{enumerable:!0,get:t[n]})},o:(e,s)=>Object.prototype.hasOwnProperty.call(e,s)};const s=window.wp.domReady;var t=e.n(s);const n=window.wp.element,r=window.wp.i18n,a=window.wp.components,i=window.wp.apiFetch;var o=e.n(i);const c=window.wp.data,l=window.wp.notices,p=window.ReactJSXRuntime,d=({label:e,value:s,onChange:t,help:r})=>{const i=(0,n.useRef)(null);return(0,n.useEffect)(()=>((e,s)=>{if(!e)return;const t=t=>{if("Enter"===t.key){const n=e.querySelector('input[type="checkbox"]');n&&(document.activeElement===n||e.contains(document.activeElement))&&(t.preventDefault(),t.stopPropagation(),s())}};return e.addEventListener("keydown",t),()=>{e.removeEventListener("keydown",t)}})(i.current,t),[t]),(0,p.jsx)("div",{ref:i,children:(0,p.jsx)(a.ToggleControl,{label:e,checked:s,onChange:t,help:r,__nextHasNoMarginBottom:!0})})},u=({onClick:e,isBusy:s})=>(0,p.jsx)(a.Button,{variant:"primary",onClick:e,isBusy:s,__next40pxDefaultSize:!0,children:(0,r.__)("Save settings","zenpress")}),b=()=>{const{removeNotice:e}=(0,c.useDispatch)(l.store),s=(0,c.useSelect)(e=>e(l.store).getNotices(),[]);return s&&0!==s.length?(0,p.jsx)(a.NoticeList,{notices:s,onRemove:e}):null},h=(0,n.createContext)(),f=({selectedTabId:e,onSelect:s,orientation:t="horizontal",children:r})=>{const[a,i]=(0,n.useState)(),o=(0,n.useRef)(null),c=void 0!==e?e:a;return(0,p.jsx)(h.Provider,{value:{selectedTabId:c,onSelect:t=>{void 0===e&&i(t),s?.(t)},orientation:t,getOrderedTabIds:()=>o.current?Array.from(o.current.querySelectorAll('[role="tab"]')).map(e=>{const s=e.getAttribute("id");return s?s.replace("zenpress-tab-",""):null}).filter(Boolean):[],tabListRef:o},children:(0,p.jsx)("div",{className:`zenpress-tabs zenpress-tabs--${t}`,children:r})})};f.TabList=({children:e})=>{const{orientation:s,tabListRef:t}=(0,n.useContext)(h);return(0,p.jsx)("div",{ref:t,className:`zenpress-tabs__list zenpress-tabs__list--${s}`,role:"tablist","aria-orientation":s,children:e})},f.Tab=({tabId:e,title:s,className:t="",children:r})=>{const{selectedTabId:a,onSelect:i,orientation:o,getOrderedTabIds:c}=(0,n.useContext)(h),l=a===e,d=(0,n.useRef)(null);return(0,p.jsx)("button",{ref:d,className:`zenpress-tabs__tab ${l?"zenpress-tabs__tab--is-active":""} ${t}`.trim(),role:"tab","aria-selected":l,"aria-controls":`zenpress-tab-panel-${e}`,id:`zenpress-tab-${e}`,tabIndex:l?0:-1,onClick:()=>i(e),onKeyDown:s=>{const t=c();if(!t||0===t.length)return;const n=t.indexOf(e);if(-1===n)return;let r=n;if("vertical"===o?"ArrowDown"===s.key?(s.preventDefault(),r=n<t.length-1?n+1:0):"ArrowUp"===s.key&&(s.preventDefault(),r=n>0?n-1:t.length-1):"ArrowRight"===s.key?(s.preventDefault(),r=n<t.length-1?n+1:0):"ArrowLeft"===s.key&&(s.preventDefault(),r=n>0?n-1:t.length-1),"Home"===s.key?(s.preventDefault(),r=0):"End"===s.key&&(s.preventDefault(),r=t.length-1)," "===s.key||"Enter"===s.key)return s.preventDefault(),void i(e);if(r!==n&&r>=0&&r<t.length){const e=t[r],s=document.getElementById(`zenpress-tab-${e}`);s&&(s.focus(),i(e))}},onFocus:()=>{l||i(e)},children:s||r})},f.TabPanel=({tabId:e,children:s})=>{const{selectedTabId:t}=(0,n.useContext)(h),r=(0,n.useRef)(null),a=t===e;return(0,n.useEffect)(()=>{a&&r.current&&(0===r.current.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])').length?r.current.setAttribute("tabindex","0"):r.current.removeAttribute("tabindex"))},[a]),a?(0,p.jsx)("div",{ref:r,className:"zenpress-tabs__panel",role:"tabpanel",id:`zenpress-tab-panel-${e}`,"aria-labelledby":`zenpress-tab-${e}`,children:s}):(0,p.jsx)("div",{className:"zenpress-tabs__panel",role:"tabpanel",id:`zenpress-tab-panel-${e}`,"aria-labelledby":`zenpress-tab-${e}`,hidden:!0,children:s})};const x=()=>{const{snippets:e,setSnippets:s,saveSettings:t,isSaving:i}=(()=>{const[e,s]=(0,n.useState)({}),[t,a]=(0,n.useState)(!1),{createSuccessNotice:i,createErrorNotice:p}=(0,c.useDispatch)(l.store);return(0,n.useEffect)(()=>{o()({path:"/wp/v2/settings"}).then(e=>{const t=Array.isArray(e?.zenpress_active_snippets)?e.zenpress_active_snippets:[],n=window?.zenpressSnippetsMeta||{},r={};Object.keys(n).forEach(e=>{r[e]={...n[e],"enable-snippet":t.includes(e)}}),s(r)}).catch(()=>{p((0,r.__)("Failed to load settings.","zenpress"))})},[p]),{snippets:e,setSnippets:s,saveSettings:async()=>{a(!0);const s=Object.keys(e).filter(s=>e[s]?.["enable-snippet"]);try{await o()({path:"/wp/v2/settings",method:"POST",data:{zenpress_active_snippets:s}}),i((0,r.__)("Settings saved.","zenpress"))}catch{p((0,r.__)("Failed to save settings.","zenpress"))}finally{a(!1)}},isSaving:t}})(),[h,x]=(0,n.useState)(),_=e=>{s(s=>{const t={};return Object.entries(s).forEach(([s,n])=>{const r=(Array.isArray(n?.preset)?n.preset:[]).includes(e);t[s]={...n,"enable-snippet":r}}),t})},m=e=>e?e.charAt(0).toUpperCase()+e.slice(1).toLowerCase():e,v=["core","gutenberg","woocommerce","ads-blocker","tools"],y={};Object.keys(e).forEach(s=>{const t=e[s],n=(t?.category||(0,r.__)("Uncategorized","zenpress")).toLowerCase(),a=(t?.subcategory||(0,r.__)("uncategorized","zenpress")).toLowerCase();y[n]||(y[n]={}),y[n][a]||(y[n][a]=[]),y[n][a].push({name:s,data:t})});const z=Object.keys(y).sort((e,s)=>{const t=v.indexOf(e.toLowerCase()),n=v.indexOf(s.toLowerCase());return-1!==t&&-1!==n?t-n:-1!==t?-1:-1!==n?1:e.localeCompare(s,void 0,{sensitivity:"base"})});return(0,n.useEffect)(()=>{!h&&z.length>0&&x(z[0])},[h,z.length]),(0,n.useEffect)(()=>{const e=e=>{(e.ctrlKey||e.metaKey)&&"s"===e.key&&(e.preventDefault(),i||t())};return document.addEventListener("keydown",e),()=>{document.removeEventListener("keydown",e)}},[t,i]),(0,p.jsxs)("article",{className:"zenpress-row",children:[(0,p.jsxs)("section",{className:"zenpress-main",children:[(0,p.jsx)("div",{className:"zenpress-notices",children:(0,p.jsx)(b,{})}),(0,p.jsxs)("div",{className:"zenpress-panel",children:[(0,p.jsxs)(f,{orientation:"vertical",selectedTabId:h,onSelect:e=>{x(e),x(e)},children:[(0,p.jsx)(f.TabList,{children:z.map(e=>{const s=`zenpress-tabs__tab--category-${e.toLowerCase().replace(/\s+/g,"-")}`;return(0,p.jsx)(f.Tab,{tabId:e,title:m(e),className:s,children:m(e)},e)})}),z.map(e=>{const t=Object.keys(y[e]).sort();return(0,p.jsxs)(f.TabPanel,{tabId:e,children:[(0,p.jsx)("h2",{children:m(e)}),t.map(t=>(0,p.jsxs)("div",{className:`zenpress-subcategory zenpress-subcategory-${t.toLowerCase().replace(/\s+/g,"-")}`,children:[(0,p.jsx)("hr",{}),(0,p.jsx)("h3",{children:m(t)}),y[e][t].map(({name:e,data:t})=>(0,p.jsx)(d,{label:t.title||e,value:t?.["enable-snippet"]||!1,onChange:()=>{return t=e,void s(e=>({...e,[t]:{...e[t],"enable-snippet":!e[t]?.["enable-snippet"]}}));var t},help:t.description||""},e))]},t))]},e)})]}),(0,p.jsxs)("div",{className:"zenpress-actions",children:[(0,p.jsxs)("div",{className:"zenpress-actions-bulk",children:[(0,p.jsx)(a.Button,{variant:"tertiary",onClick:()=>{s(e=>{const s={};return Object.keys(e).forEach(t=>{s[t]={...e[t],"enable-snippet":!0}}),s})},__next40pxDefaultSize:!0,children:(0,r.__)("Enable all actions","zenpress")}),(0,p.jsx)(a.Button,{isDestructive:!0,onClick:()=>{s(e=>{const s={};return Object.keys(e).forEach(t=>{s[t]={...e[t],"enable-snippet":!1}}),s})},__next40pxDefaultSize:!0,children:(0,r.__)("Disable all actions","zenpress")})]}),(0,p.jsx)(u,{onClick:t,isBusy:i})]})]})]}),(0,p.jsx)("aside",{className:"zenpress-sidebar",children:(0,p.jsx)("div",{className:"zenpress-presets",children:(0,p.jsxs)("div",{className:"zenpress-presets-description",children:[(0,p.jsx)("h2",{children:(0,r.__)("Pick configuration preset","zenpress")}),(0,p.jsx)("p",{children:(0,r.__)("Don't know which features to enable? Quickly configure ZenPress by selecting a preset that matches your site type. Each preset enables optimized features for your specific use case.","zenpress")}),(0,p.jsx)("hr",{}),(0,p.jsxs)("h3",{children:["🖼️ ",(0,r.__)("Corporate website","zenpress")]}),(0,p.jsx)("p",{children:(0,r.__)("Optimized for business sites and portfolios. Focuses on security, performance, and removing unnecessary features like RSS feeds and author archives.","zenpress")}),(0,p.jsx)(a.Button,{variant:"secondary",onClick:()=>_("corporate-website"),__next40pxDefaultSize:!0,children:(0,r.__)("Enable","zenpress")}),(0,p.jsx)("hr",{}),(0,p.jsxs)("h3",{children:[" 📰 ",(0,r.__)("Blog","zenpress")]}),(0,p.jsx)("p",{children:(0,r.__)("Tailored for content-focused blogs. Includes performance and security optimizations while preserving essential blog features like RSS feeds.","zenpress")}),(0,p.jsx)(a.Button,{variant:"secondary",onClick:()=>_("blog"),__next40pxDefaultSize:!0,children:(0,r.__)("Enable","zenpress")}),(0,p.jsx)("hr",{}),(0,p.jsxs)("h3",{children:["🛒 ",(0,r.__)("E-commerce","zenpress")]}),(0,p.jsx)("p",{children:(0,r.__)("Designed for WooCommerce stores. Includes all performance and security features plus WooCommerce-specific optimizations for faster checkout.","zenpress")}),(0,p.jsx)(a.Button,{variant:"secondary",onClick:()=>_("ecommerce"),__next40pxDefaultSize:!0,children:(0,r.__)("Enable","zenpress")})]})})})]})};t()(()=>{const e=document.getElementById("zenpress-settings");e&&(0,n.createRoot)(e).render((0,p.jsx)(x,{}))})})();
     1(()=>{"use strict";var e={n:s=>{var t=s&&s.__esModule?()=>s.default:()=>s;return e.d(t,{a:t}),t},d:(s,t)=>{for(var n in t)e.o(t,n)&&!e.o(s,n)&&Object.defineProperty(s,n,{enumerable:!0,get:t[n]})},o:(e,s)=>Object.prototype.hasOwnProperty.call(e,s)};const s=window.wp.domReady;var t=e.n(s);const n=window.wp.element,r=window.wp.i18n,a=window.wp.components,i=window.wp.apiFetch;var o=e.n(i);const c=window.wp.data,l=window.wp.notices,p=window.ReactJSXRuntime,d=({label:e,value:s,onChange:t,help:r})=>{const i=(0,n.useRef)(null);return(0,n.useEffect)(()=>((e,s)=>{if(!e)return;const t=t=>{if("Enter"===t.key){const n=e.querySelector('input[type="checkbox"]'),r=e.ownerDocument||document;n&&(r.activeElement===n||e.contains(r.activeElement))&&(t.preventDefault(),t.stopPropagation(),s())}};return e.addEventListener("keydown",t),()=>{e.removeEventListener("keydown",t)}})(i.current,t),[t]),(0,p.jsx)("div",{ref:i,children:(0,p.jsx)(a.ToggleControl,{label:e,checked:s,onChange:t,help:r,__nextHasNoMarginBottom:!0})})},u=({onClick:e,isBusy:s})=>(0,p.jsx)(a.Button,{variant:"primary",onClick:e,isBusy:s,__next40pxDefaultSize:!0,children:(0,r.__)("Save settings","zenpress")}),b=()=>{const{removeNotice:e}=(0,c.useDispatch)(l.store),s=(0,c.useSelect)(e=>e(l.store).getNotices(),[]);return s&&0!==s.length?(0,p.jsx)(a.NoticeList,{notices:s,onRemove:e}):null},h=(0,n.createContext)(),f=({selectedTabId:e,onSelect:s,orientation:t="horizontal",children:r})=>{const[a,i]=(0,n.useState)(),o=(0,n.useRef)(null),c=void 0!==e?e:a;return(0,p.jsx)(h.Provider,{value:{selectedTabId:c,onSelect:t=>{void 0===e&&i(t),s?.(t)},orientation:t,getOrderedTabIds:()=>o.current?Array.from(o.current.querySelectorAll('[role="tab"]')).map(e=>{const s=e.getAttribute("id");return s?s.replace("zenpress-tab-",""):null}).filter(Boolean):[],tabListRef:o},children:(0,p.jsx)("div",{className:`zenpress-tabs zenpress-tabs--${t}`,children:r})})};f.TabList=({children:e})=>{const{orientation:s,tabListRef:t}=(0,n.useContext)(h);return(0,p.jsx)("div",{ref:t,className:`zenpress-tabs__list zenpress-tabs__list--${s}`,role:"tablist","aria-orientation":s,children:e})},f.Tab=({tabId:e,title:s,className:t="",children:r})=>{const{selectedTabId:a,onSelect:i,orientation:o,getOrderedTabIds:c}=(0,n.useContext)(h),l=a===e,d=(0,n.useRef)(null);return(0,p.jsx)("button",{ref:d,className:`zenpress-tabs__tab ${l?"zenpress-tabs__tab--is-active":""} ${t}`.trim(),role:"tab","aria-selected":l,"aria-controls":`zenpress-tab-panel-${e}`,id:`zenpress-tab-${e}`,tabIndex:l?0:-1,onClick:()=>i(e),onKeyDown:s=>{const t=c();if(!t||0===t.length)return;const n=t.indexOf(e);if(-1===n)return;let r=n;if("vertical"===o?"ArrowDown"===s.key?(s.preventDefault(),r=n<t.length-1?n+1:0):"ArrowUp"===s.key&&(s.preventDefault(),r=n>0?n-1:t.length-1):"ArrowRight"===s.key?(s.preventDefault(),r=n<t.length-1?n+1:0):"ArrowLeft"===s.key&&(s.preventDefault(),r=n>0?n-1:t.length-1),"Home"===s.key?(s.preventDefault(),r=0):"End"===s.key&&(s.preventDefault(),r=t.length-1)," "===s.key||"Enter"===s.key)return s.preventDefault(),void i(e);if(r!==n&&r>=0&&r<t.length){const e=t[r],s=document.getElementById(`zenpress-tab-${e}`);s&&(s.focus(),i(e))}},onFocus:()=>{l||i(e)},children:s||r})},f.TabPanel=({tabId:e,children:s})=>{const{selectedTabId:t}=(0,n.useContext)(h),r=(0,n.useRef)(null),a=t===e;return(0,n.useEffect)(()=>{a&&r.current&&(0===r.current.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])').length?r.current.setAttribute("tabindex","0"):r.current.removeAttribute("tabindex"))},[a]),a?(0,p.jsx)("div",{ref:r,className:"zenpress-tabs__panel",role:"tabpanel",id:`zenpress-tab-panel-${e}`,"aria-labelledby":`zenpress-tab-${e}`,children:s}):(0,p.jsx)("div",{className:"zenpress-tabs__panel",role:"tabpanel",id:`zenpress-tab-panel-${e}`,"aria-labelledby":`zenpress-tab-${e}`,hidden:!0,children:s})};const x=()=>{const{snippets:e,setSnippets:s,saveSettings:t,isSaving:i}=(()=>{const[e,s]=(0,n.useState)({}),[t,a]=(0,n.useState)(!1),{createSuccessNotice:i,createErrorNotice:p}=(0,c.useDispatch)(l.store);return(0,n.useEffect)(()=>{o()({path:"/wp/v2/settings"}).then(e=>{const t=Array.isArray(e?.zenpress_active_snippets)?e.zenpress_active_snippets:[],n=window?.zenpressSnippetsMeta||{},r={};Object.keys(n).forEach(e=>{r[e]={...n[e],"enable-snippet":t.includes(e)}}),s(r)}).catch(()=>{p((0,r.__)("Failed to load settings.","zenpress"))})},[p]),{snippets:e,setSnippets:s,saveSettings:async()=>{a(!0);const s=Object.keys(e).filter(s=>e[s]?.["enable-snippet"]);try{await o()({path:"/wp/v2/settings",method:"POST",data:{zenpress_active_snippets:s}}),i((0,r.__)("Settings saved.","zenpress"))}catch{p((0,r.__)("Failed to save settings.","zenpress"))}finally{a(!1)}},isSaving:t}})(),[h,x]=(0,n.useState)(),_=e=>{s(s=>{const t={};return Object.entries(s).forEach(([s,n])=>{const r=(Array.isArray(n?.preset)?n.preset:[]).includes(e);t[s]={...n,"enable-snippet":r}}),t})},m=e=>e?e.charAt(0).toUpperCase()+e.slice(1).toLowerCase():e,v=["core","gutenberg","woocommerce","ads-blocker","tools"],y={};Object.keys(e).forEach(s=>{const t=e[s],n=(t?.category||(0,r.__)("Uncategorized","zenpress")).toLowerCase(),a=(t?.subcategory||(0,r.__)("uncategorized","zenpress")).toLowerCase();y[n]||(y[n]={}),y[n][a]||(y[n][a]=[]),y[n][a].push({name:s,data:t})});const z=Object.keys(y).sort((e,s)=>{const t=v.indexOf(e.toLowerCase()),n=v.indexOf(s.toLowerCase());return-1!==t&&-1!==n?t-n:-1!==t?-1:-1!==n?1:e.localeCompare(s,void 0,{sensitivity:"base"})});return(0,n.useEffect)(()=>{!h&&z.length>0&&x(z[0])},[h,z]),(0,n.useEffect)(()=>{const e=e=>{(e.ctrlKey||e.metaKey)&&"s"===e.key&&(e.preventDefault(),i||t())};return document.addEventListener("keydown",e),()=>{document.removeEventListener("keydown",e)}},[t,i]),(0,p.jsxs)("article",{className:"zenpress-row",children:[(0,p.jsxs)("section",{className:"zenpress-main",children:[(0,p.jsx)("div",{className:"zenpress-notices",children:(0,p.jsx)(b,{})}),(0,p.jsxs)("div",{className:"zenpress-panel",children:[(0,p.jsxs)(f,{orientation:"vertical",selectedTabId:h,onSelect:e=>{x(e),x(e)},children:[(0,p.jsx)(f.TabList,{children:z.map(e=>{const s=`zenpress-tabs__tab--category-${e.toLowerCase().replace(/\s+/g,"-")}`;return(0,p.jsx)(f.Tab,{tabId:e,title:m(e),className:s,children:m(e)},e)})}),z.map(e=>{const t=Object.keys(y[e]).sort();return(0,p.jsxs)(f.TabPanel,{tabId:e,children:[(0,p.jsx)("h2",{children:m(e)}),t.map(t=>(0,p.jsxs)("div",{className:`zenpress-subcategory zenpress-subcategory-${t.toLowerCase().replace(/\s+/g,"-")}`,children:[(0,p.jsx)("hr",{}),(0,p.jsx)("h3",{children:m(t)}),y[e][t].map(({name:e,data:t})=>(0,p.jsx)(d,{label:t.title||e,value:t?.["enable-snippet"]||!1,onChange:()=>{return t=e,void s(e=>({...e,[t]:{...e[t],"enable-snippet":!e[t]?.["enable-snippet"]}}));var t},help:t.description||""},e))]},t))]},e)})]}),(0,p.jsxs)("div",{className:"zenpress-actions",children:[(0,p.jsxs)("div",{className:"zenpress-actions-bulk",children:[(0,p.jsx)(a.Button,{variant:"tertiary",onClick:()=>{s(e=>{const s={};return Object.keys(e).forEach(t=>{s[t]={...e[t],"enable-snippet":!0}}),s})},__next40pxDefaultSize:!0,children:(0,r.__)("Enable all actions","zenpress")}),(0,p.jsx)(a.Button,{isDestructive:!0,onClick:()=>{s(e=>{const s={};return Object.keys(e).forEach(t=>{s[t]={...e[t],"enable-snippet":!1}}),s})},__next40pxDefaultSize:!0,children:(0,r.__)("Disable all actions","zenpress")})]}),(0,p.jsx)(u,{onClick:t,isBusy:i})]})]})]}),(0,p.jsx)("aside",{className:"zenpress-sidebar",children:(0,p.jsx)("div",{className:"zenpress-presets",children:(0,p.jsxs)("div",{className:"zenpress-presets-description",children:[(0,p.jsx)("h2",{children:(0,r.__)("Pick configuration preset","zenpress")}),(0,p.jsx)("p",{children:(0,r.__)("Don't know which features to enable? Quickly configure ZenPress by selecting a preset that matches your site type. Each preset enables optimized features for your specific use case.","zenpress")}),(0,p.jsx)("hr",{}),(0,p.jsxs)("h3",{children:["🖼️ ",(0,r.__)("Corporate website","zenpress")]}),(0,p.jsx)("p",{children:(0,r.__)("Optimized for business sites and portfolios. Focuses on security, performance, and removing unnecessary features like RSS feeds and author archives.","zenpress")}),(0,p.jsx)(a.Button,{variant:"secondary",onClick:()=>_("corporate-website"),__next40pxDefaultSize:!0,children:(0,r.__)("Enable","zenpress")}),(0,p.jsx)("hr",{}),(0,p.jsxs)("h3",{children:[" 📰 ",(0,r.__)("Blog","zenpress")]}),(0,p.jsx)("p",{children:(0,r.__)("Tailored for content-focused blogs. Includes performance and security optimizations while preserving essential blog features like RSS feeds.","zenpress")}),(0,p.jsx)(a.Button,{variant:"secondary",onClick:()=>_("blog"),__next40pxDefaultSize:!0,children:(0,r.__)("Enable","zenpress")}),(0,p.jsx)("hr",{}),(0,p.jsxs)("h3",{children:["🛒 ",(0,r.__)("E-commerce","zenpress")]}),(0,p.jsx)("p",{children:(0,r.__)("Designed for WooCommerce stores. Includes all performance and security features plus WooCommerce-specific optimizations for faster checkout.","zenpress")}),(0,p.jsx)(a.Button,{variant:"secondary",onClick:()=>_("ecommerce"),__next40pxDefaultSize:!0,children:(0,r.__)("Enable","zenpress")})]})})})]})};t()(()=>{const e=document.getElementById("zenpress-settings");e&&(0,n.createRoot)(e).render((0,p.jsx)(x,{}))})})();
  • zenpress/tags/2.2.0/assets/src/index.js

    r3412245 r3448585  
    88 */
    99domReady(() => {
    10     const rootEl = document.getElementById('zenpress-settings');
    11     if (!rootEl) return;
     10    const rootEl = document.getElementById('zenpress-settings');
     11    if (!rootEl) {
     12        return;
     13    }
    1214
    13     const root = createRoot(rootEl);
    14     root.render(<SettingsPage />);
     15    const root = createRoot(rootEl);
     16    root.render(<SettingsPage />);
    1517});
  • zenpress/tags/2.2.0/assets/src/js/components/Notices.js

    r3412245 r3448585  
    99 */
    1010export const Notices = () => {
    11     const { removeNotice } = useDispatch(noticesStore);
    12     const notices = useSelect((select) => select(noticesStore).getNotices(), []);
     11    const { removeNotice } = useDispatch(noticesStore);
     12    const notices = useSelect(
     13        (select) => select(noticesStore).getNotices(),
     14        []
     15    );
    1316
    14     if (!notices || notices.length === 0) {
    15         return null;
    16     }
     17    if (!notices || notices.length === 0) {
     18        return null;
     19    }
    1720
    18     return <NoticeList notices={notices} onRemove={removeNotice} />;
     21    return <NoticeList notices={notices} onRemove={removeNotice} />;
    1922};
  • zenpress/tags/2.2.0/assets/src/js/components/SaveButton.js

    r3412245 r3448585  
    1111 */
    1212export const SaveButton = ({ onClick, isBusy }) => {
    13     return (
    14         <Button variant="primary" onClick={onClick} isBusy={isBusy} __next40pxDefaultSize>
    15             {__('Save settings', 'zenpress')}
    16         </Button>
    17     );
     13    return (
     14        <Button
     15            variant="primary"
     16            onClick={onClick}
     17            isBusy={isBusy}
     18            __next40pxDefaultSize
     19        >
     20            {__('Save settings', 'zenpress')}
     21        </Button>
     22    );
    1823};
  • zenpress/tags/2.2.0/assets/src/js/components/SnippetToggleControl.js

    r3412245 r3448585  
    77 *
    88 * @param {HTMLElement} container - Container element to attach listener to.
    9  * @param {Function} onChange - Change handler to call when Enter is pressed.
     9 * @param {Function}    onChange - Change handler to call when Enter is pressed.
    1010 */
    1111const addEnterKeySupport = (container, onChange) => {
    12     if (!container) return;
     12    if (!container) {
     13        return;
     14    }
    1315
    14     const handleKeyDown = (e) => {
    15         // Handle Enter key on the toggle control
    16         if (e.key === 'Enter') {
    17             const toggleInput = container.querySelector('input[type="checkbox"]');
    18             if (toggleInput && (document.activeElement === toggleInput || container.contains(document.activeElement))) {
    19                 e.preventDefault();
    20                 e.stopPropagation();
    21                 onChange();
    22             }
    23         }
    24     };
     16    const handleKeyDown = (e) => {
     17        // Handle Enter key on the toggle control
     18        if (e.key === 'Enter') {
     19            const toggleInput = container.querySelector(
     20                'input[type="checkbox"]'
     21            );
     22            const ownerDocument = container.ownerDocument || document;
     23            if (
     24                toggleInput &&
     25                (ownerDocument.activeElement === toggleInput ||
     26                    container.contains(ownerDocument.activeElement))
     27            ) {
     28                e.preventDefault();
     29                e.stopPropagation();
     30                onChange();
     31            }
     32        }
     33    };
    2534
    26     container.addEventListener('keydown', handleKeyDown);
    27     return () => {
    28         container.removeEventListener('keydown', handleKeyDown);
    29     };
     35    container.addEventListener('keydown', handleKeyDown);
     36    return () => {
     37        container.removeEventListener('keydown', handleKeyDown);
     38    };
    3039};
    3140
     
    4453 */
    4554export const SnippetToggleControl = ({ label, value, onChange, help }) => {
    46     const containerRef = useRef(null);
     55    const containerRef = useRef(null);
    4756
    48     // Temporary support for Enter key handling
    49     useEffect(() => {
    50         return addEnterKeySupport(containerRef.current, onChange);
    51     }, [onChange]);
     57    // Temporary support for Enter key handling
     58    useEffect(() => {
     59        return addEnterKeySupport(containerRef.current, onChange);
     60    }, [onChange]);
    5261
    53     return (
    54         <div ref={containerRef}>
    55             <ToggleControl label={label} checked={value} onChange={onChange} help={help} __nextHasNoMarginBottom />
    56         </div>
    57     );
     62    return (
     63        <div ref={containerRef}>
     64            <ToggleControl
     65                label={label}
     66                checked={value}
     67                onChange={onChange}
     68                help={help}
     69                __nextHasNoMarginBottom
     70            />
     71        </div>
     72    );
    5873};
  • zenpress/tags/2.2.0/assets/src/js/components/Tabs.js

    r3412245 r3448585  
    1 import { useState, createContext, useContext, useRef, useEffect } from '@wordpress/element';
     1import {
     2    useState,
     3    createContext,
     4    useContext,
     5    useRef,
     6    useEffect,
     7} from '@wordpress/element';
    28
    39const TabsContext = createContext();
     
    612 * Custom Tabs component with vertical orientation support
    713 *
    8  * @param {Object}   props                - Component props.
    9  * @param {string}   props.selectedTabId  - Currently selected tab ID.
    10  * @param {Function} props.onSelect       - Callback when tab is selected.
    11  * @param {string}   props.orientation    - Tab orientation ('vertical' or 'horizontal').
    12  * @param {Object}   props.children       - Child components (TabList and TabPanels).
     14 * @param {Object}   props               - Component props.
     15 * @param {string}   props.selectedTabId - Currently selected tab ID.
     16 * @param {Function} props.onSelect      - Callback when tab is selected.
     17 * @param {string}   props.orientation   - Tab orientation ('vertical' or 'horizontal').
     18 * @param {Object}   props.children      - Child components (TabList and TabPanels).
    1319 * @return {JSX.Element} The tabs container.
    1420 */
    15 export const Tabs = ({ selectedTabId: controlledId, onSelect, orientation = 'horizontal', children }) => {
    16     const [internalId, setInternalId] = useState();
    17     const tabListRef = useRef(null);
    18     const selectedId = controlledId !== undefined ? controlledId : internalId;
    19     const handleSelect = (id) => {
    20         if (controlledId === undefined) setInternalId(id);
    21         onSelect?.(id);
    22     };
    23 
    24     // Function to get ordered tab IDs from DOM
    25     const getOrderedTabIds = () => {
    26         if (!tabListRef.current) return [];
    27 
    28         const tabs = Array.from(tabListRef.current.querySelectorAll('[role="tab"]'));
    29         return tabs
    30             .map((tab) => {
    31                 const id = tab.getAttribute('id');
    32                 return id ? id.replace('zenpress-tab-', '') : null;
    33             })
    34             .filter(Boolean);
    35     };
    36 
    37     return (
    38         <TabsContext.Provider
    39             value={{ selectedTabId: selectedId, onSelect: handleSelect, orientation, getOrderedTabIds, tabListRef }}
    40         >
    41             <div className={`zenpress-tabs zenpress-tabs--${orientation}`}>{children}</div>
    42         </TabsContext.Provider>
    43     );
     21export const Tabs = ({
     22    selectedTabId: controlledId,
     23    onSelect,
     24    orientation = 'horizontal',
     25    children,
     26}) => {
     27    const [internalId, setInternalId] = useState();
     28    const tabListRef = useRef(null);
     29    const selectedId = controlledId !== undefined ? controlledId : internalId;
     30    const handleSelect = (id) => {
     31        if (controlledId === undefined) {
     32            setInternalId(id);
     33        }
     34        onSelect?.(id);
     35    };
     36
     37    // Function to get ordered tab IDs from DOM
     38    const getOrderedTabIds = () => {
     39        if (!tabListRef.current) {
     40            return [];
     41        }
     42
     43        const tabs = Array.from(
     44            tabListRef.current.querySelectorAll('[role="tab"]')
     45        );
     46        return tabs
     47            .map((tab) => {
     48                const id = tab.getAttribute('id');
     49                return id ? id.replace('zenpress-tab-', '') : null;
     50            })
     51            .filter(Boolean);
     52    };
     53
     54    return (
     55        <TabsContext.Provider
     56            value={{
     57                selectedTabId: selectedId,
     58                onSelect: handleSelect,
     59                orientation,
     60                getOrderedTabIds,
     61                tabListRef,
     62            }}
     63        >
     64            <div className={`zenpress-tabs zenpress-tabs--${orientation}`}>
     65                {children}
     66            </div>
     67        </TabsContext.Provider>
     68    );
    4469};
    4570
     
    4772 * TabList component - container for Tab components
    4873 *
    49  * @param {Object} props       - Component props.
     74 * @param {Object} props          - Component props.
    5075 * @param {Object} props.children - Tab components.
    5176 * @return {JSX.Element} The tab list container.
    5277 */
    5378export const TabList = ({ children }) => {
    54     const { orientation, tabListRef } = useContext(TabsContext);
    55 
    56     return (
    57         <div
    58             ref={tabListRef}
    59             className={`zenpress-tabs__list zenpress-tabs__list--${orientation}`}
    60             role="tablist"
    61             aria-orientation={orientation}
    62         >
    63             {children}
    64         </div>
    65     );
     79    const { orientation, tabListRef } = useContext(TabsContext);
     80
     81    return (
     82        <div
     83            ref={tabListRef}
     84            className={`zenpress-tabs__list zenpress-tabs__list--${orientation}`}
     85            role="tablist"
     86            aria-orientation={orientation}
     87        >
     88            {children}
     89        </div>
     90    );
    6691};
    6792
     
    6994 * Tab component - individual tab button
    7095 *
    71  * @param {Object}   props          - Component props.
    72  * @param {string}   props.tabId    - Unique identifier for the tab.
    73  * @param {string}   props.title    - Tab title (optional, uses children if not provided).
    74  * @param {string}   props.className - Additional CSS class name.
    75  * @param {Object}   props.children - Tab content.
     96 * @param {Object} props           - Component props.
     97 * @param {string} props.tabId     - Unique identifier for the tab.
     98 * @param {string} props.title     - Tab title (optional, uses children if not provided).
     99 * @param {string} props.className - Additional CSS class name.
     100 * @param {Object} props.children - Tab content.
    76101 * @return {JSX.Element} The tab button.
    77102 */
    78103export const Tab = ({ tabId, title, className = '', children }) => {
    79     const { selectedTabId, onSelect, orientation, getOrderedTabIds } = useContext(TabsContext);
    80     const isSelected = selectedTabId === tabId;
    81     const tabRef = useRef(null);
    82 
    83     // Handle keyboard navigation according to W3C ARIA pattern
    84     const handleKeyDown = (e) => {
    85         const tabIds = getOrderedTabIds();
    86         if (!tabIds || tabIds.length === 0) return;
    87 
    88         const currentIndex = tabIds.indexOf(tabId);
    89         if (currentIndex === -1) return;
    90 
    91         let targetIndex = currentIndex;
    92 
    93         // Handle arrow keys based on orientation
    94         if (orientation === 'vertical') {
    95             if (e.key === 'ArrowDown') {
    96                 e.preventDefault();
    97                 targetIndex = currentIndex < tabIds.length - 1 ? currentIndex + 1 : 0;
    98             } else if (e.key === 'ArrowUp') {
    99                 e.preventDefault();
    100                 targetIndex = currentIndex > 0 ? currentIndex - 1 : tabIds.length - 1;
    101             }
    102         } else {
    103             // Horizontal orientation
    104             if (e.key === 'ArrowRight') {
    105                 e.preventDefault();
    106                 targetIndex = currentIndex < tabIds.length - 1 ? currentIndex + 1 : 0;
    107             } else if (e.key === 'ArrowLeft') {
    108                 e.preventDefault();
    109                 targetIndex = currentIndex > 0 ? currentIndex - 1 : tabIds.length - 1;
    110             }
    111         }
    112 
    113         // Handle Home and End keys
    114         if (e.key === 'Home') {
    115             e.preventDefault();
    116             targetIndex = 0;
    117         } else if (e.key === 'End') {
    118             e.preventDefault();
    119             targetIndex = tabIds.length - 1;
    120         }
    121 
    122         // Handle Space and Enter for activation (if not auto-activated)
    123         if (e.key === ' ' || e.key === 'Enter') {
    124             e.preventDefault();
    125             onSelect(tabId);
    126             return;
    127         }
    128 
    129         // Move focus to target tab if index changed
    130         if (targetIndex !== currentIndex && targetIndex >= 0 && targetIndex < tabIds.length) {
    131             const targetTabId = tabIds[targetIndex];
    132             const targetTabElement = document.getElementById(`zenpress-tab-${targetTabId}`);
    133             if (targetTabElement) {
    134                 targetTabElement.focus();
    135                 // Auto-activate on focus (recommended by W3C for better UX)
    136                 onSelect(targetTabId);
    137             }
    138         }
    139     };
    140 
    141     // Auto-activate tab when it receives focus (recommended by W3C)
    142     const handleFocus = () => {
    143         if (!isSelected) {
    144             onSelect(tabId);
    145         }
    146     };
    147 
    148     return (
    149         <button
    150             ref={tabRef}
    151             className={`zenpress-tabs__tab ${isSelected ? 'zenpress-tabs__tab--is-active' : ''} ${className}`.trim()}
    152             role="tab"
    153             aria-selected={isSelected}
    154             aria-controls={`zenpress-tab-panel-${tabId}`}
    155             id={`zenpress-tab-${tabId}`}
    156             tabIndex={isSelected ? 0 : -1}
    157             onClick={() => onSelect(tabId)}
    158             onKeyDown={handleKeyDown}
    159             onFocus={handleFocus}
    160         >
    161             {title || children}
    162         </button>
    163     );
     104    const { selectedTabId, onSelect, orientation, getOrderedTabIds } =
     105        useContext(TabsContext);
     106    const isSelected = selectedTabId === tabId;
     107    const tabRef = useRef(null);
     108
     109    // Handle keyboard navigation according to W3C ARIA pattern
     110    const handleKeyDown = (e) => {
     111        const tabIds = getOrderedTabIds();
     112        if (!tabIds || tabIds.length === 0) {
     113            return;
     114        }
     115
     116        const currentIndex = tabIds.indexOf(tabId);
     117        if (currentIndex === -1) {
     118            return;
     119        }
     120
     121        let targetIndex = currentIndex;
     122
     123        // Handle arrow keys based on orientation
     124        if (orientation === 'vertical') {
     125            if (e.key === 'ArrowDown') {
     126                e.preventDefault();
     127                targetIndex =
     128                    currentIndex < tabIds.length - 1 ? currentIndex + 1 : 0;
     129            } else if (e.key === 'ArrowUp') {
     130                e.preventDefault();
     131                targetIndex =
     132                    currentIndex > 0 ? currentIndex - 1 : tabIds.length - 1;
     133            }
     134        } else if (e.key === 'ArrowRight') {
     135            // Horizontal orientation
     136            e.preventDefault();
     137            targetIndex =
     138                currentIndex < tabIds.length - 1 ? currentIndex + 1 : 0;
     139        } else if (e.key === 'ArrowLeft') {
     140            // Horizontal orientation
     141            e.preventDefault();
     142            targetIndex =
     143                currentIndex > 0 ? currentIndex - 1 : tabIds.length - 1;
     144        }
     145
     146        // Handle Home and End keys
     147        if (e.key === 'Home') {
     148            e.preventDefault();
     149            targetIndex = 0;
     150        } else if (e.key === 'End') {
     151            e.preventDefault();
     152            targetIndex = tabIds.length - 1;
     153        }
     154
     155        // Handle Space and Enter for activation (if not auto-activated)
     156        if (e.key === ' ' || e.key === 'Enter') {
     157            e.preventDefault();
     158            onSelect(tabId);
     159            return;
     160        }
     161
     162        // Move focus to target tab if index changed
     163        if (
     164            targetIndex !== currentIndex &&
     165            targetIndex >= 0 &&
     166            targetIndex < tabIds.length
     167        ) {
     168            const targetTabId = tabIds[targetIndex];
     169            const targetTabElement = document.getElementById(
     170                `zenpress-tab-${targetTabId}`
     171            );
     172            if (targetTabElement) {
     173                targetTabElement.focus();
     174                // Auto-activate on focus (recommended by W3C for better UX)
     175                onSelect(targetTabId);
     176            }
     177        }
     178    };
     179
     180    // Auto-activate tab when it receives focus (recommended by W3C)
     181    const handleFocus = () => {
     182        if (!isSelected) {
     183            onSelect(tabId);
     184        }
     185    };
     186
     187    return (
     188        <button
     189            ref={tabRef}
     190            className={`zenpress-tabs__tab ${isSelected ? 'zenpress-tabs__tab--is-active' : ''} ${className}`.trim()}
     191            role="tab"
     192            aria-selected={isSelected}
     193            aria-controls={`zenpress-tab-panel-${tabId}`}
     194            id={`zenpress-tab-${tabId}`}
     195            tabIndex={isSelected ? 0 : -1}
     196            onClick={() => onSelect(tabId)}
     197            onKeyDown={handleKeyDown}
     198            onFocus={handleFocus}
     199        >
     200            {title || children}
     201        </button>
     202    );
    164203};
    165204
     
    167206 * TabPanel component - container for tab content
    168207 *
    169  * @param {Object}   props          - Component props.
    170  * @param {string}   props.tabId    - Unique identifier matching a Tab's tabId.
    171  * @param {Object}   props.children - Panel content.
     208 * @param {Object} props          - Component props.
     209 * @param {string} props.tabId    - Unique identifier matching a Tab's tabId.
     210 * @param {Object} props.children - Panel content.
    172211 * @return {JSX.Element|null} The tab panel or null if not selected.
    173212 */
    174213export const TabPanel = ({ tabId, children }) => {
    175     const { selectedTabId } = useContext(TabsContext);
    176     const panelRef = useRef(null);
    177     const isSelected = selectedTabId === tabId;
    178 
    179     // Check if panel contains focusable elements
    180     useEffect(() => {
    181         if (isSelected && panelRef.current) {
    182             const focusableElements = panelRef.current.querySelectorAll(
    183                 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'
    184             );
    185 
    186             // If no focusable elements, make the panel itself focusable
    187             if (focusableElements.length === 0) {
    188                 panelRef.current.setAttribute('tabindex', '0');
    189             } else {
    190                 panelRef.current.removeAttribute('tabindex');
    191             }
    192         }
    193     }, [isSelected]);
    194 
    195     if (!isSelected) {
    196         return (
    197             <div
    198                 className="zenpress-tabs__panel"
    199                 role="tabpanel"
    200                 id={`zenpress-tab-panel-${tabId}`}
    201                 aria-labelledby={`zenpress-tab-${tabId}`}
    202                 hidden
    203             >
    204                 {children}
    205             </div>
    206         );
    207     }
    208 
    209     return (
    210         <div
    211             ref={panelRef}
    212             className="zenpress-tabs__panel"
    213             role="tabpanel"
    214             id={`zenpress-tab-panel-${tabId}`}
    215             aria-labelledby={`zenpress-tab-${tabId}`}
    216         >
    217             {children}
    218         </div>
    219     );
     214    const { selectedTabId } = useContext(TabsContext);
     215    const panelRef = useRef(null);
     216    const isSelected = selectedTabId === tabId;
     217
     218    // Check if panel contains focusable elements
     219    useEffect(() => {
     220        if (isSelected && panelRef.current) {
     221            const focusableElements = panelRef.current.querySelectorAll(
     222                'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'
     223            );
     224
     225            // If no focusable elements, make the panel itself focusable
     226            if (focusableElements.length === 0) {
     227                panelRef.current.setAttribute('tabindex', '0');
     228            } else {
     229                panelRef.current.removeAttribute('tabindex');
     230            }
     231        }
     232    }, [isSelected]);
     233
     234    if (!isSelected) {
     235        return (
     236            <div
     237                className="zenpress-tabs__panel"
     238                role="tabpanel"
     239                id={`zenpress-tab-panel-${tabId}`}
     240                aria-labelledby={`zenpress-tab-${tabId}`}
     241                hidden
     242            >
     243                {children}
     244            </div>
     245        );
     246    }
     247
     248    return (
     249        <div
     250            ref={panelRef}
     251            className="zenpress-tabs__panel"
     252            role="tabpanel"
     253            id={`zenpress-tab-panel-${tabId}`}
     254            aria-labelledby={`zenpress-tab-${tabId}`}
     255        >
     256            {children}
     257        </div>
     258    );
    220259};
    221260
  • zenpress/tags/2.2.0/assets/src/js/hooks/useSettings.js

    r3412245 r3448585  
    1515 */
    1616export const useSettings = () => {
    17     const [snippets, setSnippets] = useState({});
    18     const [isSaving, setIsSaving] = useState(false);
    19     const { createSuccessNotice, createErrorNotice } = useDispatch(noticesStore);
     17    const [snippets, setSnippets] = useState({});
     18    const [isSaving, setIsSaving] = useState(false);
     19    const { createSuccessNotice, createErrorNotice } =
     20        useDispatch(noticesStore);
    2021
    21     useEffect(() => {
    22         apiFetch({ path: '/wp/v2/settings' })
    23             .then((settings) => {
    24                 const active = Array.isArray(settings?.zenpress_active_snippets)
    25                     ? settings.zenpress_active_snippets
    26                     : [];
     22    useEffect(() => {
     23        apiFetch({ path: '/wp/v2/settings' })
     24            .then((settings) => {
     25                const active = Array.isArray(settings?.zenpress_active_snippets)
     26                    ? settings.zenpress_active_snippets
     27                    : [];
    2728
    28                 const meta = window?.zenpressSnippetsMeta || {};
    29                 const snippetsData = {};
     29                const meta = window?.zenpressSnippetsMeta || {};
     30                const snippetsData = {};
    3031
    31                 Object.keys(meta).forEach((name) => {
    32                     snippetsData[name] = {
    33                         ...meta[name],
    34                         'enable-snippet': active.includes(name),
    35                     };
    36                 });
     32                Object.keys(meta).forEach((name) => {
     33                    snippetsData[name] = {
     34                        ...meta[name],
     35                        'enable-snippet': active.includes(name),
     36                    };
     37                });
    3738
    38                 setSnippets(snippetsData);
    39             })
    40             .catch(() => {
    41                 createErrorNotice(__('Failed to load settings.', 'zenpress'));
    42             });
    43     }, [createErrorNotice]);
     39                setSnippets(snippetsData);
     40            })
     41            .catch(() => {
     42                createErrorNotice(__('Failed to load settings.', 'zenpress'));
     43            });
     44    }, [createErrorNotice]);
    4445
    45     const saveSettings = async () => {
    46         setIsSaving(true);
     46    const saveSettings = async () => {
     47        setIsSaving(true);
    4748
    48         const active = Object.keys(snippets).filter((name) => snippets[name]?.['enable-snippet']);
     49        const active = Object.keys(snippets).filter(
     50            (name) => snippets[name]?.['enable-snippet']
     51        );
    4952
    50         try {
    51             await apiFetch({
    52                 path: '/wp/v2/settings',
    53                 method: 'POST',
    54                 data: { zenpress_active_snippets: active },
    55             });
    56             createSuccessNotice(__('Settings saved.', 'zenpress'));
    57         } catch {
    58             createErrorNotice(__('Failed to save settings.', 'zenpress'));
    59         } finally {
    60             setIsSaving(false);
    61         }
    62     };
     53        try {
     54            await apiFetch({
     55                path: '/wp/v2/settings',
     56                method: 'POST',
     57                data: { zenpress_active_snippets: active },
     58            });
     59            createSuccessNotice(__('Settings saved.', 'zenpress'));
     60        } catch {
     61            createErrorNotice(__('Failed to save settings.', 'zenpress'));
     62        } finally {
     63            setIsSaving(false);
     64        }
     65    };
    6366
    64     return { snippets, setSnippets, saveSettings, isSaving };
     67    return { snippets, setSnippets, saveSettings, isSaving };
    6568};
  • zenpress/tags/2.2.0/assets/src/js/pages/SettingsPage.js

    r3412245 r3448585  
    1 import { useState, useEffect, useRef } from '@wordpress/element';
     1import { useState, useEffect } from '@wordpress/element';
    22import { __ } from '@wordpress/i18n';
    33import { Button } from '@wordpress/components';
     
    1414 */
    1515export const SettingsPage = () => {
    16     const { snippets, setSnippets, saveSettings, isSaving } = useSettings();
    17     const [selectedTabId, setSelectedTabId] = useState();
    18 
    19     const handleToggleChange = (snippetName) => {
    20         setSnippets((prev) => ({
    21             ...prev,
    22             [snippetName]: {
    23                 ...prev[snippetName],
    24                 'enable-snippet': !prev[snippetName]?.['enable-snippet'],
    25             },
    26         }));
    27     };
    28 
    29     const enableAllSnippets = () => {
    30         setSnippets((prev) => {
    31             const updated = {};
    32             Object.keys(prev).forEach((name) => {
    33                 updated[name] = { ...prev[name], 'enable-snippet': true };
    34             });
    35             return updated;
    36         });
    37     };
    38 
    39     const resetSettings = () => {
    40         setSnippets((prev) => {
    41             const updated = {};
    42             Object.keys(prev).forEach((name) => {
    43                 updated[name] = { ...prev[name], 'enable-snippet': false };
    44             });
    45             return updated;
    46         });
    47     };
    48 
    49     const enableByPreset = (preset) => {
    50         setSnippets((prev) => {
    51             const updated = {};
    52             Object.entries(prev).forEach(([name, data]) => {
    53                 const presets = Array.isArray(data?.preset) ? data.preset : [];
    54                 const isEnabled = presets.includes(preset);
    55                 updated[name] = { ...data, 'enable-snippet': isEnabled };
    56             });
    57             return updated;
    58         });
    59     };
    60 
    61     // Helper function to capitalize category name
    62     const capitalizeCategory = (category) => {
    63         if (!category) return category;
    64         return category.charAt(0).toUpperCase() + category.slice(1).toLowerCase();
    65     };
    66 
    67     // Category order: Core, Gutenberg, WooCommerce, ads-blocker, Tools
    68     // Using English lowercase values for comparison since categories are stored in lowercase
    69     const categoryOrder = ['core', 'gutenberg', 'woocommerce', 'ads-blocker', 'tools'];
    70 
    71     // Group snippets by category, then by subcategory
    72     const groupedSnippets = {};
    73     Object.keys(snippets).forEach((snippetName) => {
    74         const snippet = snippets[snippetName];
    75         const category = (snippet?.category || __('Uncategorized', 'zenpress')).toLowerCase();
    76         const subcategory = (snippet?.subcategory || __('uncategorized', 'zenpress')).toLowerCase();
    77 
    78         if (!groupedSnippets[category]) {
    79             groupedSnippets[category] = {};
    80         }
    81         if (!groupedSnippets[category][subcategory]) {
    82             groupedSnippets[category][subcategory] = [];
    83         }
    84         groupedSnippets[category][subcategory].push({ name: snippetName, data: snippet });
    85     });
    86 
    87     // Sort categories according to the specified order
    88     const sortedCategories = Object.keys(groupedSnippets).sort((a, b) => {
    89         const indexA = categoryOrder.indexOf(a.toLowerCase());
    90         const indexB = categoryOrder.indexOf(b.toLowerCase());
    91 
    92         // If both are in the order array, sort by their position
    93         if (indexA !== -1 && indexB !== -1) {
    94             return indexA - indexB;
    95         }
    96         // If only A is in the order array, A comes first
    97         if (indexA !== -1) return -1;
    98         // If only B is in the order array, B comes first
    99         if (indexB !== -1) return 1;
    100         // If neither is in the order array, sort alphabetically
    101         return a.localeCompare(b, undefined, { sensitivity: 'base' });
    102     });
    103 
    104     // Set initial selected tab if none is selected
    105     useEffect(() => {
    106         if (!selectedTabId && sortedCategories.length > 0) {
    107             setSelectedTabId(sortedCategories[0]);
    108         }
    109     }, [selectedTabId, sortedCategories.length]);
    110 
    111     const onSelect = (tabName) => {
    112         setSelectedTabId(tabName);
    113     };
    114 
    115     // Add keyboard shortcuts and ensure toggles are keyboard accessible
    116     useEffect(() => {
    117         const handleKeyDown = (e) => {
    118             // Ctrl+S or Cmd+S to save
    119             if ((e.ctrlKey || e.metaKey) && e.key === 's') {
    120                 e.preventDefault();
    121                 if (!isSaving) {
    122                     saveSettings();
    123                 }
    124             }
    125         };
    126 
    127         document.addEventListener('keydown', handleKeyDown);
    128         return () => {
    129             document.removeEventListener('keydown', handleKeyDown);
    130         };
    131     }, [saveSettings, isSaving]);
    132 
    133     return (
    134         <article className="zenpress-row">
    135             <section className="zenpress-main">
    136                 <div className="zenpress-notices">
    137                     <Notices />
    138                 </div>
    139                 <div className="zenpress-panel">
    140                     <Tabs
    141                         orientation="vertical"
    142                         selectedTabId={selectedTabId}
    143                         onSelect={(selectedId) => {
    144                             setSelectedTabId(selectedId);
    145                             onSelect(selectedId);
    146                         }}
    147                     >
    148                         <Tabs.TabList>
    149                             {sortedCategories.map((category) => {
    150                                 const categoryClass = `zenpress-tabs__tab--category-${category.toLowerCase().replace(/\s+/g, '-')}`;
    151                                 return (
    152                                     <Tabs.Tab
    153                                         key={category}
    154                                         tabId={category}
    155                                         title={capitalizeCategory(category)}
    156                                         className={categoryClass}
    157                                     >
    158                                         {capitalizeCategory(category)}
    159                                     </Tabs.Tab>
    160                                 );
    161                             })}
    162                         </Tabs.TabList>
    163                         {sortedCategories.map((category) => {
    164                             const subcategories = Object.keys(groupedSnippets[category]).sort();
    165                             return (
    166                                 <Tabs.TabPanel key={category} tabId={category}>
    167                                     <h2>{capitalizeCategory(category)}</h2>
    168                                     {subcategories.map((subcategory) => (
    169                                         <div
    170                                             key={subcategory}
    171                                             className={`zenpress-subcategory zenpress-subcategory-${subcategory.toLowerCase().replace(/\s+/g, '-')}`}
    172                                         >
    173                                             <hr />
    174                                             <h3>{capitalizeCategory(subcategory)}</h3>
    175                                             {groupedSnippets[category][subcategory].map(({ name, data }) => (
    176                                                 <SnippetToggleControl
    177                                                     key={name}
    178                                                     label={data.title || name}
    179                                                     value={data?.['enable-snippet'] || false}
    180                                                     onChange={() => handleToggleChange(name)}
    181                                                     help={data.description || ''}
    182                                                 />
    183                                             ))}
    184                                         </div>
    185                                     ))}
    186                                 </Tabs.TabPanel>
    187                             );
    188                         })}
    189                     </Tabs>
    190                     <div className="zenpress-actions">
    191                         <div className="zenpress-actions-bulk">
    192                             <Button variant="tertiary" onClick={enableAllSnippets} __next40pxDefaultSize>
    193                                 {__('Enable all actions', 'zenpress')}
    194                             </Button>
    195                             <Button isDestructive onClick={resetSettings} __next40pxDefaultSize>
    196                                 {__('Disable all actions', 'zenpress')}
    197                             </Button>
    198                         </div>
    199                         <SaveButton onClick={saveSettings} isBusy={isSaving} />
    200                     </div>
    201                 </div>
    202             </section>
    203             <aside className="zenpress-sidebar">
    204                 <div className="zenpress-presets">
    205                     <div className="zenpress-presets-description">
    206                         <h2>{__('Pick configuration preset', 'zenpress')}</h2>
    207                         <p>
    208                             {__(
    209                                 "Don't know which features to enable? Quickly configure ZenPress by selecting a preset that matches your site type. Each preset enables optimized features for your specific use case.",
    210                                 'zenpress'
    211                             )}
    212                         </p>
    213                         <hr />
    214                         <h3>🖼️ {__('Corporate website', 'zenpress')}</h3>
    215                         <p>
    216                             {__(
    217                                 'Optimized for business sites and portfolios. Focuses on security, performance, and removing unnecessary features like RSS feeds and author archives.',
    218                                 'zenpress'
    219                             )}
    220                         </p>
    221                         <Button
    222                             variant="secondary"
    223                             onClick={() => enableByPreset('corporate-website')}
    224                             __next40pxDefaultSize
    225                         >
    226                             {__('Enable', 'zenpress')}
    227                         </Button>
    228                         <hr />
    229                         <h3> 📰 {__('Blog', 'zenpress')}</h3>
    230                         <p>
    231                             {__(
    232                                 'Tailored for content-focused blogs. Includes performance and security optimizations while preserving essential blog features like RSS feeds.',
    233                                 'zenpress'
    234                             )}
    235                         </p>
    236                         <Button variant="secondary" onClick={() => enableByPreset('blog')} __next40pxDefaultSize>
    237                             {__('Enable', 'zenpress')}
    238                         </Button>
    239                         <hr />
    240                         <h3>🛒 {__('E-commerce', 'zenpress')}</h3>
    241                         <p>
    242                             {__(
    243                                 'Designed for WooCommerce stores. Includes all performance and security features plus WooCommerce-specific optimizations for faster checkout.',
    244                                 'zenpress'
    245                             )}
    246                         </p>
    247                         <Button variant="secondary" onClick={() => enableByPreset('ecommerce')} __next40pxDefaultSize>
    248                             {__('Enable', 'zenpress')}
    249                         </Button>
    250                     </div>
    251                 </div>
    252             </aside>
    253         </article>
    254     );
     16    const { snippets, setSnippets, saveSettings, isSaving } = useSettings();
     17    const [selectedTabId, setSelectedTabId] = useState();
     18
     19    const handleToggleChange = (snippetName) => {
     20        setSnippets((prev) => ({
     21            ...prev,
     22            [snippetName]: {
     23                ...prev[snippetName],
     24                'enable-snippet': !prev[snippetName]?.['enable-snippet'],
     25            },
     26        }));
     27    };
     28
     29    const enableAllSnippets = () => {
     30        setSnippets((prev) => {
     31            const updated = {};
     32            Object.keys(prev).forEach((name) => {
     33                updated[name] = { ...prev[name], 'enable-snippet': true };
     34            });
     35            return updated;
     36        });
     37    };
     38
     39    const resetSettings = () => {
     40        setSnippets((prev) => {
     41            const updated = {};
     42            Object.keys(prev).forEach((name) => {
     43                updated[name] = { ...prev[name], 'enable-snippet': false };
     44            });
     45            return updated;
     46        });
     47    };
     48
     49    const enableByPreset = (preset) => {
     50        setSnippets((prev) => {
     51            const updated = {};
     52            Object.entries(prev).forEach(([name, data]) => {
     53                const presets = Array.isArray(data?.preset) ? data.preset : [];
     54                const isEnabled = presets.includes(preset);
     55                updated[name] = { ...data, 'enable-snippet': isEnabled };
     56            });
     57            return updated;
     58        });
     59    };
     60
     61    // Helper function to capitalize category name
     62    const capitalizeCategory = (category) => {
     63        if (!category) {
     64            return category;
     65        }
     66        return (
     67            category.charAt(0).toUpperCase() + category.slice(1).toLowerCase()
     68        );
     69    };
     70
     71    // Category order: Core, Gutenberg, WooCommerce, ads-blocker, Tools
     72    // Using English lowercase values for comparison since categories are stored in lowercase
     73    const categoryOrder = [
     74        'core',
     75        'gutenberg',
     76        'woocommerce',
     77        'ads-blocker',
     78        'tools',
     79    ];
     80
     81    // Group snippets by category, then by subcategory
     82    const groupedSnippets = {};
     83    Object.keys(snippets).forEach((snippetName) => {
     84        const snippet = snippets[snippetName];
     85        const category = (
     86            snippet?.category || __('Uncategorized', 'zenpress')
     87        ).toLowerCase();
     88        const subcategory = (
     89            snippet?.subcategory || __('uncategorized', 'zenpress')
     90        ).toLowerCase();
     91
     92        if (!groupedSnippets[category]) {
     93            groupedSnippets[category] = {};
     94        }
     95        if (!groupedSnippets[category][subcategory]) {
     96            groupedSnippets[category][subcategory] = [];
     97        }
     98        groupedSnippets[category][subcategory].push({
     99            name: snippetName,
     100            data: snippet,
     101        });
     102    });
     103
     104    // Sort categories according to the specified order
     105    const sortedCategories = Object.keys(groupedSnippets).sort((a, b) => {
     106        const indexA = categoryOrder.indexOf(a.toLowerCase());
     107        const indexB = categoryOrder.indexOf(b.toLowerCase());
     108
     109        // If both are in the order array, sort by their position
     110        if (indexA !== -1 && indexB !== -1) {
     111            return indexA - indexB;
     112        }
     113        // If only A is in the order array, A comes first
     114        if (indexA !== -1) {
     115            return -1;
     116        }
     117        // If only B is in the order array, B comes first
     118        if (indexB !== -1) {
     119            return 1;
     120        }
     121        // If neither is in the order array, sort alphabetically
     122        return a.localeCompare(b, undefined, { sensitivity: 'base' });
     123    });
     124
     125    // Set initial selected tab if none is selected
     126    useEffect(() => {
     127        if (!selectedTabId && sortedCategories.length > 0) {
     128            setSelectedTabId(sortedCategories[0]);
     129        }
     130    }, [selectedTabId, sortedCategories]);
     131
     132    const onSelect = (tabName) => {
     133        setSelectedTabId(tabName);
     134    };
     135
     136    // Add keyboard shortcuts and ensure toggles are keyboard accessible
     137    useEffect(() => {
     138        const handleKeyDown = (e) => {
     139            // Ctrl+S or Cmd+S to save
     140            if ((e.ctrlKey || e.metaKey) && e.key === 's') {
     141                e.preventDefault();
     142                if (!isSaving) {
     143                    saveSettings();
     144                }
     145            }
     146        };
     147
     148        document.addEventListener('keydown', handleKeyDown);
     149        return () => {
     150            document.removeEventListener('keydown', handleKeyDown);
     151        };
     152    }, [saveSettings, isSaving]);
     153
     154    return (
     155        <article className="zenpress-row">
     156            <section className="zenpress-main">
     157                <div className="zenpress-notices">
     158                    <Notices />
     159                </div>
     160                <div className="zenpress-panel">
     161                    <Tabs
     162                        orientation="vertical"
     163                        selectedTabId={selectedTabId}
     164                        onSelect={(selectedId) => {
     165                            setSelectedTabId(selectedId);
     166                            onSelect(selectedId);
     167                        }}
     168                    >
     169                        <Tabs.TabList>
     170                            {sortedCategories.map((category) => {
     171                                const categoryClass = `zenpress-tabs__tab--category-${category.toLowerCase().replace(/\s+/g, '-')}`;
     172                                return (
     173                                    <Tabs.Tab
     174                                        key={category}
     175                                        tabId={category}
     176                                        title={capitalizeCategory(category)}
     177                                        className={categoryClass}
     178                                    >
     179                                        {capitalizeCategory(category)}
     180                                    </Tabs.Tab>
     181                                );
     182                            })}
     183                        </Tabs.TabList>
     184                        {sortedCategories.map((category) => {
     185                            const subcategories = Object.keys(
     186                                groupedSnippets[category]
     187                            ).sort();
     188                            return (
     189                                <Tabs.TabPanel key={category} tabId={category}>
     190                                    <h2>{capitalizeCategory(category)}</h2>
     191                                    {subcategories.map((subcategory) => (
     192                                        <div
     193                                            key={subcategory}
     194                                            className={`zenpress-subcategory zenpress-subcategory-${subcategory.toLowerCase().replace(/\s+/g, '-')}`}
     195                                        >
     196                                            <hr />
     197                                            <h3>
     198                                                {capitalizeCategory(
     199                                                    subcategory
     200                                                )}
     201                                            </h3>
     202                                            {groupedSnippets[category][
     203                                                subcategory
     204                                            ].map(({ name, data }) => (
     205                                                <SnippetToggleControl
     206                                                    key={name}
     207                                                    label={data.title || name}
     208                                                    value={
     209                                                        data?.[
     210                                                            'enable-snippet'
     211                                                        ] || false
     212                                                    }
     213                                                    onChange={() =>
     214                                                        handleToggleChange(name)
     215                                                    }
     216                                                    help={
     217                                                        data.description || ''
     218                                                    }
     219                                                />
     220                                            ))}
     221                                        </div>
     222                                    ))}
     223                                </Tabs.TabPanel>
     224                            );
     225                        })}
     226                    </Tabs>
     227                    <div className="zenpress-actions">
     228                        <div className="zenpress-actions-bulk">
     229                            <Button
     230                                variant="tertiary"
     231                                onClick={enableAllSnippets}
     232                                __next40pxDefaultSize
     233                            >
     234                                {__('Enable all actions', 'zenpress')}
     235                            </Button>
     236                            <Button
     237                                isDestructive
     238                                onClick={resetSettings}
     239                                __next40pxDefaultSize
     240                            >
     241                                {__('Disable all actions', 'zenpress')}
     242                            </Button>
     243                        </div>
     244                        <SaveButton onClick={saveSettings} isBusy={isSaving} />
     245                    </div>
     246                </div>
     247            </section>
     248            <aside className="zenpress-sidebar">
     249                <div className="zenpress-presets">
     250                    <div className="zenpress-presets-description">
     251                        <h2>{__('Pick configuration preset', 'zenpress')}</h2>
     252                        <p>
     253                            {__(
     254                                "Don't know which features to enable? Quickly configure ZenPress by selecting a preset that matches your site type. Each preset enables optimized features for your specific use case.",
     255                                'zenpress'
     256                            )}
     257                        </p>
     258                        <hr />
     259                        <h3>🖼️ {__('Corporate website', 'zenpress')}</h3>
     260                        <p>
     261                            {__(
     262                                'Optimized for business sites and portfolios. Focuses on security, performance, and removing unnecessary features like RSS feeds and author archives.',
     263                                'zenpress'
     264                            )}
     265                        </p>
     266                        <Button
     267                            variant="secondary"
     268                            onClick={() => enableByPreset('corporate-website')}
     269                            __next40pxDefaultSize
     270                        >
     271                            {__('Enable', 'zenpress')}
     272                        </Button>
     273                        <hr />
     274                        <h3> 📰 {__('Blog', 'zenpress')}</h3>
     275                        <p>
     276                            {__(
     277                                'Tailored for content-focused blogs. Includes performance and security optimizations while preserving essential blog features like RSS feeds.',
     278                                'zenpress'
     279                            )}
     280                        </p>
     281                        <Button
     282                            variant="secondary"
     283                            onClick={() => enableByPreset('blog')}
     284                            __next40pxDefaultSize
     285                        >
     286                            {__('Enable', 'zenpress')}
     287                        </Button>
     288                        <hr />
     289                        <h3>🛒 {__('E-commerce', 'zenpress')}</h3>
     290                        <p>
     291                            {__(
     292                                'Designed for WooCommerce stores. Includes all performance and security features plus WooCommerce-specific optimizations for faster checkout.',
     293                                'zenpress'
     294                            )}
     295                        </p>
     296                        <Button
     297                            variant="secondary"
     298                            onClick={() => enableByPreset('ecommerce')}
     299                            __next40pxDefaultSize
     300                        >
     301                            {__('Enable', 'zenpress')}
     302                        </Button>
     303                    </div>
     304                </div>
     305            </aside>
     306        </article>
     307    );
    255308};
  • zenpress/tags/2.2.0/inc/admin/enqueue.php

    r3372200 r3448585  
    2323
    2424    $asset = include $asset_file;
     25    if (!is_array($asset) || empty($asset['dependencies']) || empty($asset['version'])) {
     26        return;
     27    }
     28
    2529    wp_enqueue_script(
    2630        'zenpress-scripts',
    2731        plugins_url('assets/build/index.js', ZENPRESS_PLUGIN_FILE),
    28         $asset['dependencies'],
     32        (array) $asset['dependencies'],
    2933        $asset['version'],
    3034        true
     
    3539        plugins_url('assets/build/index.css', ZENPRESS_PLUGIN_FILE),
    3640        array_filter(
    37             $asset['dependencies'],
    38             function ($style) {
     41            (array) $asset['dependencies'],
     42            static function (string $style): bool {
    3943                return wp_style_is($style, 'registered');
    4044            }
     
    6266    }
    6367
    64     foreach (glob($snippets_path . '*.php') as $file) {
     68    $files = glob($snippets_path . '*.php') ?: [];
     69    foreach ($files as $file) {
    6570        $basename = basename($file, '.php');
    6671        $snippets[$basename] = zenpress_extract_snippet_metadata($basename);
  • zenpress/tags/2.2.0/inc/core/constants.php

    r3372200 r3448585  
    1010
    1111if (!defined('ZENPRESS_PLUGIN_FILE')) {
    12     define(
    13         'ZENPRESS_PLUGIN_FILE',
    14         __FILE__ === realpath(__FILE__)
    15         ? __FILE__ // fallback
    16         : dirname(__DIR__, 1) . '/zenpress.php'
    17     );
     12    $plugin_file = dirname(__DIR__, 2) . '/zenpress.php';
     13    define('ZENPRESS_PLUGIN_FILE', is_file($plugin_file) ? $plugin_file : __FILE__);
    1814}
    1915
  • zenpress/tags/2.2.0/inc/core/metadata.php

    r3412245 r3448585  
    1818        'subcategory' => '',
    1919        'weight' => 0,
    20         'preset' => []
     20        'preset' => [],
    2121    ];
    2222
     
    3131        'subcategory' => sanitize_text_field($metadata['subcategory']),
    3232        'weight' => (int) $metadata['weight'],
    33         'preset' => array_map('sanitize_text_field', (array) $metadata['preset'])
     33        'preset' => array_map('sanitize_text_field', (array) $metadata['preset']),
    3434    ];
    3535}
  • zenpress/tags/2.2.0/inc/core/sanitize.php

    r3372200 r3448585  
    1111 * @return array<string> Sanitized base names.
    1212 */
    13 function zenpress_sanitize_snippets_option($value): array {
     13function zenpress_sanitize_snippets_option(mixed $value): array {
    1414    return array_values(
    1515        array_filter(
  • zenpress/tags/2.2.0/inc/settings/loader.php

    r3372200 r3448585  
    1212 */
    1313function zenpress_load_snippets(string $folder = 'inc/snippets/functions/'): array {
     14    // Prevent path traversal if $folder is ever passed from external input.
     15    if (str_contains($folder, '..')) {
     16        return [];
     17    }
     18
    1419    $path = ZENPRESS_PLUGIN_DIR . rtrim($folder, '/') . '/';
    1520
     
    3641 *  BOOT ZENPRESS PLUGIN
    3742 **************************************/
    38 add_action('init', function() : void {
     43add_action('init', static function (): void {
    3944    zenpress_load_snippets();
    4045});
  • zenpress/tags/2.2.0/inc/settings/options.php

    r3372200 r3448585  
    2020                'schema' => [
    2121                    'type' => 'array',
    22                     'items' => ['type' => 'string']
    23                 ]
    24             ]
     22                    'items' => ['type' => 'string'],
     23                ],
     24            ],
    2525        ]
    2626    );
  • zenpress/tags/2.2.0/inc/snippets/functions/block-user-enumeration.php

    r3372200 r3448585  
    77if (!is_admin()) {
    88    // Block enumeration via query string (?author=1).
    9     if (
    10         isset($_SERVER['QUERY_STRING']) &&
    11         preg_match('/author=([0-9]+)/i', sanitize_text_field(wp_unslash($_SERVER['QUERY_STRING'])))
    12     ) {
     9    $query_string = $_SERVER['QUERY_STRING'] ?? '';
     10    if (preg_match('/author=([0-9]+)/i', sanitize_text_field(wp_unslash($query_string)))) {
    1311        wp_die(esc_html__('Access denied.', 'zenpress'), '', ['response' => 403]);
    1412    }
     
    1715    add_filter(
    1816        'redirect_canonical',
    19         static function ($redirect, $request) {
     17        static function (string|false $redirect, string $request): string|false {
    2018            if (preg_match('/\?author=([0-9]+)(\/*)/i', sanitize_text_field(wp_unslash($request)))) {
    2119                wp_die(esc_html__('Access denied.', 'zenpress'), '', ['response' => 403]);
  • zenpress/tags/2.2.0/inc/snippets/functions/clean-admin-bar.php

    r3372200 r3448585  
    77add_action(
    88    'admin_bar_menu',
    9     static function ($wp_admin_bar) {
    10         if (!($wp_admin_bar instanceof WP_Admin_Bar)) {
    11             return;
    12         }
    13 
     9    static function (WP_Admin_Bar $wp_admin_bar): void {
    1410        /**
    1511         * Backend clean-up.
  • zenpress/tags/2.2.0/inc/snippets/functions/clean-dashboard-items.php

    r3372200 r3448585  
    77add_action(
    88    'wp_dashboard_setup',
    9     static function () {
     9    static function (): void {
    1010        /**
    1111         * Core widgets.
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-author-archives.php

    r3372200 r3448585  
    88remove_filter('template_redirect', 'redirect_canonical');
    99
    10 add_action('template_redirect', static function () {
     10add_action('template_redirect', static function (): void {
    1111    if (is_author()) {
    1212        global $wp_query;
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-dashicons.php

    r3372200 r3448585  
    55}
    66
    7 add_action('wp_enqueue_scripts', static function () {
     7add_action('wp_enqueue_scripts', static function (): void {
    88    if (!is_user_logged_in()) {
    99        wp_dequeue_style('dashicons');
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-default-pattern-categories.php

    r3412245 r3448585  
    1111 * simplifying the interface and reducing clutter.
    1212 */
    13 add_action('init', static function() {
     13add_action('init', static function (): void {
    1414    if (!class_exists('WP_Block_Pattern_Categories_Registry')) {
    1515        return;
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-emoji-scripts.php

    r3372200 r3448585  
    2222
    2323// Remove emoji support from TinyMCE editor.
    24 add_filter('tiny_mce_plugins', static function ($plugins) {
     24add_filter('tiny_mce_plugins', static function (array $plugins): array {
    2525    return array_diff($plugins, ['wpemoji']);
    2626});
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-jquery-migrate.php

    r3372200 r3448585  
    55}
    66
    7 add_action('wp_default_scripts', static function (&$scripts) {
     7add_action('wp_default_scripts', static function (WP_Scripts $scripts): void {
    88    if (!is_admin() && isset($scripts->registered['jquery'])) {
    99        $script = $scripts->registered['jquery'];
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-oembed.php

    r3372200 r3448585  
    55}
    66
    7 add_action('init', static function () {
     7add_action('init', static function (): void {
    88    // Remove oEmbed from query vars.
    99    global $wp;
     
    2525
    2626    // Remove the wpembed TinyMCE plugin.
    27     add_filter('tiny_mce_plugins', function ($plugins) {
     27    add_filter('tiny_mce_plugins', static function (array $plugins): array {
    2828        return array_diff($plugins, ['wpembed']);
    2929    });
    3030
    3131    // Remove embed-related rewrite rules.
    32     add_filter('rewrite_rules_array', function ($rules) {
     32    add_filter('rewrite_rules_array', static function (array $rules): array {
    3333        foreach ($rules as $rule => $rewrite) {
    34             if (strpos($rewrite, 'embed=true') !== false) {
     34            if (str_contains($rewrite, 'embed=true')) {
    3535                unset($rules[$rule]);
    3636            }
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-pdf-thumbnails.php

    r3372200 r3448585  
    55}
    66
    7 add_filter('fallback_intermediate_image_sizes', static function () {
     7add_filter('fallback_intermediate_image_sizes', static function (): array {
    88    return [];
    99});
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-pingback-trackback.php

    r3372200 r3448585  
    66
    77// Remove X-Pingback header from HTTP responses.
    8 add_filter('wp_headers', static function ($headers) {
     8add_filter('wp_headers', static function (array $headers): array {
    99    unset($headers['X-Pingback']);
    1010
     
    1313
    1414// Disable pingbacks and trackbacks for new posts.
    15 add_action('after_setup_theme', static function () {
     15add_action('after_setup_theme', static function (): void {
    1616    update_option('default_ping_status', 'closed');
    1717    update_option('default_pingback_flag', 0);
     
    1919
    2020// Prevent self-pingbacks.
    21 add_action('pre_ping', static function (&$links) {
     21add_action('pre_ping', static function (array &$links): void {
    2222    $home = get_option('home');
    2323    foreach ($links as $l => $link) {
    24         if (strpos($link, $home) === 0) {
     24        if (str_starts_with($link, $home)) {
    2525            unset($links[$l]);
    2626        }
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-rest-api.php

    r3412245 r3448585  
    1414remove_action('xmlrpc_rsd_apis', 'rest_output_rsd');
    1515
    16 // Disable REST API
    17 if (version_compare(get_bloginfo('version'), '4.7', '>=')) {
    18     add_filter('rest_authentication_errors', 'zenpress_disable_wp_rest_api');
    19 } else {
    20     zenpress_disable_wp_rest_api_legacy();
    21 }
    22 
    23 function zenpress_disable_wp_rest_api($access) {
    24     if (!is_user_logged_in() && !zenpress_disable_wp_rest_api_allow_access()) {
    25         $message = apply_filters('zenpress_disable_wp_rest_api_error', __('REST API restricted to authenticated users.', 'zenpress'));
    26 
    27         return new WP_Error('rest_login_required', $message, ['status' => rest_authorization_required_code()]);
    28     }
    29 
    30     return $access;
    31 }
    32 
    33 function zenpress_disable_wp_rest_api_allow_access() {
     16/**
     17 * Check whether to allow unauthenticated REST API access (bypass).
     18 *
     19 * Filters zenpress_disable_wp_rest_api_post_var and zenpress_disable_wp_rest_api_server_var
     20 * let site owners allow specific POST keys or REQUEST_URI values for webhooks/third-party
     21 * integrations. Security note: only use values that are secret or not guessable (e.g. a
     22 * random token in POST, or a unique path). Using predictable values can re-enable public
     23 * REST API access and weaken the protection provided by the "Disable REST API" snippet.
     24 */
     25$zenpress_disable_wp_rest_api_allow_access = static function (): bool {
    3426    $post_var = apply_filters('zenpress_disable_wp_rest_api_post_var', false);
    3527    $server_var = apply_filters('zenpress_disable_wp_rest_api_server_var', false);
    3628
    3729    if (!empty($post_var)) {
    38         if (is_array($post_var)) {
    39             foreach($post_var as $var) {
    40                 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Intentional: Allows bypass via specific POST vars for webhooks/third-party integrations
    41                 if (isset($_POST[$var]) && !empty($_POST[$var])) {
    42                     return true;
    43                 }
    44             }
    45         } else {
     30        $post_vars = is_array($post_var) ? $post_var : [$post_var];
     31        foreach ($post_vars as $var) {
    4632            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Intentional: Allows bypass via specific POST vars for webhooks/third-party integrations
    47             if (isset($_POST[$post_var]) && !empty($_POST[$post_var])) {
     33            if (!empty($_POST[$var] ?? null)) {
    4834                return true;
    4935            }
     
    5238
    5339    if (!empty($server_var)) {
    54         if (is_array($server_var)) {
    55             foreach($server_var as $var) {
    56                 if (isset($_SERVER['REQUEST_URI']) && $_SERVER['REQUEST_URI'] === $var) {
    57                     return true;
    58                 }
    59             }
    60         } else {
    61             if (isset($_SERVER['REQUEST_URI']) && $_SERVER['REQUEST_URI'] === $server_var) {
     40        $server_vars = is_array($server_var) ? $server_var : [$server_var];
     41        $request_uri = $_SERVER['REQUEST_URI'] ?? '';
     42        foreach ($server_vars as $var) {
     43            if ($request_uri === $var) {
    6244                return true;
    6345            }
     
    6648
    6749    return false;
    68 }
     50};
    6951
    70 function zenpress_disable_wp_rest_api_legacy() {
     52// Disable REST API
     53if (version_compare(get_bloginfo('version'), '4.7', '>=')) {
     54    add_filter('rest_authentication_errors', static function (WP_Error|bool|null $access) use ($zenpress_disable_wp_rest_api_allow_access): WP_Error|bool|null {
     55        if (!is_user_logged_in() && !$zenpress_disable_wp_rest_api_allow_access()) {
     56            $message = apply_filters('zenpress_disable_wp_rest_api_error', __('REST API restricted to authenticated users.', 'zenpress'));
     57
     58            return new WP_Error('rest_login_required', $message, ['status' => rest_authorization_required_code()]);
     59        }
     60
     61        return $access;
     62    });
     63} else {
    7164    // REST API 1.x
    7265    add_filter('json_enabled', '__return_false');
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-rss.php

    r3372200 r3448585  
    22
    33if (!defined('ABSPATH')) {
     4    exit;
     5}
     6
     7// Redirect all feed requests to homepage.
     8function zenpress_disable_all_feeds(): void {
     9    wp_safe_redirect(home_url(), 301);
    410    exit;
    511}
     
    1723remove_action('wp_head', 'feed_links_extra', 3);
    1824remove_action('wp_head', 'feed_links', 2);
    19 
    20 // Redirect all feed requests to homepage.
    21 function zenpress_disable_all_feeds() {
    22     wp_safe_redirect(home_url(), 301);
    23     exit;
    24 }
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-woocommerce-cart-fragments.php

    r3372200 r3448585  
    66
    77if (class_exists('WooCommerce')) {
    8     add_action('wp_enqueue_scripts', static function () {
     8    add_action('wp_enqueue_scripts', static function (): void {
    99        wp_dequeue_script('wc-cart-fragments');
    1010    }, 11);
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-woocommerce-scripts-styles.php

    r3372200 r3448585  
    66
    77if (class_exists('WooCommerce')) {
    8     add_action('wp_enqueue_scripts', static function () {
     8    add_action('wp_enqueue_scripts', static function (): void {
    99        if (!is_woocommerce() && !is_cart() && !is_checkout() && !is_account_page() && !is_product() && !is_product_category() && !is_shop()) {
    1010            // Dequeue WooCommerce Styles
  • zenpress/tags/2.2.0/inc/snippets/functions/disable-woocommerce-widgets.php

    r3372200 r3448585  
    66
    77if (class_exists('WooCommerce')) {
    8     add_action('widgets_init', static function() {
     8    add_action('widgets_init', static function (): void {
    99        unregister_widget('WC_Widget_Products');
    1010        unregister_widget('WC_Widget_Product_Categories');
  • zenpress/tags/2.2.0/inc/snippets/functions/hide-woocommerce-version.php

    r3372200 r3448585  
    77if (class_exists('WooCommerce') && !is_admin()) {
    88    // Remove WooCommerce version from HTTP headers
    9     add_filter('wp_headers', static function ($headers) {
    10         if (isset($headers['X-WooCommerce-Version'])) {
    11             unset($headers['X-WooCommerce-Version']);
    12         }
     9    add_filter('wp_headers', static function (array $headers): array {
     10        unset($headers['X-WooCommerce-Version']);
    1311
    1412        return $headers;
     
    1614
    1715    // Remove WooCommerce version from style URLs
    18     add_filter('style_loader_src', static function ($src) {
    19         if (strpos($src, 'ver=') !== false && strpos($src, 'woocommerce') !== false) {
     16    add_filter('style_loader_src', static function (string $src): string {
     17        if (str_contains($src, 'ver=') && str_contains($src, 'woocommerce')) {
    2018            $src = remove_query_arg('ver', $src);
    2119        }
     
    2523
    2624    // Remove WooCommerce version from script URLs
    27     add_filter('script_loader_src', static function ($src) {
    28         if (strpos($src, 'ver=') !== false && strpos($src, 'woocommerce') !== false) {
     25    add_filter('script_loader_src', static function (string $src): string {
     26        if (str_contains($src, 'ver=') && str_contains($src, 'woocommerce')) {
    2927            $src = remove_query_arg('ver', $src);
    3028        }
  • zenpress/tags/2.2.0/inc/snippets/functions/hide-wordpress-version.php

    r3372200 r3448585  
    99
    1010// Remove WordPress version from generator
    11 add_filter('the_generator', static function () {
     11add_filter('the_generator', static function (): string {
    1212    return '';
    1313});
    1414
    1515// Remove WordPress version from script URLs
    16 add_filter('script_loader_src', static function ($src) {
    17     if (strpos($src, 'ver=' . get_bloginfo('version')) !== false) {
     16add_filter('script_loader_src', static function (string $src): string {
     17    if (str_contains($src, 'ver=' . get_bloginfo('version'))) {
    1818        $src = remove_query_arg('ver', $src);
    1919    }
     
    2323
    2424// Remove WordPress version from style URLs
    25 add_filter('style_loader_src', static function ($src) {
    26     if (strpos($src, 'ver=' . get_bloginfo('version')) !== false) {
     25add_filter('style_loader_src', static function (string $src): string {
     26    if (str_contains($src, 'ver=' . get_bloginfo('version'))) {
    2727        $src = remove_query_arg('ver', $src);
    2828    }
  • zenpress/tags/2.2.0/inc/snippets/functions/protect-wp-login.php

    r3372200 r3448585  
    66
    77// Remove detailed login errors
    8 add_filter('login_errors', static function () {
     8add_filter('login_errors', static function (): string {
    99    return __('Login error.', 'zenpress');
    1010});
     
    1515    $BLOCK_DURATION = 300; // 5 minutes
    1616
    17     $ipAddress = isset($_SERVER['REMOTE_ADDR'])
    18         ? filter_var(wp_unslash($_SERVER['REMOTE_ADDR']), FILTER_VALIDATE_IP)
    19         : 'unknown';
     17    $ipAddress = filter_var(
     18        wp_unslash($_SERVER['REMOTE_ADDR'] ?? ''),
     19        FILTER_VALIDATE_IP
     20    ) ?: 'unknown';
    2021
    2122    $blockKey = 'zenpress_login_block_' . $ipAddress;
     
    3435        wp_die(
    3536            esc_html__('Too many failed login attempts. Try again later.', 'zenpress'),
    36             403
     37            '',
     38            ['response' => 403]
    3739        );
    3840    }
     
    4749        wp_die(
    4850            esc_html__('Too many failed login attempts. Try again later.', 'zenpress'),
    49             403
     51            '',
     52            ['response' => 403]
    5053        );
    5154    }
  • zenpress/tags/2.2.0/inc/snippets/functions/remove-gutenberg-unwanted-block-patterns.php

    r3372200 r3448585  
    99
    1010// Remove core block patterns
    11 add_action('after_setup_theme', static function () {
     11add_action('after_setup_theme', static function (): void {
    1212    remove_theme_support('core-block-patterns');
    1313});
  • zenpress/tags/2.2.0/inc/snippets/functions/remove-woocommerce-patterns.php

    r3412245 r3448585  
    3737});
    3838
    39 add_action('init', static function() {
     39add_action('init', static function (): void {
    4040    if (!class_exists('WP_Block_Patterns_Registry')) {
    4141        return;
     
    4444    $all_patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered();
    4545    foreach ($all_patterns as $pattern) {
    46         if (isset($pattern['name'])) {
    47             // Use str_starts_with() for PHP 8.0+, fallback to strpos() for older versions
    48             $is_woocommerce_pattern = function_exists('str_starts_with')
    49                 ? str_starts_with($pattern['name'], 'woocommerce-blocks')
    50                 : strpos($pattern['name'], 'woocommerce-blocks') === 0;
    51 
    52             if ($is_woocommerce_pattern) {
    53                 unregister_block_pattern($pattern['name']);
    54             }
     46        if (isset($pattern['name']) && str_starts_with($pattern['name'], 'woocommerce-blocks')) {
     47            unregister_block_pattern($pattern['name']);
    5548        }
    5649    }
     
    6356 * sometimes be tied to large transients/caching issues.
    6457 */
    65 add_filter('woocommerce_admin_features', static function($features) {
    66     // Ensure $features is an array
    67     if (!is_array($features)) {
    68         return $features;
    69     }
    70 
     58add_filter('woocommerce_admin_features', static function (array $features): array {
    7159    $feature_to_disable = 'pattern-toolkit-full-composability';
    7260
  • zenpress/tags/2.2.0/inc/snippets/meta/clean-dashboard-items.meta.php

    r3412245 r3448585  
    1313    'title' => __('Clean up the WordPress Dashboard', 'zenpress'),
    1414    'description' => __(
    15         'Removes unnecessary widgets and ads widgets from the dashboard. Declutters the admin area and improves usability.',
     15        'Removes unnecessary and ad widgets from the dashboard. Declutters the admin area and improves usability.',
    1616        'zenpress'
    1717    ),
  • zenpress/tags/2.2.0/inc/snippets/meta/disable-application-passwords.meta.php

    r3412245 r3448585  
    1313    'title' => __('Disable application passwords', 'zenpress'),
    1414    'description' => __(
    15         'Disables WordPress application passwords for all users, improving security. Only disable if API access is not needed.',
     15        'Disables WordPress application passwords for all users. Improves security; do not enable if you need API or app-based authentication (e.g. mobile apps, REST clients).',
    1616        'zenpress'
    1717    ),
     
    1919    'subcategory' => __('security', 'zenpress'),
    2020    'weight' => 0,
    21     'preset' => [''],
     21    'preset' => [],
    2222];
    2323
  • zenpress/tags/2.2.0/inc/snippets/meta/disable-dashicons.meta.php

    r3412245 r3448585  
    1616        'zenpress'
    1717    ),
    18     'category' => __(' core', 'zenpress'),
     18    'category' => __('core', 'zenpress'),
    1919    'subcategory' => __('performance', 'zenpress'),
    2020    'weight' => 0,
  • zenpress/tags/2.2.0/inc/snippets/meta/disable-default-pattern-categories.meta.php

    r3412245 r3448585  
    1212return [
    1313    'title' => __('Disable default pattern categories in site editor', 'zenpress'),
    14     'description' => __('Removes default pattern categories from the block pattern inserter in the site editor. Disables default categories: featured, about, audio, banner, buttons, call-to-action, columns, contact, footer, gallery, header, media, portfolio, posts, query, services, team, testimonials, text, videos, and any custom categories. This simplifies the interface by hiding category navigation while patterns remain accessible.', 'zenpress'),
     14    'description' => __('Removes default pattern categories from the block pattern inserter in the site editor (e.g. featured, about, audio, banner, buttons, call-to-action, columns, contact, footer, gallery, header, media, portfolio, posts, query, services, team, testimonials, text, videos) and any custom ones. Simplifies the interface; patterns remain accessible.', 'zenpress'),
    1515    'category' => __('gutenberg', 'zenpress'),
    1616    'subcategory' => __('user-interface', 'zenpress'),
  • zenpress/tags/2.2.0/inc/snippets/meta/disable-dns-prefetch.meta.php

    r3412245 r3448585  
    1313    'title' => __('Disable DNS prefetch', 'zenpress'),
    1414    'description' => __(
    15         'Removes DNS prefetch resource hints from wp_head avoids unnecessary DNS lookups and slightly improve performance on some sites.',
     15        'Removes DNS prefetch resource hints from wp_head. Avoids unnecessary DNS lookups and slightly improves performance on some sites.',
    1616        'zenpress'
    1717    ),
  • zenpress/tags/2.2.0/inc/snippets/meta/disable-emoji-scripts.meta.php

    r3412245 r3448585  
    1616        'zenpress'
    1717    ),
    18     'category' => __(' core', 'zenpress'),
     18    'category' => __('core', 'zenpress'),
    1919    'subcategory' => __('performance', 'zenpress'),
    2020    'weight' => 0,
  • zenpress/tags/2.2.0/inc/snippets/meta/disable-jquery-migrate.meta.php

    r3412245 r3448585  
    1616        'zenpress'
    1717    ),
    18     'category' => __(' core', 'zenpress'),
     18    'category' => __('core', 'zenpress'),
    1919    'subcategory' => __('performance', 'zenpress'),
    2020    'weight' => 0,
  • zenpress/tags/2.2.0/inc/snippets/meta/disable-pdf-thumbnails.meta.php

    r3412245 r3448585  
    1212return [
    1313    'title' => __('Disable PDF thumbnails', 'zenpress'),
    14     'description' => __('Prevents WordPress from generating thumbnails for uploaded PDF files by removing fallback image sizes. saves storage space and improves performance by avoiding unnecessary image generation.', 'zenpress'),
    15     'category' => __(' core', 'zenpress'),
     14    'description' => __('Prevents WordPress from generating thumbnails for uploaded PDF files by removing fallback image sizes. Saves storage space and improves performance by avoiding unnecessary image generation.', 'zenpress'),
     15    'category' => __('core', 'zenpress'),
    1616    'subcategory' => __('performance', 'zenpress'),
    1717    'weight' => 0,
  • zenpress/tags/2.2.0/inc/snippets/meta/disable-pingback-trackback.meta.php

    r3412245 r3448585  
    1212return [
    1313    'title' => __('Disable pingback and trackback', 'zenpress'),
    14     'description' => __('Removes the X-Pingback header, disables pingbacks and trackbacks on new posts, and prevents self-pingbacks. reduces spam, blocks potential DDoS vectors, and slightly improves performance by avoiding useless requests.', 'zenpress'),
     14    'description' => __('Removes the X-Pingback header, disables pingbacks and trackbacks on new posts, and prevents self-pingbacks. Reduces spam, blocks potential DDoS vectors, and slightly improves performance by avoiding useless requests.', 'zenpress'),
    1515    'category' => __('core', 'zenpress'),
    1616    'subcategory' => __('security', 'zenpress'),
  • zenpress/tags/2.2.0/inc/snippets/meta/disable-rest-api.meta.php

    r3412245 r3448585  
    1414    'title' => __('Disable REST API for visitors not logged into WordPress', 'zenpress'),
    1515    'description' => __(
    16         'Disable the WP REST API for visitors not logged into WordPress.',
     16        'Restricts the WP REST API to logged-in users only; unauthenticated requests receive an error. Bypass filters (zenpress_disable_wp_rest_api_post_var, zenpress_disable_wp_rest_api_server_var) allow specific integrations (e.g. webhooks); use non-guessable values only.',
    1717        'zenpress'
    1818    ),
  • zenpress/tags/2.2.0/inc/snippets/meta/protect-wp-login.meta.php

    r3412245 r3448585  
    1212return [
    1313    'title' => __('Protect the wp-login form from brute force attacks', 'zenpress'),
    14     'description' => __('Removes detailed login error messages and limits failed login attempts per IP address. Blocks further attempts for a set duration after too many failures. Improves security by mitigating brute force attacks.', 'zenpress'),
     14    'description' => __('Removes detailed login error messages and limits failed login attempts per IP. Blocks further attempts for 5 minutes after 5 failed tries. Mitigates brute-force attacks.', 'zenpress'),
    1515    'category' => __('tools', 'zenpress'),
    1616    'subcategory' => __('security', 'zenpress'),
  • zenpress/tags/2.2.0/inc/snippets/meta/remove-gutenberg-unwanted-block-patterns.meta.php

    r3412245 r3448585  
    1313    'title' => __('Remove WordPress default remote block patterns', 'zenpress'),
    1414    'description' => __('Prevents WordPress from loading remote block patterns and removes the built-in core block patterns. Reduces editor clutter and improves performance by avoiding unnecessary data loading.', 'zenpress'),
    15     'category' => __(' gutenberg', 'zenpress'),
     15    'category' => __('gutenberg', 'zenpress'),
    1616    'subcategory' => __('performance', 'zenpress'),
    1717    'weight' => 0,
  • zenpress/tags/2.2.0/inc/snippets/meta/remove-rest-api-link.meta.php

    r3412245 r3448585  
    1212return [
    1313    'title' => __('Remove REST API links', 'zenpress'),
    14     'description' => __('Prevents WordPress from adding REST API discovery links to the head section of the site. reduces unnecessary HTML output and slightly improves performance while keeping REST API functionality available.', 'zenpress'),
     14    'description' => __('Prevents WordPress from adding REST API discovery links to the head section of the site. Reduces unnecessary HTML output and slightly improves performance while keeping REST API functionality available.', 'zenpress'),
    1515    'category' => __('core', 'zenpress'),
    1616    'subcategory' => __('performance', 'zenpress'),
  • zenpress/tags/2.2.0/languages/zenpress.pot

    r3412245 r3448585  
    1 # Copyright (C) 2025 Quentin Le Duff
     1# Copyright (C) 2026 Quentin Le Duff
    22# This file is distributed under the GPL v2 or later.
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: ZenPress - Cleaner, Lighter, Faster WP 2.1.0\n"
     5"Project-Id-Version: ZenPress - Cleaner, Lighter, Faster WP 2.2.0\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/zenpress\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
     
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2025-12-05T13:56:04+00:00\n"
     12"POT-Creation-Date: 2026-01-28T10:05:55+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.12.0\n"
     
    142142msgstr ""
    143143
    144 #: inc/snippets/functions/block-user-enumeration.php:13
    145 #: inc/snippets/functions/block-user-enumeration.php:21
     144#: inc/snippets/functions/block-user-enumeration.php:11
     145#: inc/snippets/functions/block-user-enumeration.php:19
    146146msgid "Access denied."
    147147msgstr ""
    148148
    149 #: inc/snippets/functions/disable-rest-api.php:25
     149#: inc/snippets/functions/disable-rest-api.php:56
    150150msgid "REST API restricted to authenticated users."
    151151msgstr ""
     
    155155msgstr ""
    156156
    157 #: inc/snippets/functions/protect-wp-login.php:35
    158 #: inc/snippets/functions/protect-wp-login.php:48
     157#: inc/snippets/functions/protect-wp-login.php:36
     158#: inc/snippets/functions/protect-wp-login.php:50
    159159msgid "Too many failed login attempts. Try again later."
    160160msgstr ""
     
    173173#: inc/snippets/meta/disable-application-passwords.meta.php:18
    174174#: inc/snippets/meta/disable-author-archives.meta.php:18
     175#: inc/snippets/meta/disable-autosave.meta.php:18
     176#: inc/snippets/meta/disable-capital-p-dangit.meta.php:18
     177#: inc/snippets/meta/disable-dashicons.meta.php:18
    175178#: inc/snippets/meta/disable-dns-prefetch.meta.php:18
     179#: inc/snippets/meta/disable-emoji-scripts.meta.php:18
     180#: inc/snippets/meta/disable-jquery-migrate.meta.php:18
    176181#: inc/snippets/meta/disable-login-language-selector.meta.php:15
    177182#: inc/snippets/meta/disable-oembed.meta.php:15
     183#: inc/snippets/meta/disable-password-strength-meter.meta.php:18
     184#: inc/snippets/meta/disable-pdf-thumbnails.meta.php:15
    178185#: inc/snippets/meta/disable-pingback-trackback.meta.php:15
    179186#: inc/snippets/meta/disable-rest-api.meta.php:19
     
    181188#: inc/snippets/meta/disable-shortlink.meta.php:15
    182189#: inc/snippets/meta/disable-wlw-manifest.meta.php:15
     190#: inc/snippets/meta/disable-wordpress-default-lazy-loading.meta.php:18
    183191#: inc/snippets/meta/disable-xmlrpc-rsdlink.meta.php:15
    184192#: inc/snippets/meta/hide-wordpress-version.meta.php:15
     193#: inc/snippets/meta/limit-post-revisions.meta.php:18
     194#: inc/snippets/meta/remove-help-button.meta.php:18
    185195#: inc/snippets/meta/remove-rest-api-link.meta.php:15
     196#: inc/snippets/meta/remove-thanks-for-using-wordpress-in-footer.meta.php:18
     197#: inc/snippets/meta/remove-wordpress-logo.meta.php:18
    186198msgid "core"
    187199msgstr ""
     
    211223#: inc/snippets/meta/disable-default-pattern-categories.meta.php:16
    212224#: inc/snippets/meta/disable-login-language-selector.meta.php:16
     225#: inc/snippets/meta/remove-help-button.meta.php:19
     226#: inc/snippets/meta/remove-thanks-for-using-wordpress-in-footer.meta.php:19
     227#: inc/snippets/meta/remove-wordpress-logo.meta.php:19
    213228msgid "user-interface"
    214229msgstr ""
     
    219234
    220235#: inc/snippets/meta/clean-dashboard-items.meta.php:14
    221 msgid "Removes unnecessary widgets and ads widgets from the dashboard. Declutters the admin area and improves usability."
     236msgid "Removes unnecessary and ad widgets from the dashboard. Declutters the admin area and improves usability."
    222237msgstr ""
    223238
     
    235250
    236251#: inc/snippets/meta/disable-adjacent-posts.meta.php:19
     252#: inc/snippets/meta/disable-autosave.meta.php:19
     253#: inc/snippets/meta/disable-capital-p-dangit.meta.php:19
    237254#: inc/snippets/meta/disable-dashicons.meta.php:19
    238255#: inc/snippets/meta/disable-dns-prefetch.meta.php:19
     
    240257#: inc/snippets/meta/disable-jquery-migrate.meta.php:19
    241258#: inc/snippets/meta/disable-oembed.meta.php:16
     259#: inc/snippets/meta/disable-password-strength-meter.meta.php:19
    242260#: inc/snippets/meta/disable-pdf-thumbnails.meta.php:16
    243261#: inc/snippets/meta/disable-rss.meta.php:16
     
    248266#: inc/snippets/meta/disable-woocommerce-stripe-scripts.meta.php:16
    249267#: inc/snippets/meta/disable-woocommerce-widgets.meta.php:16
     268#: inc/snippets/meta/disable-wordpress-default-lazy-loading.meta.php:19
     269#: inc/snippets/meta/limit-post-revisions.meta.php:19
    250270#: inc/snippets/meta/remove-gutenberg-unwanted-block-patterns.meta.php:16
    251271#: inc/snippets/meta/remove-rest-api-link.meta.php:16
     
    260280
    261281#: inc/snippets/meta/disable-application-passwords.meta.php:14
    262 msgid "Disables WordPress application passwords for all users, improving security. Only disable if API access is not needed."
     282msgid "Disables WordPress application passwords for all users. Improves security; do not enable if you need API or app-based authentication (e.g. mobile apps, REST clients)."
    263283msgstr ""
    264284
     
    271291msgstr ""
    272292
     293#: inc/snippets/meta/disable-autosave.meta.php:13
     294msgid "Disable autosave"
     295msgstr ""
     296
     297#: inc/snippets/meta/disable-autosave.meta.php:14
     298msgid "Stops the classic editor from autosaving drafts periodically. Reduces database writes and heartbeat traffic. The block editor may still use its own autosave; this targets the legacy post editor."
     299msgstr ""
     300
     301#: inc/snippets/meta/disable-capital-p-dangit.meta.php:13
     302msgid "Disable capital_P_dangit filter"
     303msgstr ""
     304
     305#: inc/snippets/meta/disable-capital-p-dangit.meta.php:14
     306msgid "Removes the filter that forces \"Wordpress\" to \"WordPress\" in titles, content, comments, and widget text. Saves a small amount of processing on each page load."
     307msgstr ""
     308
    273309#: inc/snippets/meta/disable-dashicons.meta.php:13
    274310msgid "Disable dashicons"
     
    279315msgstr ""
    280316
    281 #: inc/snippets/meta/disable-dashicons.meta.php:18
    282 #: inc/snippets/meta/disable-emoji-scripts.meta.php:18
    283 #: inc/snippets/meta/disable-jquery-migrate.meta.php:18
    284 #: inc/snippets/meta/disable-pdf-thumbnails.meta.php:15
    285 msgid " core"
    286 msgstr ""
    287 
    288317#: inc/snippets/meta/disable-default-pattern-categories.meta.php:13
    289318msgid "Disable default pattern categories in site editor"
     
    291320
    292321#: inc/snippets/meta/disable-default-pattern-categories.meta.php:14
    293 msgid "Removes default pattern categories from the block pattern inserter in the site editor. Disables default categories: featured, about, audio, banner, buttons, call-to-action, columns, contact, footer, gallery, header, media, portfolio, posts, query, services, team, testimonials, text, videos, and any custom categories. This simplifies the interface by hiding category navigation while patterns remain accessible."
     322msgid "Removes default pattern categories from the block pattern inserter in the site editor (e.g. featured, about, audio, banner, buttons, call-to-action, columns, contact, footer, gallery, header, media, portfolio, posts, query, services, team, testimonials, text, videos) and any custom ones. Simplifies the interface; patterns remain accessible."
    294323msgstr ""
    295324
    296325#: inc/snippets/meta/disable-default-pattern-categories.meta.php:15
     326#: inc/snippets/meta/remove-gutenberg-unwanted-block-patterns.meta.php:15
    297327#: inc/snippets/meta/separate-gutenberg-core-block-styles.meta.php:15
    298328msgid "gutenberg"
     
    304334
    305335#: inc/snippets/meta/disable-dns-prefetch.meta.php:14
    306 msgid "Removes DNS prefetch resource hints from wp_head avoids unnecessary DNS lookups and slightly improve performance on some sites."
     336msgid "Removes DNS prefetch resource hints from wp_head. Avoids unnecessary DNS lookups and slightly improves performance on some sites."
    307337msgstr ""
    308338
     
    339369msgstr ""
    340370
     371#: inc/snippets/meta/disable-password-strength-meter.meta.php:13
     372msgid "Disable Password Strength Meter"
     373msgstr ""
     374
     375#: inc/snippets/meta/disable-password-strength-meter.meta.php:14
     376msgid "Prevents the password strength meter and zxcvbn scripts from loading on profile, login, and similar pages. Saves roughly 400KB and reduces script parsing. Users will not see the strength indicator when choosing passwords."
     377msgstr ""
     378
    341379#: inc/snippets/meta/disable-pdf-thumbnails.meta.php:13
    342380msgid "Disable PDF thumbnails"
     
    344382
    345383#: inc/snippets/meta/disable-pdf-thumbnails.meta.php:14
    346 msgid "Prevents WordPress from generating thumbnails for uploaded PDF files by removing fallback image sizes. saves storage space and improves performance by avoiding unnecessary image generation."
     384msgid "Prevents WordPress from generating thumbnails for uploaded PDF files by removing fallback image sizes. Saves storage space and improves performance by avoiding unnecessary image generation."
    347385msgstr ""
    348386
     
    352390
    353391#: inc/snippets/meta/disable-pingback-trackback.meta.php:14
    354 msgid "Removes the X-Pingback header, disables pingbacks and trackbacks on new posts, and prevents self-pingbacks. reduces spam, blocks potential DDoS vectors, and slightly improves performance by avoiding useless requests."
     392msgid "Removes the X-Pingback header, disables pingbacks and trackbacks on new posts, and prevents self-pingbacks. Reduces spam, blocks potential DDoS vectors, and slightly improves performance by avoiding useless requests."
    355393msgstr ""
    356394
     
    360398
    361399#: inc/snippets/meta/disable-rest-api.meta.php:15
    362 msgid "Disable the WP REST API for visitors not logged into WordPress."
     400msgid "Restricts the WP REST API to logged-in users only; unauthenticated requests receive an error. Bypass filters (zenpress_disable_wp_rest_api_post_var, zenpress_disable_wp_rest_api_server_var) allow specific integrations (e.g. webhooks); use non-guessable values only."
    363401msgstr ""
    364402
     
    428466msgstr ""
    429467
     468#: inc/snippets/meta/disable-wordpress-default-lazy-loading.meta.php:13
     469msgid "Disable WordPress default lazy loading"
     470msgstr ""
     471
     472#: inc/snippets/meta/disable-wordpress-default-lazy-loading.meta.php:14
     473msgid "Stops WordPress from adding loading=\"lazy\" to images and iframes. Use only if you rely on a theme, plugin, or CDN for lazy loading, or if you prefer to load all media immediately."
     474msgstr ""
     475
    430476#: inc/snippets/meta/disable-xmlrpc-rsdlink.meta.php:13
    431477msgid "Disable XML-RPC and remove RSD link"
     
    452498msgstr ""
    453499
     500#: inc/snippets/meta/limit-post-revisions.meta.php:13
     501msgid "Limit post revision to 10"
     502msgstr ""
     503
     504#: inc/snippets/meta/limit-post-revisions.meta.php:14
     505msgid "Keeps at most 10 revisions per post (or page). Older revisions are deleted when new ones are created. Reduces database size and improves performance."
     506msgstr ""
     507
    454508#: inc/snippets/meta/protect-wp-login.meta.php:13
    455509msgid "Protect the wp-login form from brute force attacks"
     
    457511
    458512#: inc/snippets/meta/protect-wp-login.meta.php:14
    459 msgid "Removes detailed login error messages and limits failed login attempts per IP address. Blocks further attempts for a set duration after too many failures. Improves security by mitigating brute force attacks."
     513msgid "Removes detailed login error messages and limits failed login attempts per IP. Blocks further attempts for 5 minutes after 5 failed tries. Mitigates brute-force attacks."
    460514msgstr ""
    461515
     
    472526msgstr ""
    473527
    474 #: inc/snippets/meta/remove-gutenberg-unwanted-block-patterns.meta.php:15
    475 msgid " gutenberg"
     528#: inc/snippets/meta/remove-help-button.meta.php:13
     529msgid "Remove \"Help\" button"
     530msgstr ""
     531
     532#: inc/snippets/meta/remove-help-button.meta.php:14
     533msgid "Hides the Help tab and panel on all admin screens. Reduces clutter for users who do not use the in-app help."
    476534msgstr ""
    477535
     
    481539
    482540#: inc/snippets/meta/remove-rest-api-link.meta.php:14
    483 msgid "Prevents WordPress from adding REST API discovery links to the head section of the site. reduces unnecessary HTML output and slightly improves performance while keeping REST API functionality available."
     541msgid "Prevents WordPress from adding REST API discovery links to the head section of the site. Reduces unnecessary HTML output and slightly improves performance while keeping REST API functionality available."
     542msgstr ""
     543
     544#: inc/snippets/meta/remove-thanks-for-using-wordpress-in-footer.meta.php:13
     545msgid "Remove \"Thanks for using WordPress\" in footer"
     546msgstr ""
     547
     548#: inc/snippets/meta/remove-thanks-for-using-wordpress-in-footer.meta.php:14
     549msgid "Removes the \"Thank you for creating with WordPress\" message from the admin footer. Cleans up the interface."
    484550msgstr ""
    485551
     
    490556#: inc/snippets/meta/remove-woocommerce-patterns.meta.php:14
    491557msgid "Removes all WooCommerce block patterns to avoid unnecessary pattern registration in the editor."
     558msgstr ""
     559
     560#: inc/snippets/meta/remove-wordpress-logo.meta.php:13
     561msgid "Remove WordPress logo"
     562msgstr ""
     563
     564#: inc/snippets/meta/remove-wordpress-logo.meta.php:14
     565msgid "Removes the WordPress logo and its dropdown from the admin bar. Cleans up the interface for a more neutral or branded look."
    492566msgstr ""
    493567
  • zenpress/tags/2.2.0/readme.txt

    r3412245 r3448585  
    55Requires at least: 6.0
    66Tested up to: 6.9
    7 Stable tag: 2.1.0
    8 Requires PHP: 7.4
     7Stable tag: 2.2.0
     8Requires PHP: 8.3
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html/
     
    3434
    3535= Settings subpage 🧰 =
    36 * Organized interface with vertical tabs for easy navigation between categories (Core, Gutenberg, WooCommerce, Tools).
     36* Organized interface with vertical tabs for easy navigation between categories (Core, Gutenberg, WooCommerce, Ads-blocker, Tools).
    3737* Features grouped by subcategories (Performance, Security, User Interface) with visual icons for quick identification.
    3838* Three ready-to-use presets: Corporate website, Blog, and E-commerce - each optimized for specific site types.
     
    5353* Disable WordPress shortlink.
    5454* Disable WLW link.
    55 * Remove WordPress default remote block patterns.
    5655* Remove REST API links.
    57 * Separate loading of core block styles.
     56* Disable capital_P_dangit filter.
     57* Disable autosave.
     58* Limit post revision to 10.
     59* Disable Password Strength Meter.
     60* Disable WordPress default lazy loading.
    5861
    5962= Core - Security =
     
    6669* Disable XML-RPC and remove RSD link.
    6770* Hide WordPress version.
    68 * Protect the wp-login form from brute force attacks.
    6971
    7072= Core - User Interface =
    7173
    7274* Clean up the WordPress admin bar.
    73 * Clean up the WordPress Dashboard.
    7475* Disable the login language selector.
     76* Remove WordPress logo.
     77* Remove "Help" button.
     78* Remove "Thanks for using WordPress" in footer.
    7579
    7680= WooCommerce - Performance =
     
    9195* Disable default pattern categories in site editor.
    9296
     97= Ads-blocker - User Interface =
     98* Clean up the WordPress Dashboard.
     99
     100= Tools - Security =
     101* Protect the wp-login form from brute force attacks.
     102
    93103= Presets =
    94104* Corporate website / Portfolio: Optimized for business sites and portfolios. Focuses on security, performance, and removing unnecessary features like RSS feeds and author archives.
     
    112122* Manage Heartbeat API (frontend + backend + admin whitelist).
    113123
    114 = Performance =
    115 * Disable capital_P_dangit filter.
    116 * Disable autosave.
    117 * Disable post revision.
    118 * Disable Password Strength Meter.
    119 * Disable WordPress default lazy loading.
    120 
    121124= User Interface =
    122 * Remove "howdy" from admin bar.
    123 * Remove WordPress logo.
    124 * Remove "Help button".
    125 * Remove "Thanks for using WordPress" in footer.
    126125* Remove "site health" page.
    127126* Remove "Privacy tools".
     
    175174In addition, if you like the plugin then I'd love for you to [leave a review](https://wordpress.org/support/plugin/zenpress/reviews/). Tell all your friends about it too!
    176175
     176= Does ZenPress work with my existing caching / optimization plugins? =
     177
     178Yes. ZenPress focuses on disabling unnecessary core features and plugin bloat; it does not handle page caching, minification, or image optimization. It is designed to work alongside plugins like Cache Enabler, Autoptimize, and most object-cache solutions. If a performance feature overlaps, simply disable it in one of the tools.
     179
     180= Can ZenPress break my theme or plugins? =
     181
     182Potentially, yes—especially if your theme or plugins rely on features you disable (RSS feeds, oEmbed, REST API, emojis, WooCommerce assets, etc.). That’s why each snippet includes clear descriptions and categories (performance, security, UI). Always test changes on a staging site first and enable snippets gradually.
     183
     184= How do I know which snippets are safe to enable? =
     185
     186If you are unsure, start with a preset (Corporate, Blog, E‑commerce) and then adjust. For manual tuning, prefer UI and performance snippets first (dashicons, emojis, dashboard/admin-bar cleanup) before more invasive ones (REST API, XML‑RPC, RSS). After each change, check: frontend pages, login, editor, and (if used) WooCommerce flows.
     187
     188= What happens if I disable the REST API? =
     189
     190Unauthenticated REST requests will be blocked except for any explicit bypasses configured via the filters documented in the snippet (`zenpress_disable_wp_rest_api_post_var`, `zenpress_disable_wp_rest_api_server_var`). Core features and plugins that depend on public REST endpoints (some blocks, headless/front-end apps, third-party integrations) may stop working until you whitelist the required routes.
     191
     192**Important**: If you don't know how to configure bypass filters or whitelist specific routes, don't activate this snippet. It can break functionality that relies on public REST API access.
     193
     194= Does ZenPress store any personal data or phone home? =
     195
     196No. ZenPress does not collect, store, or transmit any personal data. It does not contact external services or include third‑party trackers. All settings are stored in standard WordPress options and remain on your site only.
     197
     198= Is ZenPress multisite compatible? =
     199
     200ZenPress can be network‑activated or activated per site. Settings are stored per site, so each site in a network can have different snippets enabled. As with any optimization/security plugin, test network‑wide changes carefully, especially REST API and XML‑RPC related snippets.
     201
    177202= I have a suggestion =
    178203
    179204Nice ! If you can't find anything in the roadmap, feel free to submit your suggestion on the support page! If you know how to code, you can even contribute on GitHub.
    180205
    181 = Does this plugin work with PHP 8 =
    182 
    183 Yes, it 's been tested actively and works from PHP 7.4 to PHP 8.4.
    184 
    185206== Changelog ==
     207
     208= 2.2.0 =
     209- Global: Dropped PHP 7.4 support and aligned minimum PHP requirement with the currently recommended WordPress version.
     210- Global: Replaced strpos() with str_contains() and str_starts_with() throughout all snippets.
     211- Snippets: Converted snippets to use direct execution pattern where applicable for better performance.
     212- Security: Protect wp-login: fix wp_die() so blocked login responses return HTTP 403 instead of 200.
     213- Security: Loader: guard against path traversal in zenpress_load_snippets() when the folder argument contains '..'.
     214- Security: Disable REST API: document in-code that bypass filters (zenpress_disable_wp_rest_api_post_var, zenpress_disable_wp_rest_api_server_var) should use non-guessable values only.
     215- New actionable function: Disable autosave.
     216- New actionable function: Disable capital_P_dangit filter.
     217- New actionable function: Disable Password Strength Meter.
     218- New actionable function: Disable WordPress default lazy loading.
     219- New actionable function: Limit post revision to 10.
     220- New actionable function: Remove "Help" button.
     221- New actionable function: Remove "Thanks for using WordPress" in footer.
     222- New actionable function: Remove WordPress logo.
    186223
    187224= 2.1.0 =
     
    296333== Upgrade Notice ==
    297334
     335= 2.2.0 =
     336- Breaking: PHP 8.3 is now required (PHP 7.4 support dropped). Major code modernization with improved type safety and performance.
     337- Security: HTTP 403 on login block, path traversal guard in snippet loader, and in-code docs for REST API bypass filters.
     338
    298339= 1.0.0.1 =
    299340
  • zenpress/tags/2.2.0/zenpress.php

    r3412245 r3448585  
    1212 * Plugin Name: ZenPress - Cleaner, Lighter, Faster WP
    1313 * Description: Easily speed up and strengthen your WordPress site by cleaning out unnecessary features and protecting weak points.
    14  * Version: 2.1.0
     14 * Version: 2.2.0
    1515 * Plugin URI: https://wordpress.org/plugins/zenpress/
    1616 * Author: Quentin Le Duff
     
    2020 * Requires at least: 6.0
    2121 * Tested up to: 6.9
    22  * Requires PHP: 7.4
     22 * Requires PHP: 8.3
    2323 * License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html/
    2424 * License: GPL v2 or later
  • zenpress/trunk/assets/build/index.asset.php

    r3412245 r3448585  
    1 <?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-notices'), 'version' => '980bfa58281a120e84d0');
     1<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-notices'), 'version' => '253355e0f124eff32114');
  • zenpress/trunk/assets/build/index.js

    r3412245 r3448585  
    1 (()=>{"use strict";var e={n:s=>{var t=s&&s.__esModule?()=>s.default:()=>s;return e.d(t,{a:t}),t},d:(s,t)=>{for(var n in t)e.o(t,n)&&!e.o(s,n)&&Object.defineProperty(s,n,{enumerable:!0,get:t[n]})},o:(e,s)=>Object.prototype.hasOwnProperty.call(e,s)};const s=window.wp.domReady;var t=e.n(s);const n=window.wp.element,r=window.wp.i18n,a=window.wp.components,i=window.wp.apiFetch;var o=e.n(i);const c=window.wp.data,l=window.wp.notices,p=window.ReactJSXRuntime,d=({label:e,value:s,onChange:t,help:r})=>{const i=(0,n.useRef)(null);return(0,n.useEffect)(()=>((e,s)=>{if(!e)return;const t=t=>{if("Enter"===t.key){const n=e.querySelector('input[type="checkbox"]');n&&(document.activeElement===n||e.contains(document.activeElement))&&(t.preventDefault(),t.stopPropagation(),s())}};return e.addEventListener("keydown",t),()=>{e.removeEventListener("keydown",t)}})(i.current,t),[t]),(0,p.jsx)("div",{ref:i,children:(0,p.jsx)(a.ToggleControl,{label:e,checked:s,onChange:t,help:r,__nextHasNoMarginBottom:!0})})},u=({onClick:e,isBusy:s})=>(0,p.jsx)(a.Button,{variant:"primary",onClick:e,isBusy:s,__next40pxDefaultSize:!0,children:(0,r.__)("Save settings","zenpress")}),b=()=>{const{removeNotice:e}=(0,c.useDispatch)(l.store),s=(0,c.useSelect)(e=>e(l.store).getNotices(),[]);return s&&0!==s.length?(0,p.jsx)(a.NoticeList,{notices:s,onRemove:e}):null},h=(0,n.createContext)(),f=({selectedTabId:e,onSelect:s,orientation:t="horizontal",children:r})=>{const[a,i]=(0,n.useState)(),o=(0,n.useRef)(null),c=void 0!==e?e:a;return(0,p.jsx)(h.Provider,{value:{selectedTabId:c,onSelect:t=>{void 0===e&&i(t),s?.(t)},orientation:t,getOrderedTabIds:()=>o.current?Array.from(o.current.querySelectorAll('[role="tab"]')).map(e=>{const s=e.getAttribute("id");return s?s.replace("zenpress-tab-",""):null}).filter(Boolean):[],tabListRef:o},children:(0,p.jsx)("div",{className:`zenpress-tabs zenpress-tabs--${t}`,children:r})})};f.TabList=({children:e})=>{const{orientation:s,tabListRef:t}=(0,n.useContext)(h);return(0,p.jsx)("div",{ref:t,className:`zenpress-tabs__list zenpress-tabs__list--${s}`,role:"tablist","aria-orientation":s,children:e})},f.Tab=({tabId:e,title:s,className:t="",children:r})=>{const{selectedTabId:a,onSelect:i,orientation:o,getOrderedTabIds:c}=(0,n.useContext)(h),l=a===e,d=(0,n.useRef)(null);return(0,p.jsx)("button",{ref:d,className:`zenpress-tabs__tab ${l?"zenpress-tabs__tab--is-active":""} ${t}`.trim(),role:"tab","aria-selected":l,"aria-controls":`zenpress-tab-panel-${e}`,id:`zenpress-tab-${e}`,tabIndex:l?0:-1,onClick:()=>i(e),onKeyDown:s=>{const t=c();if(!t||0===t.length)return;const n=t.indexOf(e);if(-1===n)return;let r=n;if("vertical"===o?"ArrowDown"===s.key?(s.preventDefault(),r=n<t.length-1?n+1:0):"ArrowUp"===s.key&&(s.preventDefault(),r=n>0?n-1:t.length-1):"ArrowRight"===s.key?(s.preventDefault(),r=n<t.length-1?n+1:0):"ArrowLeft"===s.key&&(s.preventDefault(),r=n>0?n-1:t.length-1),"Home"===s.key?(s.preventDefault(),r=0):"End"===s.key&&(s.preventDefault(),r=t.length-1)," "===s.key||"Enter"===s.key)return s.preventDefault(),void i(e);if(r!==n&&r>=0&&r<t.length){const e=t[r],s=document.getElementById(`zenpress-tab-${e}`);s&&(s.focus(),i(e))}},onFocus:()=>{l||i(e)},children:s||r})},f.TabPanel=({tabId:e,children:s})=>{const{selectedTabId:t}=(0,n.useContext)(h),r=(0,n.useRef)(null),a=t===e;return(0,n.useEffect)(()=>{a&&r.current&&(0===r.current.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])').length?r.current.setAttribute("tabindex","0"):r.current.removeAttribute("tabindex"))},[a]),a?(0,p.jsx)("div",{ref:r,className:"zenpress-tabs__panel",role:"tabpanel",id:`zenpress-tab-panel-${e}`,"aria-labelledby":`zenpress-tab-${e}`,children:s}):(0,p.jsx)("div",{className:"zenpress-tabs__panel",role:"tabpanel",id:`zenpress-tab-panel-${e}`,"aria-labelledby":`zenpress-tab-${e}`,hidden:!0,children:s})};const x=()=>{const{snippets:e,setSnippets:s,saveSettings:t,isSaving:i}=(()=>{const[e,s]=(0,n.useState)({}),[t,a]=(0,n.useState)(!1),{createSuccessNotice:i,createErrorNotice:p}=(0,c.useDispatch)(l.store);return(0,n.useEffect)(()=>{o()({path:"/wp/v2/settings"}).then(e=>{const t=Array.isArray(e?.zenpress_active_snippets)?e.zenpress_active_snippets:[],n=window?.zenpressSnippetsMeta||{},r={};Object.keys(n).forEach(e=>{r[e]={...n[e],"enable-snippet":t.includes(e)}}),s(r)}).catch(()=>{p((0,r.__)("Failed to load settings.","zenpress"))})},[p]),{snippets:e,setSnippets:s,saveSettings:async()=>{a(!0);const s=Object.keys(e).filter(s=>e[s]?.["enable-snippet"]);try{await o()({path:"/wp/v2/settings",method:"POST",data:{zenpress_active_snippets:s}}),i((0,r.__)("Settings saved.","zenpress"))}catch{p((0,r.__)("Failed to save settings.","zenpress"))}finally{a(!1)}},isSaving:t}})(),[h,x]=(0,n.useState)(),_=e=>{s(s=>{const t={};return Object.entries(s).forEach(([s,n])=>{const r=(Array.isArray(n?.preset)?n.preset:[]).includes(e);t[s]={...n,"enable-snippet":r}}),t})},m=e=>e?e.charAt(0).toUpperCase()+e.slice(1).toLowerCase():e,v=["core","gutenberg","woocommerce","ads-blocker","tools"],y={};Object.keys(e).forEach(s=>{const t=e[s],n=(t?.category||(0,r.__)("Uncategorized","zenpress")).toLowerCase(),a=(t?.subcategory||(0,r.__)("uncategorized","zenpress")).toLowerCase();y[n]||(y[n]={}),y[n][a]||(y[n][a]=[]),y[n][a].push({name:s,data:t})});const z=Object.keys(y).sort((e,s)=>{const t=v.indexOf(e.toLowerCase()),n=v.indexOf(s.toLowerCase());return-1!==t&&-1!==n?t-n:-1!==t?-1:-1!==n?1:e.localeCompare(s,void 0,{sensitivity:"base"})});return(0,n.useEffect)(()=>{!h&&z.length>0&&x(z[0])},[h,z.length]),(0,n.useEffect)(()=>{const e=e=>{(e.ctrlKey||e.metaKey)&&"s"===e.key&&(e.preventDefault(),i||t())};return document.addEventListener("keydown",e),()=>{document.removeEventListener("keydown",e)}},[t,i]),(0,p.jsxs)("article",{className:"zenpress-row",children:[(0,p.jsxs)("section",{className:"zenpress-main",children:[(0,p.jsx)("div",{className:"zenpress-notices",children:(0,p.jsx)(b,{})}),(0,p.jsxs)("div",{className:"zenpress-panel",children:[(0,p.jsxs)(f,{orientation:"vertical",selectedTabId:h,onSelect:e=>{x(e),x(e)},children:[(0,p.jsx)(f.TabList,{children:z.map(e=>{const s=`zenpress-tabs__tab--category-${e.toLowerCase().replace(/\s+/g,"-")}`;return(0,p.jsx)(f.Tab,{tabId:e,title:m(e),className:s,children:m(e)},e)})}),z.map(e=>{const t=Object.keys(y[e]).sort();return(0,p.jsxs)(f.TabPanel,{tabId:e,children:[(0,p.jsx)("h2",{children:m(e)}),t.map(t=>(0,p.jsxs)("div",{className:`zenpress-subcategory zenpress-subcategory-${t.toLowerCase().replace(/\s+/g,"-")}`,children:[(0,p.jsx)("hr",{}),(0,p.jsx)("h3",{children:m(t)}),y[e][t].map(({name:e,data:t})=>(0,p.jsx)(d,{label:t.title||e,value:t?.["enable-snippet"]||!1,onChange:()=>{return t=e,void s(e=>({...e,[t]:{...e[t],"enable-snippet":!e[t]?.["enable-snippet"]}}));var t},help:t.description||""},e))]},t))]},e)})]}),(0,p.jsxs)("div",{className:"zenpress-actions",children:[(0,p.jsxs)("div",{className:"zenpress-actions-bulk",children:[(0,p.jsx)(a.Button,{variant:"tertiary",onClick:()=>{s(e=>{const s={};return Object.keys(e).forEach(t=>{s[t]={...e[t],"enable-snippet":!0}}),s})},__next40pxDefaultSize:!0,children:(0,r.__)("Enable all actions","zenpress")}),(0,p.jsx)(a.Button,{isDestructive:!0,onClick:()=>{s(e=>{const s={};return Object.keys(e).forEach(t=>{s[t]={...e[t],"enable-snippet":!1}}),s})},__next40pxDefaultSize:!0,children:(0,r.__)("Disable all actions","zenpress")})]}),(0,p.jsx)(u,{onClick:t,isBusy:i})]})]})]}),(0,p.jsx)("aside",{className:"zenpress-sidebar",children:(0,p.jsx)("div",{className:"zenpress-presets",children:(0,p.jsxs)("div",{className:"zenpress-presets-description",children:[(0,p.jsx)("h2",{children:(0,r.__)("Pick configuration preset","zenpress")}),(0,p.jsx)("p",{children:(0,r.__)("Don't know which features to enable? Quickly configure ZenPress by selecting a preset that matches your site type. Each preset enables optimized features for your specific use case.","zenpress")}),(0,p.jsx)("hr",{}),(0,p.jsxs)("h3",{children:["🖼️ ",(0,r.__)("Corporate website","zenpress")]}),(0,p.jsx)("p",{children:(0,r.__)("Optimized for business sites and portfolios. Focuses on security, performance, and removing unnecessary features like RSS feeds and author archives.","zenpress")}),(0,p.jsx)(a.Button,{variant:"secondary",onClick:()=>_("corporate-website"),__next40pxDefaultSize:!0,children:(0,r.__)("Enable","zenpress")}),(0,p.jsx)("hr",{}),(0,p.jsxs)("h3",{children:[" 📰 ",(0,r.__)("Blog","zenpress")]}),(0,p.jsx)("p",{children:(0,r.__)("Tailored for content-focused blogs. Includes performance and security optimizations while preserving essential blog features like RSS feeds.","zenpress")}),(0,p.jsx)(a.Button,{variant:"secondary",onClick:()=>_("blog"),__next40pxDefaultSize:!0,children:(0,r.__)("Enable","zenpress")}),(0,p.jsx)("hr",{}),(0,p.jsxs)("h3",{children:["🛒 ",(0,r.__)("E-commerce","zenpress")]}),(0,p.jsx)("p",{children:(0,r.__)("Designed for WooCommerce stores. Includes all performance and security features plus WooCommerce-specific optimizations for faster checkout.","zenpress")}),(0,p.jsx)(a.Button,{variant:"secondary",onClick:()=>_("ecommerce"),__next40pxDefaultSize:!0,children:(0,r.__)("Enable","zenpress")})]})})})]})};t()(()=>{const e=document.getElementById("zenpress-settings");e&&(0,n.createRoot)(e).render((0,p.jsx)(x,{}))})})();
     1(()=>{"use strict";var e={n:s=>{var t=s&&s.__esModule?()=>s.default:()=>s;return e.d(t,{a:t}),t},d:(s,t)=>{for(var n in t)e.o(t,n)&&!e.o(s,n)&&Object.defineProperty(s,n,{enumerable:!0,get:t[n]})},o:(e,s)=>Object.prototype.hasOwnProperty.call(e,s)};const s=window.wp.domReady;var t=e.n(s);const n=window.wp.element,r=window.wp.i18n,a=window.wp.components,i=window.wp.apiFetch;var o=e.n(i);const c=window.wp.data,l=window.wp.notices,p=window.ReactJSXRuntime,d=({label:e,value:s,onChange:t,help:r})=>{const i=(0,n.useRef)(null);return(0,n.useEffect)(()=>((e,s)=>{if(!e)return;const t=t=>{if("Enter"===t.key){const n=e.querySelector('input[type="checkbox"]'),r=e.ownerDocument||document;n&&(r.activeElement===n||e.contains(r.activeElement))&&(t.preventDefault(),t.stopPropagation(),s())}};return e.addEventListener("keydown",t),()=>{e.removeEventListener("keydown",t)}})(i.current,t),[t]),(0,p.jsx)("div",{ref:i,children:(0,p.jsx)(a.ToggleControl,{label:e,checked:s,onChange:t,help:r,__nextHasNoMarginBottom:!0})})},u=({onClick:e,isBusy:s})=>(0,p.jsx)(a.Button,{variant:"primary",onClick:e,isBusy:s,__next40pxDefaultSize:!0,children:(0,r.__)("Save settings","zenpress")}),b=()=>{const{removeNotice:e}=(0,c.useDispatch)(l.store),s=(0,c.useSelect)(e=>e(l.store).getNotices(),[]);return s&&0!==s.length?(0,p.jsx)(a.NoticeList,{notices:s,onRemove:e}):null},h=(0,n.createContext)(),f=({selectedTabId:e,onSelect:s,orientation:t="horizontal",children:r})=>{const[a,i]=(0,n.useState)(),o=(0,n.useRef)(null),c=void 0!==e?e:a;return(0,p.jsx)(h.Provider,{value:{selectedTabId:c,onSelect:t=>{void 0===e&&i(t),s?.(t)},orientation:t,getOrderedTabIds:()=>o.current?Array.from(o.current.querySelectorAll('[role="tab"]')).map(e=>{const s=e.getAttribute("id");return s?s.replace("zenpress-tab-",""):null}).filter(Boolean):[],tabListRef:o},children:(0,p.jsx)("div",{className:`zenpress-tabs zenpress-tabs--${t}`,children:r})})};f.TabList=({children:e})=>{const{orientation:s,tabListRef:t}=(0,n.useContext)(h);return(0,p.jsx)("div",{ref:t,className:`zenpress-tabs__list zenpress-tabs__list--${s}`,role:"tablist","aria-orientation":s,children:e})},f.Tab=({tabId:e,title:s,className:t="",children:r})=>{const{selectedTabId:a,onSelect:i,orientation:o,getOrderedTabIds:c}=(0,n.useContext)(h),l=a===e,d=(0,n.useRef)(null);return(0,p.jsx)("button",{ref:d,className:`zenpress-tabs__tab ${l?"zenpress-tabs__tab--is-active":""} ${t}`.trim(),role:"tab","aria-selected":l,"aria-controls":`zenpress-tab-panel-${e}`,id:`zenpress-tab-${e}`,tabIndex:l?0:-1,onClick:()=>i(e),onKeyDown:s=>{const t=c();if(!t||0===t.length)return;const n=t.indexOf(e);if(-1===n)return;let r=n;if("vertical"===o?"ArrowDown"===s.key?(s.preventDefault(),r=n<t.length-1?n+1:0):"ArrowUp"===s.key&&(s.preventDefault(),r=n>0?n-1:t.length-1):"ArrowRight"===s.key?(s.preventDefault(),r=n<t.length-1?n+1:0):"ArrowLeft"===s.key&&(s.preventDefault(),r=n>0?n-1:t.length-1),"Home"===s.key?(s.preventDefault(),r=0):"End"===s.key&&(s.preventDefault(),r=t.length-1)," "===s.key||"Enter"===s.key)return s.preventDefault(),void i(e);if(r!==n&&r>=0&&r<t.length){const e=t[r],s=document.getElementById(`zenpress-tab-${e}`);s&&(s.focus(),i(e))}},onFocus:()=>{l||i(e)},children:s||r})},f.TabPanel=({tabId:e,children:s})=>{const{selectedTabId:t}=(0,n.useContext)(h),r=(0,n.useRef)(null),a=t===e;return(0,n.useEffect)(()=>{a&&r.current&&(0===r.current.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])').length?r.current.setAttribute("tabindex","0"):r.current.removeAttribute("tabindex"))},[a]),a?(0,p.jsx)("div",{ref:r,className:"zenpress-tabs__panel",role:"tabpanel",id:`zenpress-tab-panel-${e}`,"aria-labelledby":`zenpress-tab-${e}`,children:s}):(0,p.jsx)("div",{className:"zenpress-tabs__panel",role:"tabpanel",id:`zenpress-tab-panel-${e}`,"aria-labelledby":`zenpress-tab-${e}`,hidden:!0,children:s})};const x=()=>{const{snippets:e,setSnippets:s,saveSettings:t,isSaving:i}=(()=>{const[e,s]=(0,n.useState)({}),[t,a]=(0,n.useState)(!1),{createSuccessNotice:i,createErrorNotice:p}=(0,c.useDispatch)(l.store);return(0,n.useEffect)(()=>{o()({path:"/wp/v2/settings"}).then(e=>{const t=Array.isArray(e?.zenpress_active_snippets)?e.zenpress_active_snippets:[],n=window?.zenpressSnippetsMeta||{},r={};Object.keys(n).forEach(e=>{r[e]={...n[e],"enable-snippet":t.includes(e)}}),s(r)}).catch(()=>{p((0,r.__)("Failed to load settings.","zenpress"))})},[p]),{snippets:e,setSnippets:s,saveSettings:async()=>{a(!0);const s=Object.keys(e).filter(s=>e[s]?.["enable-snippet"]);try{await o()({path:"/wp/v2/settings",method:"POST",data:{zenpress_active_snippets:s}}),i((0,r.__)("Settings saved.","zenpress"))}catch{p((0,r.__)("Failed to save settings.","zenpress"))}finally{a(!1)}},isSaving:t}})(),[h,x]=(0,n.useState)(),_=e=>{s(s=>{const t={};return Object.entries(s).forEach(([s,n])=>{const r=(Array.isArray(n?.preset)?n.preset:[]).includes(e);t[s]={...n,"enable-snippet":r}}),t})},m=e=>e?e.charAt(0).toUpperCase()+e.slice(1).toLowerCase():e,v=["core","gutenberg","woocommerce","ads-blocker","tools"],y={};Object.keys(e).forEach(s=>{const t=e[s],n=(t?.category||(0,r.__)("Uncategorized","zenpress")).toLowerCase(),a=(t?.subcategory||(0,r.__)("uncategorized","zenpress")).toLowerCase();y[n]||(y[n]={}),y[n][a]||(y[n][a]=[]),y[n][a].push({name:s,data:t})});const z=Object.keys(y).sort((e,s)=>{const t=v.indexOf(e.toLowerCase()),n=v.indexOf(s.toLowerCase());return-1!==t&&-1!==n?t-n:-1!==t?-1:-1!==n?1:e.localeCompare(s,void 0,{sensitivity:"base"})});return(0,n.useEffect)(()=>{!h&&z.length>0&&x(z[0])},[h,z]),(0,n.useEffect)(()=>{const e=e=>{(e.ctrlKey||e.metaKey)&&"s"===e.key&&(e.preventDefault(),i||t())};return document.addEventListener("keydown",e),()=>{document.removeEventListener("keydown",e)}},[t,i]),(0,p.jsxs)("article",{className:"zenpress-row",children:[(0,p.jsxs)("section",{className:"zenpress-main",children:[(0,p.jsx)("div",{className:"zenpress-notices",children:(0,p.jsx)(b,{})}),(0,p.jsxs)("div",{className:"zenpress-panel",children:[(0,p.jsxs)(f,{orientation:"vertical",selectedTabId:h,onSelect:e=>{x(e),x(e)},children:[(0,p.jsx)(f.TabList,{children:z.map(e=>{const s=`zenpress-tabs__tab--category-${e.toLowerCase().replace(/\s+/g,"-")}`;return(0,p.jsx)(f.Tab,{tabId:e,title:m(e),className:s,children:m(e)},e)})}),z.map(e=>{const t=Object.keys(y[e]).sort();return(0,p.jsxs)(f.TabPanel,{tabId:e,children:[(0,p.jsx)("h2",{children:m(e)}),t.map(t=>(0,p.jsxs)("div",{className:`zenpress-subcategory zenpress-subcategory-${t.toLowerCase().replace(/\s+/g,"-")}`,children:[(0,p.jsx)("hr",{}),(0,p.jsx)("h3",{children:m(t)}),y[e][t].map(({name:e,data:t})=>(0,p.jsx)(d,{label:t.title||e,value:t?.["enable-snippet"]||!1,onChange:()=>{return t=e,void s(e=>({...e,[t]:{...e[t],"enable-snippet":!e[t]?.["enable-snippet"]}}));var t},help:t.description||""},e))]},t))]},e)})]}),(0,p.jsxs)("div",{className:"zenpress-actions",children:[(0,p.jsxs)("div",{className:"zenpress-actions-bulk",children:[(0,p.jsx)(a.Button,{variant:"tertiary",onClick:()=>{s(e=>{const s={};return Object.keys(e).forEach(t=>{s[t]={...e[t],"enable-snippet":!0}}),s})},__next40pxDefaultSize:!0,children:(0,r.__)("Enable all actions","zenpress")}),(0,p.jsx)(a.Button,{isDestructive:!0,onClick:()=>{s(e=>{const s={};return Object.keys(e).forEach(t=>{s[t]={...e[t],"enable-snippet":!1}}),s})},__next40pxDefaultSize:!0,children:(0,r.__)("Disable all actions","zenpress")})]}),(0,p.jsx)(u,{onClick:t,isBusy:i})]})]})]}),(0,p.jsx)("aside",{className:"zenpress-sidebar",children:(0,p.jsx)("div",{className:"zenpress-presets",children:(0,p.jsxs)("div",{className:"zenpress-presets-description",children:[(0,p.jsx)("h2",{children:(0,r.__)("Pick configuration preset","zenpress")}),(0,p.jsx)("p",{children:(0,r.__)("Don't know which features to enable? Quickly configure ZenPress by selecting a preset that matches your site type. Each preset enables optimized features for your specific use case.","zenpress")}),(0,p.jsx)("hr",{}),(0,p.jsxs)("h3",{children:["🖼️ ",(0,r.__)("Corporate website","zenpress")]}),(0,p.jsx)("p",{children:(0,r.__)("Optimized for business sites and portfolios. Focuses on security, performance, and removing unnecessary features like RSS feeds and author archives.","zenpress")}),(0,p.jsx)(a.Button,{variant:"secondary",onClick:()=>_("corporate-website"),__next40pxDefaultSize:!0,children:(0,r.__)("Enable","zenpress")}),(0,p.jsx)("hr",{}),(0,p.jsxs)("h3",{children:[" 📰 ",(0,r.__)("Blog","zenpress")]}),(0,p.jsx)("p",{children:(0,r.__)("Tailored for content-focused blogs. Includes performance and security optimizations while preserving essential blog features like RSS feeds.","zenpress")}),(0,p.jsx)(a.Button,{variant:"secondary",onClick:()=>_("blog"),__next40pxDefaultSize:!0,children:(0,r.__)("Enable","zenpress")}),(0,p.jsx)("hr",{}),(0,p.jsxs)("h3",{children:["🛒 ",(0,r.__)("E-commerce","zenpress")]}),(0,p.jsx)("p",{children:(0,r.__)("Designed for WooCommerce stores. Includes all performance and security features plus WooCommerce-specific optimizations for faster checkout.","zenpress")}),(0,p.jsx)(a.Button,{variant:"secondary",onClick:()=>_("ecommerce"),__next40pxDefaultSize:!0,children:(0,r.__)("Enable","zenpress")})]})})})]})};t()(()=>{const e=document.getElementById("zenpress-settings");e&&(0,n.createRoot)(e).render((0,p.jsx)(x,{}))})})();
  • zenpress/trunk/assets/src/index.js

    r3412245 r3448585  
    88 */
    99domReady(() => {
    10     const rootEl = document.getElementById('zenpress-settings');
    11     if (!rootEl) return;
     10    const rootEl = document.getElementById('zenpress-settings');
     11    if (!rootEl) {
     12        return;
     13    }
    1214
    13     const root = createRoot(rootEl);
    14     root.render(<SettingsPage />);
     15    const root = createRoot(rootEl);
     16    root.render(<SettingsPage />);
    1517});
  • zenpress/trunk/assets/src/js/components/Notices.js

    r3412245 r3448585  
    99 */
    1010export const Notices = () => {
    11     const { removeNotice } = useDispatch(noticesStore);
    12     const notices = useSelect((select) => select(noticesStore).getNotices(), []);
     11    const { removeNotice } = useDispatch(noticesStore);
     12    const notices = useSelect(
     13        (select) => select(noticesStore).getNotices(),
     14        []
     15    );
    1316
    14     if (!notices || notices.length === 0) {
    15         return null;
    16     }
     17    if (!notices || notices.length === 0) {
     18        return null;
     19    }
    1720
    18     return <NoticeList notices={notices} onRemove={removeNotice} />;
     21    return <NoticeList notices={notices} onRemove={removeNotice} />;
    1922};
  • zenpress/trunk/assets/src/js/components/SaveButton.js

    r3412245 r3448585  
    1111 */
    1212export const SaveButton = ({ onClick, isBusy }) => {
    13     return (
    14         <Button variant="primary" onClick={onClick} isBusy={isBusy} __next40pxDefaultSize>
    15             {__('Save settings', 'zenpress')}
    16         </Button>
    17     );
     13    return (
     14        <Button
     15            variant="primary"
     16            onClick={onClick}
     17            isBusy={isBusy}
     18            __next40pxDefaultSize
     19        >
     20            {__('Save settings', 'zenpress')}
     21        </Button>
     22    );
    1823};
  • zenpress/trunk/assets/src/js/components/SnippetToggleControl.js

    r3412245 r3448585  
    77 *
    88 * @param {HTMLElement} container - Container element to attach listener to.
    9  * @param {Function} onChange - Change handler to call when Enter is pressed.
     9 * @param {Function}    onChange - Change handler to call when Enter is pressed.
    1010 */
    1111const addEnterKeySupport = (container, onChange) => {
    12     if (!container) return;
     12    if (!container) {
     13        return;
     14    }
    1315
    14     const handleKeyDown = (e) => {
    15         // Handle Enter key on the toggle control
    16         if (e.key === 'Enter') {
    17             const toggleInput = container.querySelector('input[type="checkbox"]');
    18             if (toggleInput && (document.activeElement === toggleInput || container.contains(document.activeElement))) {
    19                 e.preventDefault();
    20                 e.stopPropagation();
    21                 onChange();
    22             }
    23         }
    24     };
     16    const handleKeyDown = (e) => {
     17        // Handle Enter key on the toggle control
     18        if (e.key === 'Enter') {
     19            const toggleInput = container.querySelector(
     20                'input[type="checkbox"]'
     21            );
     22            const ownerDocument = container.ownerDocument || document;
     23            if (
     24                toggleInput &&
     25                (ownerDocument.activeElement === toggleInput ||
     26                    container.contains(ownerDocument.activeElement))
     27            ) {
     28                e.preventDefault();
     29                e.stopPropagation();
     30                onChange();
     31            }
     32        }
     33    };
    2534
    26     container.addEventListener('keydown', handleKeyDown);
    27     return () => {
    28         container.removeEventListener('keydown', handleKeyDown);
    29     };
     35    container.addEventListener('keydown', handleKeyDown);
     36    return () => {
     37        container.removeEventListener('keydown', handleKeyDown);
     38    };
    3039};
    3140
     
    4453 */
    4554export const SnippetToggleControl = ({ label, value, onChange, help }) => {
    46     const containerRef = useRef(null);
     55    const containerRef = useRef(null);
    4756
    48     // Temporary support for Enter key handling
    49     useEffect(() => {
    50         return addEnterKeySupport(containerRef.current, onChange);
    51     }, [onChange]);
     57    // Temporary support for Enter key handling
     58    useEffect(() => {
     59        return addEnterKeySupport(containerRef.current, onChange);
     60    }, [onChange]);
    5261
    53     return (
    54         <div ref={containerRef}>
    55             <ToggleControl label={label} checked={value} onChange={onChange} help={help} __nextHasNoMarginBottom />
    56         </div>
    57     );
     62    return (
     63        <div ref={containerRef}>
     64            <ToggleControl
     65                label={label}
     66                checked={value}
     67                onChange={onChange}
     68                help={help}
     69                __nextHasNoMarginBottom
     70            />
     71        </div>
     72    );
    5873};
  • zenpress/trunk/assets/src/js/components/Tabs.js

    r3412245 r3448585  
    1 import { useState, createContext, useContext, useRef, useEffect } from '@wordpress/element';
     1import {
     2    useState,
     3    createContext,
     4    useContext,
     5    useRef,
     6    useEffect,
     7} from '@wordpress/element';
    28
    39const TabsContext = createContext();
     
    612 * Custom Tabs component with vertical orientation support
    713 *
    8  * @param {Object}   props                - Component props.
    9  * @param {string}   props.selectedTabId  - Currently selected tab ID.
    10  * @param {Function} props.onSelect       - Callback when tab is selected.
    11  * @param {string}   props.orientation    - Tab orientation ('vertical' or 'horizontal').
    12  * @param {Object}   props.children       - Child components (TabList and TabPanels).
     14 * @param {Object}   props               - Component props.
     15 * @param {string}   props.selectedTabId - Currently selected tab ID.
     16 * @param {Function} props.onSelect      - Callback when tab is selected.
     17 * @param {string}   props.orientation   - Tab orientation ('vertical' or 'horizontal').
     18 * @param {Object}   props.children      - Child components (TabList and TabPanels).
    1319 * @return {JSX.Element} The tabs container.
    1420 */
    15 export const Tabs = ({ selectedTabId: controlledId, onSelect, orientation = 'horizontal', children }) => {
    16     const [internalId, setInternalId] = useState();
    17     const tabListRef = useRef(null);
    18     const selectedId = controlledId !== undefined ? controlledId : internalId;
    19     const handleSelect = (id) => {
    20         if (controlledId === undefined) setInternalId(id);
    21         onSelect?.(id);
    22     };
    23 
    24     // Function to get ordered tab IDs from DOM
    25     const getOrderedTabIds = () => {
    26         if (!tabListRef.current) return [];
    27 
    28         const tabs = Array.from(tabListRef.current.querySelectorAll('[role="tab"]'));
    29         return tabs
    30             .map((tab) => {
    31                 const id = tab.getAttribute('id');
    32                 return id ? id.replace('zenpress-tab-', '') : null;
    33             })
    34             .filter(Boolean);
    35     };
    36 
    37     return (
    38         <TabsContext.Provider
    39             value={{ selectedTabId: selectedId, onSelect: handleSelect, orientation, getOrderedTabIds, tabListRef }}
    40         >
    41             <div className={`zenpress-tabs zenpress-tabs--${orientation}`}>{children}</div>
    42         </TabsContext.Provider>
    43     );
     21export const Tabs = ({
     22    selectedTabId: controlledId,
     23    onSelect,
     24    orientation = 'horizontal',
     25    children,
     26}) => {
     27    const [internalId, setInternalId] = useState();
     28    const tabListRef = useRef(null);
     29    const selectedId = controlledId !== undefined ? controlledId : internalId;
     30    const handleSelect = (id) => {
     31        if (controlledId === undefined) {
     32            setInternalId(id);
     33        }
     34        onSelect?.(id);
     35    };
     36
     37    // Function to get ordered tab IDs from DOM
     38    const getOrderedTabIds = () => {
     39        if (!tabListRef.current) {
     40            return [];
     41        }
     42
     43        const tabs = Array.from(
     44            tabListRef.current.querySelectorAll('[role="tab"]')
     45        );
     46        return tabs
     47            .map((tab) => {
     48                const id = tab.getAttribute('id');
     49                return id ? id.replace('zenpress-tab-', '') : null;
     50            })
     51            .filter(Boolean);
     52    };
     53
     54    return (
     55        <TabsContext.Provider
     56            value={{
     57                selectedTabId: selectedId,
     58                onSelect: handleSelect,
     59                orientation,
     60                getOrderedTabIds,
     61                tabListRef,
     62            }}
     63        >
     64            <div className={`zenpress-tabs zenpress-tabs--${orientation}`}>
     65                {children}
     66            </div>
     67        </TabsContext.Provider>
     68    );
    4469};
    4570
     
    4772 * TabList component - container for Tab components
    4873 *
    49  * @param {Object} props       - Component props.
     74 * @param {Object} props          - Component props.
    5075 * @param {Object} props.children - Tab components.
    5176 * @return {JSX.Element} The tab list container.
    5277 */
    5378export const TabList = ({ children }) => {
    54     const { orientation, tabListRef } = useContext(TabsContext);
    55 
    56     return (
    57         <div
    58             ref={tabListRef}
    59             className={`zenpress-tabs__list zenpress-tabs__list--${orientation}`}
    60             role="tablist"
    61             aria-orientation={orientation}
    62         >
    63             {children}
    64         </div>
    65     );
     79    const { orientation, tabListRef } = useContext(TabsContext);
     80
     81    return (
     82        <div
     83            ref={tabListRef}
     84            className={`zenpress-tabs__list zenpress-tabs__list--${orientation}`}
     85            role="tablist"
     86            aria-orientation={orientation}
     87        >
     88            {children}
     89        </div>
     90    );
    6691};
    6792
     
    6994 * Tab component - individual tab button
    7095 *
    71  * @param {Object}   props          - Component props.
    72  * @param {string}   props.tabId    - Unique identifier for the tab.
    73  * @param {string}   props.title    - Tab title (optional, uses children if not provided).
    74  * @param {string}   props.className - Additional CSS class name.
    75  * @param {Object}   props.children - Tab content.
     96 * @param {Object} props           - Component props.
     97 * @param {string} props.tabId     - Unique identifier for the tab.
     98 * @param {string} props.title     - Tab title (optional, uses children if not provided).
     99 * @param {string} props.className - Additional CSS class name.
     100 * @param {Object} props.children - Tab content.
    76101 * @return {JSX.Element} The tab button.
    77102 */
    78103export const Tab = ({ tabId, title, className = '', children }) => {
    79     const { selectedTabId, onSelect, orientation, getOrderedTabIds } = useContext(TabsContext);
    80     const isSelected = selectedTabId === tabId;
    81     const tabRef = useRef(null);
    82 
    83     // Handle keyboard navigation according to W3C ARIA pattern
    84     const handleKeyDown = (e) => {
    85         const tabIds = getOrderedTabIds();
    86         if (!tabIds || tabIds.length === 0) return;
    87 
    88         const currentIndex = tabIds.indexOf(tabId);
    89         if (currentIndex === -1) return;
    90 
    91         let targetIndex = currentIndex;
    92 
    93         // Handle arrow keys based on orientation
    94         if (orientation === 'vertical') {
    95             if (e.key === 'ArrowDown') {
    96                 e.preventDefault();
    97                 targetIndex = currentIndex < tabIds.length - 1 ? currentIndex + 1 : 0;
    98             } else if (e.key === 'ArrowUp') {
    99                 e.preventDefault();
    100                 targetIndex = currentIndex > 0 ? currentIndex - 1 : tabIds.length - 1;
    101             }
    102         } else {
    103             // Horizontal orientation
    104             if (e.key === 'ArrowRight') {
    105                 e.preventDefault();
    106                 targetIndex = currentIndex < tabIds.length - 1 ? currentIndex + 1 : 0;
    107             } else if (e.key === 'ArrowLeft') {
    108                 e.preventDefault();
    109                 targetIndex = currentIndex > 0 ? currentIndex - 1 : tabIds.length - 1;
    110             }
    111         }
    112 
    113         // Handle Home and End keys
    114         if (e.key === 'Home') {
    115             e.preventDefault();
    116             targetIndex = 0;
    117         } else if (e.key === 'End') {
    118             e.preventDefault();
    119             targetIndex = tabIds.length - 1;
    120         }
    121 
    122         // Handle Space and Enter for activation (if not auto-activated)
    123         if (e.key === ' ' || e.key === 'Enter') {
    124             e.preventDefault();
    125             onSelect(tabId);
    126             return;
    127         }
    128 
    129         // Move focus to target tab if index changed
    130         if (targetIndex !== currentIndex && targetIndex >= 0 && targetIndex < tabIds.length) {
    131             const targetTabId = tabIds[targetIndex];
    132             const targetTabElement = document.getElementById(`zenpress-tab-${targetTabId}`);
    133             if (targetTabElement) {
    134                 targetTabElement.focus();
    135                 // Auto-activate on focus (recommended by W3C for better UX)
    136                 onSelect(targetTabId);
    137             }
    138         }
    139     };
    140 
    141     // Auto-activate tab when it receives focus (recommended by W3C)
    142     const handleFocus = () => {
    143         if (!isSelected) {
    144             onSelect(tabId);
    145         }
    146     };
    147 
    148     return (
    149         <button
    150             ref={tabRef}
    151             className={`zenpress-tabs__tab ${isSelected ? 'zenpress-tabs__tab--is-active' : ''} ${className}`.trim()}
    152             role="tab"
    153             aria-selected={isSelected}
    154             aria-controls={`zenpress-tab-panel-${tabId}`}
    155             id={`zenpress-tab-${tabId}`}
    156             tabIndex={isSelected ? 0 : -1}
    157             onClick={() => onSelect(tabId)}
    158             onKeyDown={handleKeyDown}
    159             onFocus={handleFocus}
    160         >
    161             {title || children}
    162         </button>
    163     );
     104    const { selectedTabId, onSelect, orientation, getOrderedTabIds } =
     105        useContext(TabsContext);
     106    const isSelected = selectedTabId === tabId;
     107    const tabRef = useRef(null);
     108
     109    // Handle keyboard navigation according to W3C ARIA pattern
     110    const handleKeyDown = (e) => {
     111        const tabIds = getOrderedTabIds();
     112        if (!tabIds || tabIds.length === 0) {
     113            return;
     114        }
     115
     116        const currentIndex = tabIds.indexOf(tabId);
     117        if (currentIndex === -1) {
     118            return;
     119        }
     120
     121        let targetIndex = currentIndex;
     122
     123        // Handle arrow keys based on orientation
     124        if (orientation === 'vertical') {
     125            if (e.key === 'ArrowDown') {
     126                e.preventDefault();
     127                targetIndex =
     128                    currentIndex < tabIds.length - 1 ? currentIndex + 1 : 0;
     129            } else if (e.key === 'ArrowUp') {
     130                e.preventDefault();
     131                targetIndex =
     132                    currentIndex > 0 ? currentIndex - 1 : tabIds.length - 1;
     133            }
     134        } else if (e.key === 'ArrowRight') {
     135            // Horizontal orientation
     136            e.preventDefault();
     137            targetIndex =
     138                currentIndex < tabIds.length - 1 ? currentIndex + 1 : 0;
     139        } else if (e.key === 'ArrowLeft') {
     140            // Horizontal orientation
     141            e.preventDefault();
     142            targetIndex =
     143                currentIndex > 0 ? currentIndex - 1 : tabIds.length - 1;
     144        }
     145
     146        // Handle Home and End keys
     147        if (e.key === 'Home') {
     148            e.preventDefault();
     149            targetIndex = 0;
     150        } else if (e.key === 'End') {
     151            e.preventDefault();
     152            targetIndex = tabIds.length - 1;
     153        }
     154
     155        // Handle Space and Enter for activation (if not auto-activated)
     156        if (e.key === ' ' || e.key === 'Enter') {
     157            e.preventDefault();
     158            onSelect(tabId);
     159            return;
     160        }
     161
     162        // Move focus to target tab if index changed
     163        if (
     164            targetIndex !== currentIndex &&
     165            targetIndex >= 0 &&
     166            targetIndex < tabIds.length
     167        ) {
     168            const targetTabId = tabIds[targetIndex];
     169            const targetTabElement = document.getElementById(
     170                `zenpress-tab-${targetTabId}`
     171            );
     172            if (targetTabElement) {
     173                targetTabElement.focus();
     174                // Auto-activate on focus (recommended by W3C for better UX)
     175                onSelect(targetTabId);
     176            }
     177        }
     178    };
     179
     180    // Auto-activate tab when it receives focus (recommended by W3C)
     181    const handleFocus = () => {
     182        if (!isSelected) {
     183            onSelect(tabId);
     184        }
     185    };
     186
     187    return (
     188        <button
     189            ref={tabRef}
     190            className={`zenpress-tabs__tab ${isSelected ? 'zenpress-tabs__tab--is-active' : ''} ${className}`.trim()}
     191            role="tab"
     192            aria-selected={isSelected}
     193            aria-controls={`zenpress-tab-panel-${tabId}`}
     194            id={`zenpress-tab-${tabId}`}
     195            tabIndex={isSelected ? 0 : -1}
     196            onClick={() => onSelect(tabId)}
     197            onKeyDown={handleKeyDown}
     198            onFocus={handleFocus}
     199        >
     200            {title || children}
     201        </button>
     202    );
    164203};
    165204
     
    167206 * TabPanel component - container for tab content
    168207 *
    169  * @param {Object}   props          - Component props.
    170  * @param {string}   props.tabId    - Unique identifier matching a Tab's tabId.
    171  * @param {Object}   props.children - Panel content.
     208 * @param {Object} props          - Component props.
     209 * @param {string} props.tabId    - Unique identifier matching a Tab's tabId.
     210 * @param {Object} props.children - Panel content.
    172211 * @return {JSX.Element|null} The tab panel or null if not selected.
    173212 */
    174213export const TabPanel = ({ tabId, children }) => {
    175     const { selectedTabId } = useContext(TabsContext);
    176     const panelRef = useRef(null);
    177     const isSelected = selectedTabId === tabId;
    178 
    179     // Check if panel contains focusable elements
    180     useEffect(() => {
    181         if (isSelected && panelRef.current) {
    182             const focusableElements = panelRef.current.querySelectorAll(
    183                 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'
    184             );
    185 
    186             // If no focusable elements, make the panel itself focusable
    187             if (focusableElements.length === 0) {
    188                 panelRef.current.setAttribute('tabindex', '0');
    189             } else {
    190                 panelRef.current.removeAttribute('tabindex');
    191             }
    192         }
    193     }, [isSelected]);
    194 
    195     if (!isSelected) {
    196         return (
    197             <div
    198                 className="zenpress-tabs__panel"
    199                 role="tabpanel"
    200                 id={`zenpress-tab-panel-${tabId}`}
    201                 aria-labelledby={`zenpress-tab-${tabId}`}
    202                 hidden
    203             >
    204                 {children}
    205             </div>
    206         );
    207     }
    208 
    209     return (
    210         <div
    211             ref={panelRef}
    212             className="zenpress-tabs__panel"
    213             role="tabpanel"
    214             id={`zenpress-tab-panel-${tabId}`}
    215             aria-labelledby={`zenpress-tab-${tabId}`}
    216         >
    217             {children}
    218         </div>
    219     );
     214    const { selectedTabId } = useContext(TabsContext);
     215    const panelRef = useRef(null);
     216    const isSelected = selectedTabId === tabId;
     217
     218    // Check if panel contains focusable elements
     219    useEffect(() => {
     220        if (isSelected && panelRef.current) {
     221            const focusableElements = panelRef.current.querySelectorAll(
     222                'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'
     223            );
     224
     225            // If no focusable elements, make the panel itself focusable
     226            if (focusableElements.length === 0) {
     227                panelRef.current.setAttribute('tabindex', '0');
     228            } else {
     229                panelRef.current.removeAttribute('tabindex');
     230            }
     231        }
     232    }, [isSelected]);
     233
     234    if (!isSelected) {
     235        return (
     236            <div
     237                className="zenpress-tabs__panel"
     238                role="tabpanel"
     239                id={`zenpress-tab-panel-${tabId}`}
     240                aria-labelledby={`zenpress-tab-${tabId}`}
     241                hidden
     242            >
     243                {children}
     244            </div>
     245        );
     246    }
     247
     248    return (
     249        <div
     250            ref={panelRef}
     251            className="zenpress-tabs__panel"
     252            role="tabpanel"
     253            id={`zenpress-tab-panel-${tabId}`}
     254            aria-labelledby={`zenpress-tab-${tabId}`}
     255        >
     256            {children}
     257        </div>
     258    );
    220259};
    221260
  • zenpress/trunk/assets/src/js/hooks/useSettings.js

    r3412245 r3448585  
    1515 */
    1616export const useSettings = () => {
    17     const [snippets, setSnippets] = useState({});
    18     const [isSaving, setIsSaving] = useState(false);
    19     const { createSuccessNotice, createErrorNotice } = useDispatch(noticesStore);
     17    const [snippets, setSnippets] = useState({});
     18    const [isSaving, setIsSaving] = useState(false);
     19    const { createSuccessNotice, createErrorNotice } =
     20        useDispatch(noticesStore);
    2021
    21     useEffect(() => {
    22         apiFetch({ path: '/wp/v2/settings' })
    23             .then((settings) => {
    24                 const active = Array.isArray(settings?.zenpress_active_snippets)
    25                     ? settings.zenpress_active_snippets
    26                     : [];
     22    useEffect(() => {
     23        apiFetch({ path: '/wp/v2/settings' })
     24            .then((settings) => {
     25                const active = Array.isArray(settings?.zenpress_active_snippets)
     26                    ? settings.zenpress_active_snippets
     27                    : [];
    2728
    28                 const meta = window?.zenpressSnippetsMeta || {};
    29                 const snippetsData = {};
     29                const meta = window?.zenpressSnippetsMeta || {};
     30                const snippetsData = {};
    3031
    31                 Object.keys(meta).forEach((name) => {
    32                     snippetsData[name] = {
    33                         ...meta[name],
    34                         'enable-snippet': active.includes(name),
    35                     };
    36                 });
     32                Object.keys(meta).forEach((name) => {
     33                    snippetsData[name] = {
     34                        ...meta[name],
     35                        'enable-snippet': active.includes(name),
     36                    };
     37                });
    3738
    38                 setSnippets(snippetsData);
    39             })
    40             .catch(() => {
    41                 createErrorNotice(__('Failed to load settings.', 'zenpress'));
    42             });
    43     }, [createErrorNotice]);
     39                setSnippets(snippetsData);
     40            })
     41            .catch(() => {
     42                createErrorNotice(__('Failed to load settings.', 'zenpress'));
     43            });
     44    }, [createErrorNotice]);
    4445
    45     const saveSettings = async () => {
    46         setIsSaving(true);
     46    const saveSettings = async () => {
     47        setIsSaving(true);
    4748
    48         const active = Object.keys(snippets).filter((name) => snippets[name]?.['enable-snippet']);
     49        const active = Object.keys(snippets).filter(
     50            (name) => snippets[name]?.['enable-snippet']
     51        );
    4952
    50         try {
    51             await apiFetch({
    52                 path: '/wp/v2/settings',
    53                 method: 'POST',
    54                 data: { zenpress_active_snippets: active },
    55             });
    56             createSuccessNotice(__('Settings saved.', 'zenpress'));
    57         } catch {
    58             createErrorNotice(__('Failed to save settings.', 'zenpress'));
    59         } finally {
    60             setIsSaving(false);
    61         }
    62     };
     53        try {
     54            await apiFetch({
     55                path: '/wp/v2/settings',
     56                method: 'POST',
     57                data: { zenpress_active_snippets: active },
     58            });
     59            createSuccessNotice(__('Settings saved.', 'zenpress'));
     60        } catch {
     61            createErrorNotice(__('Failed to save settings.', 'zenpress'));
     62        } finally {
     63            setIsSaving(false);
     64        }
     65    };
    6366
    64     return { snippets, setSnippets, saveSettings, isSaving };
     67    return { snippets, setSnippets, saveSettings, isSaving };
    6568};
  • zenpress/trunk/assets/src/js/pages/SettingsPage.js

    r3412245 r3448585  
    1 import { useState, useEffect, useRef } from '@wordpress/element';
     1import { useState, useEffect } from '@wordpress/element';
    22import { __ } from '@wordpress/i18n';
    33import { Button } from '@wordpress/components';
     
    1414 */
    1515export const SettingsPage = () => {
    16     const { snippets, setSnippets, saveSettings, isSaving } = useSettings();
    17     const [selectedTabId, setSelectedTabId] = useState();
    18 
    19     const handleToggleChange = (snippetName) => {
    20         setSnippets((prev) => ({
    21             ...prev,
    22             [snippetName]: {
    23                 ...prev[snippetName],
    24                 'enable-snippet': !prev[snippetName]?.['enable-snippet'],
    25             },
    26         }));
    27     };
    28 
    29     const enableAllSnippets = () => {
    30         setSnippets((prev) => {
    31             const updated = {};
    32             Object.keys(prev).forEach((name) => {
    33                 updated[name] = { ...prev[name], 'enable-snippet': true };
    34             });
    35             return updated;
    36         });
    37     };
    38 
    39     const resetSettings = () => {
    40         setSnippets((prev) => {
    41             const updated = {};
    42             Object.keys(prev).forEach((name) => {
    43                 updated[name] = { ...prev[name], 'enable-snippet': false };
    44             });
    45             return updated;
    46         });
    47     };
    48 
    49     const enableByPreset = (preset) => {
    50         setSnippets((prev) => {
    51             const updated = {};
    52             Object.entries(prev).forEach(([name, data]) => {
    53                 const presets = Array.isArray(data?.preset) ? data.preset : [];
    54                 const isEnabled = presets.includes(preset);
    55                 updated[name] = { ...data, 'enable-snippet': isEnabled };
    56             });
    57             return updated;
    58         });
    59     };
    60 
    61     // Helper function to capitalize category name
    62     const capitalizeCategory = (category) => {
    63         if (!category) return category;
    64         return category.charAt(0).toUpperCase() + category.slice(1).toLowerCase();
    65     };
    66 
    67     // Category order: Core, Gutenberg, WooCommerce, ads-blocker, Tools
    68     // Using English lowercase values for comparison since categories are stored in lowercase
    69     const categoryOrder = ['core', 'gutenberg', 'woocommerce', 'ads-blocker', 'tools'];
    70 
    71     // Group snippets by category, then by subcategory
    72     const groupedSnippets = {};
    73     Object.keys(snippets).forEach((snippetName) => {
    74         const snippet = snippets[snippetName];
    75         const category = (snippet?.category || __('Uncategorized', 'zenpress')).toLowerCase();
    76         const subcategory = (snippet?.subcategory || __('uncategorized', 'zenpress')).toLowerCase();
    77 
    78         if (!groupedSnippets[category]) {
    79             groupedSnippets[category] = {};
    80         }
    81         if (!groupedSnippets[category][subcategory]) {
    82             groupedSnippets[category][subcategory] = [];
    83         }
    84         groupedSnippets[category][subcategory].push({ name: snippetName, data: snippet });
    85     });
    86 
    87     // Sort categories according to the specified order
    88     const sortedCategories = Object.keys(groupedSnippets).sort((a, b) => {
    89         const indexA = categoryOrder.indexOf(a.toLowerCase());
    90         const indexB = categoryOrder.indexOf(b.toLowerCase());
    91 
    92         // If both are in the order array, sort by their position
    93         if (indexA !== -1 && indexB !== -1) {
    94             return indexA - indexB;
    95         }
    96         // If only A is in the order array, A comes first
    97         if (indexA !== -1) return -1;
    98         // If only B is in the order array, B comes first
    99         if (indexB !== -1) return 1;
    100         // If neither is in the order array, sort alphabetically
    101         return a.localeCompare(b, undefined, { sensitivity: 'base' });
    102     });
    103 
    104     // Set initial selected tab if none is selected
    105     useEffect(() => {
    106         if (!selectedTabId && sortedCategories.length > 0) {
    107             setSelectedTabId(sortedCategories[0]);
    108         }
    109     }, [selectedTabId, sortedCategories.length]);
    110 
    111     const onSelect = (tabName) => {
    112         setSelectedTabId(tabName);
    113     };
    114 
    115     // Add keyboard shortcuts and ensure toggles are keyboard accessible
    116     useEffect(() => {
    117         const handleKeyDown = (e) => {
    118             // Ctrl+S or Cmd+S to save
    119             if ((e.ctrlKey || e.metaKey) && e.key === 's') {
    120                 e.preventDefault();
    121                 if (!isSaving) {
    122                     saveSettings();
    123                 }
    124             }
    125         };
    126 
    127         document.addEventListener('keydown', handleKeyDown);
    128         return () => {
    129             document.removeEventListener('keydown', handleKeyDown);
    130         };
    131     }, [saveSettings, isSaving]);
    132 
    133     return (
    134         <article className="zenpress-row">
    135             <section className="zenpress-main">
    136                 <div className="zenpress-notices">
    137                     <Notices />
    138                 </div>
    139                 <div className="zenpress-panel">
    140                     <Tabs
    141                         orientation="vertical"
    142                         selectedTabId={selectedTabId}
    143                         onSelect={(selectedId) => {
    144                             setSelectedTabId(selectedId);
    145                             onSelect(selectedId);
    146                         }}
    147                     >
    148                         <Tabs.TabList>
    149                             {sortedCategories.map((category) => {
    150                                 const categoryClass = `zenpress-tabs__tab--category-${category.toLowerCase().replace(/\s+/g, '-')}`;
    151                                 return (
    152                                     <Tabs.Tab
    153                                         key={category}
    154                                         tabId={category}
    155                                         title={capitalizeCategory(category)}
    156                                         className={categoryClass}
    157                                     >
    158                                         {capitalizeCategory(category)}
    159                                     </Tabs.Tab>
    160                                 );
    161                             })}
    162                         </Tabs.TabList>
    163                         {sortedCategories.map((category) => {
    164                             const subcategories = Object.keys(groupedSnippets[category]).sort();
    165                             return (
    166                                 <Tabs.TabPanel key={category} tabId={category}>
    167                                     <h2>{capitalizeCategory(category)}</h2>
    168                                     {subcategories.map((subcategory) => (
    169                                         <div
    170                                             key={subcategory}
    171                                             className={`zenpress-subcategory zenpress-subcategory-${subcategory.toLowerCase().replace(/\s+/g, '-')}`}
    172                                         >
    173                                             <hr />
    174                                             <h3>{capitalizeCategory(subcategory)}</h3>
    175                                             {groupedSnippets[category][subcategory].map(({ name, data }) => (
    176                                                 <SnippetToggleControl
    177                                                     key={name}
    178                                                     label={data.title || name}
    179                                                     value={data?.['enable-snippet'] || false}
    180                                                     onChange={() => handleToggleChange(name)}
    181                                                     help={data.description || ''}
    182                                                 />
    183                                             ))}
    184                                         </div>
    185                                     ))}
    186                                 </Tabs.TabPanel>
    187                             );
    188                         })}
    189                     </Tabs>
    190                     <div className="zenpress-actions">
    191                         <div className="zenpress-actions-bulk">
    192                             <Button variant="tertiary" onClick={enableAllSnippets} __next40pxDefaultSize>
    193                                 {__('Enable all actions', 'zenpress')}
    194                             </Button>
    195                             <Button isDestructive onClick={resetSettings} __next40pxDefaultSize>
    196                                 {__('Disable all actions', 'zenpress')}
    197                             </Button>
    198                         </div>
    199                         <SaveButton onClick={saveSettings} isBusy={isSaving} />
    200                     </div>
    201                 </div>
    202             </section>
    203             <aside className="zenpress-sidebar">
    204                 <div className="zenpress-presets">
    205                     <div className="zenpress-presets-description">
    206                         <h2>{__('Pick configuration preset', 'zenpress')}</h2>
    207                         <p>
    208                             {__(
    209                                 "Don't know which features to enable? Quickly configure ZenPress by selecting a preset that matches your site type. Each preset enables optimized features for your specific use case.",
    210                                 'zenpress'
    211                             )}
    212                         </p>
    213                         <hr />
    214                         <h3>🖼️ {__('Corporate website', 'zenpress')}</h3>
    215                         <p>
    216                             {__(
    217                                 'Optimized for business sites and portfolios. Focuses on security, performance, and removing unnecessary features like RSS feeds and author archives.',
    218                                 'zenpress'
    219                             )}
    220                         </p>
    221                         <Button
    222                             variant="secondary"
    223                             onClick={() => enableByPreset('corporate-website')}
    224                             __next40pxDefaultSize
    225                         >
    226                             {__('Enable', 'zenpress')}
    227                         </Button>
    228                         <hr />
    229                         <h3> 📰 {__('Blog', 'zenpress')}</h3>
    230                         <p>
    231                             {__(
    232                                 'Tailored for content-focused blogs. Includes performance and security optimizations while preserving essential blog features like RSS feeds.',
    233                                 'zenpress'
    234                             )}
    235                         </p>
    236                         <Button variant="secondary" onClick={() => enableByPreset('blog')} __next40pxDefaultSize>
    237                             {__('Enable', 'zenpress')}
    238                         </Button>
    239                         <hr />
    240                         <h3>🛒 {__('E-commerce', 'zenpress')}</h3>
    241                         <p>
    242                             {__(
    243                                 'Designed for WooCommerce stores. Includes all performance and security features plus WooCommerce-specific optimizations for faster checkout.',
    244                                 'zenpress'
    245                             )}
    246                         </p>
    247                         <Button variant="secondary" onClick={() => enableByPreset('ecommerce')} __next40pxDefaultSize>
    248                             {__('Enable', 'zenpress')}
    249                         </Button>
    250                     </div>
    251                 </div>
    252             </aside>
    253         </article>
    254     );
     16    const { snippets, setSnippets, saveSettings, isSaving } = useSettings();
     17    const [selectedTabId, setSelectedTabId] = useState();
     18
     19    const handleToggleChange = (snippetName) => {
     20        setSnippets((prev) => ({
     21            ...prev,
     22            [snippetName]: {
     23                ...prev[snippetName],
     24                'enable-snippet': !prev[snippetName]?.['enable-snippet'],
     25            },
     26        }));
     27    };
     28
     29    const enableAllSnippets = () => {
     30        setSnippets((prev) => {
     31            const updated = {};
     32            Object.keys(prev).forEach((name) => {
     33                updated[name] = { ...prev[name], 'enable-snippet': true };
     34            });
     35            return updated;
     36        });
     37    };
     38
     39    const resetSettings = () => {
     40        setSnippets((prev) => {
     41            const updated = {};
     42            Object.keys(prev).forEach((name) => {
     43                updated[name] = { ...prev[name], 'enable-snippet': false };
     44            });
     45            return updated;
     46        });
     47    };
     48
     49    const enableByPreset = (preset) => {
     50        setSnippets((prev) => {
     51            const updated = {};
     52            Object.entries(prev).forEach(([name, data]) => {
     53                const presets = Array.isArray(data?.preset) ? data.preset : [];
     54                const isEnabled = presets.includes(preset);
     55                updated[name] = { ...data, 'enable-snippet': isEnabled };
     56            });
     57            return updated;
     58        });
     59    };
     60
     61    // Helper function to capitalize category name
     62    const capitalizeCategory = (category) => {
     63        if (!category) {
     64            return category;
     65        }
     66        return (
     67            category.charAt(0).toUpperCase() + category.slice(1).toLowerCase()
     68        );
     69    };
     70
     71    // Category order: Core, Gutenberg, WooCommerce, ads-blocker, Tools
     72    // Using English lowercase values for comparison since categories are stored in lowercase
     73    const categoryOrder = [
     74        'core',
     75        'gutenberg',
     76        'woocommerce',
     77        'ads-blocker',
     78        'tools',
     79    ];
     80
     81    // Group snippets by category, then by subcategory
     82    const groupedSnippets = {};
     83    Object.keys(snippets).forEach((snippetName) => {
     84        const snippet = snippets[snippetName];
     85        const category = (
     86            snippet?.category || __('Uncategorized', 'zenpress')
     87        ).toLowerCase();
     88        const subcategory = (
     89            snippet?.subcategory || __('uncategorized', 'zenpress')
     90        ).toLowerCase();
     91
     92        if (!groupedSnippets[category]) {
     93            groupedSnippets[category] = {};
     94        }
     95        if (!groupedSnippets[category][subcategory]) {
     96            groupedSnippets[category][subcategory] = [];
     97        }
     98        groupedSnippets[category][subcategory].push({
     99            name: snippetName,
     100            data: snippet,
     101        });
     102    });
     103
     104    // Sort categories according to the specified order
     105    const sortedCategories = Object.keys(groupedSnippets).sort((a, b) => {
     106        const indexA = categoryOrder.indexOf(a.toLowerCase());
     107        const indexB = categoryOrder.indexOf(b.toLowerCase());
     108
     109        // If both are in the order array, sort by their position
     110        if (indexA !== -1 && indexB !== -1) {
     111            return indexA - indexB;
     112        }
     113        // If only A is in the order array, A comes first
     114        if (indexA !== -1) {
     115            return -1;
     116        }
     117        // If only B is in the order array, B comes first
     118        if (indexB !== -1) {
     119            return 1;
     120        }
     121        // If neither is in the order array, sort alphabetically
     122        return a.localeCompare(b, undefined, { sensitivity: 'base' });
     123    });
     124
     125    // Set initial selected tab if none is selected
     126    useEffect(() => {
     127        if (!selectedTabId && sortedCategories.length > 0) {
     128            setSelectedTabId(sortedCategories[0]);
     129        }
     130    }, [selectedTabId, sortedCategories]);
     131
     132    const onSelect = (tabName) => {
     133        setSelectedTabId(tabName);
     134    };
     135
     136    // Add keyboard shortcuts and ensure toggles are keyboard accessible
     137    useEffect(() => {
     138        const handleKeyDown = (e) => {
     139            // Ctrl+S or Cmd+S to save
     140            if ((e.ctrlKey || e.metaKey) && e.key === 's') {
     141                e.preventDefault();
     142                if (!isSaving) {
     143                    saveSettings();
     144                }
     145            }
     146        };
     147
     148        document.addEventListener('keydown', handleKeyDown);
     149        return () => {
     150            document.removeEventListener('keydown', handleKeyDown);
     151        };
     152    }, [saveSettings, isSaving]);
     153
     154    return (
     155        <article className="zenpress-row">
     156            <section className="zenpress-main">
     157                <div className="zenpress-notices">
     158                    <Notices />
     159                </div>
     160                <div className="zenpress-panel">
     161                    <Tabs
     162                        orientation="vertical"
     163                        selectedTabId={selectedTabId}
     164                        onSelect={(selectedId) => {
     165                            setSelectedTabId(selectedId);
     166                            onSelect(selectedId);
     167                        }}
     168                    >
     169                        <Tabs.TabList>
     170                            {sortedCategories.map((category) => {
     171                                const categoryClass = `zenpress-tabs__tab--category-${category.toLowerCase().replace(/\s+/g, '-')}`;
     172                                return (
     173                                    <Tabs.Tab
     174                                        key={category}
     175                                        tabId={category}
     176                                        title={capitalizeCategory(category)}
     177                                        className={categoryClass}
     178                                    >
     179                                        {capitalizeCategory(category)}
     180                                    </Tabs.Tab>
     181                                );
     182                            })}
     183                        </Tabs.TabList>
     184                        {sortedCategories.map((category) => {
     185                            const subcategories = Object.keys(
     186                                groupedSnippets[category]
     187                            ).sort();
     188                            return (
     189                                <Tabs.TabPanel key={category} tabId={category}>
     190                                    <h2>{capitalizeCategory(category)}</h2>
     191                                    {subcategories.map((subcategory) => (
     192                                        <div
     193                                            key={subcategory}
     194                                            className={`zenpress-subcategory zenpress-subcategory-${subcategory.toLowerCase().replace(/\s+/g, '-')}`}
     195                                        >
     196                                            <hr />
     197                                            <h3>
     198                                                {capitalizeCategory(
     199                                                    subcategory
     200                                                )}
     201                                            </h3>
     202                                            {groupedSnippets[category][
     203                                                subcategory
     204                                            ].map(({ name, data }) => (
     205                                                <SnippetToggleControl
     206                                                    key={name}
     207                                                    label={data.title || name}
     208                                                    value={
     209                                                        data?.[
     210                                                            'enable-snippet'
     211                                                        ] || false
     212                                                    }
     213                                                    onChange={() =>
     214                                                        handleToggleChange(name)
     215                                                    }
     216                                                    help={
     217                                                        data.description || ''
     218                                                    }
     219                                                />
     220                                            ))}
     221                                        </div>
     222                                    ))}
     223                                </Tabs.TabPanel>
     224                            );
     225                        })}
     226                    </Tabs>
     227                    <div className="zenpress-actions">
     228                        <div className="zenpress-actions-bulk">
     229                            <Button
     230                                variant="tertiary"
     231                                onClick={enableAllSnippets}
     232                                __next40pxDefaultSize
     233                            >
     234                                {__('Enable all actions', 'zenpress')}
     235                            </Button>
     236                            <Button
     237                                isDestructive
     238                                onClick={resetSettings}
     239                                __next40pxDefaultSize
     240                            >
     241                                {__('Disable all actions', 'zenpress')}
     242                            </Button>
     243                        </div>
     244                        <SaveButton onClick={saveSettings} isBusy={isSaving} />
     245                    </div>
     246                </div>
     247            </section>
     248            <aside className="zenpress-sidebar">
     249                <div className="zenpress-presets">
     250                    <div className="zenpress-presets-description">
     251                        <h2>{__('Pick configuration preset', 'zenpress')}</h2>
     252                        <p>
     253                            {__(
     254                                "Don't know which features to enable? Quickly configure ZenPress by selecting a preset that matches your site type. Each preset enables optimized features for your specific use case.",
     255                                'zenpress'
     256                            )}
     257                        </p>
     258                        <hr />
     259                        <h3>🖼️ {__('Corporate website', 'zenpress')}</h3>
     260                        <p>
     261                            {__(
     262                                'Optimized for business sites and portfolios. Focuses on security, performance, and removing unnecessary features like RSS feeds and author archives.',
     263                                'zenpress'
     264                            )}
     265                        </p>
     266                        <Button
     267                            variant="secondary"
     268                            onClick={() => enableByPreset('corporate-website')}
     269                            __next40pxDefaultSize
     270                        >
     271                            {__('Enable', 'zenpress')}
     272                        </Button>
     273                        <hr />
     274                        <h3> 📰 {__('Blog', 'zenpress')}</h3>
     275                        <p>
     276                            {__(
     277                                'Tailored for content-focused blogs. Includes performance and security optimizations while preserving essential blog features like RSS feeds.',
     278                                'zenpress'
     279                            )}
     280                        </p>
     281                        <Button
     282                            variant="secondary"
     283                            onClick={() => enableByPreset('blog')}
     284                            __next40pxDefaultSize
     285                        >
     286                            {__('Enable', 'zenpress')}
     287                        </Button>
     288                        <hr />
     289                        <h3>🛒 {__('E-commerce', 'zenpress')}</h3>
     290                        <p>
     291                            {__(
     292                                'Designed for WooCommerce stores. Includes all performance and security features plus WooCommerce-specific optimizations for faster checkout.',
     293                                'zenpress'
     294                            )}
     295                        </p>
     296                        <Button
     297                            variant="secondary"
     298                            onClick={() => enableByPreset('ecommerce')}
     299                            __next40pxDefaultSize
     300                        >
     301                            {__('Enable', 'zenpress')}
     302                        </Button>
     303                    </div>
     304                </div>
     305            </aside>
     306        </article>
     307    );
    255308};
  • zenpress/trunk/inc/admin/enqueue.php

    r3372200 r3448585  
    2323
    2424    $asset = include $asset_file;
     25    if (!is_array($asset) || empty($asset['dependencies']) || empty($asset['version'])) {
     26        return;
     27    }
     28
    2529    wp_enqueue_script(
    2630        'zenpress-scripts',
    2731        plugins_url('assets/build/index.js', ZENPRESS_PLUGIN_FILE),
    28         $asset['dependencies'],
     32        (array) $asset['dependencies'],
    2933        $asset['version'],
    3034        true
     
    3539        plugins_url('assets/build/index.css', ZENPRESS_PLUGIN_FILE),
    3640        array_filter(
    37             $asset['dependencies'],
    38             function ($style) {
     41            (array) $asset['dependencies'],
     42            static function (string $style): bool {
    3943                return wp_style_is($style, 'registered');
    4044            }
     
    6266    }
    6367
    64     foreach (glob($snippets_path . '*.php') as $file) {
     68    $files = glob($snippets_path . '*.php') ?: [];
     69    foreach ($files as $file) {
    6570        $basename = basename($file, '.php');
    6671        $snippets[$basename] = zenpress_extract_snippet_metadata($basename);
  • zenpress/trunk/inc/core/constants.php

    r3372200 r3448585  
    1010
    1111if (!defined('ZENPRESS_PLUGIN_FILE')) {
    12     define(
    13         'ZENPRESS_PLUGIN_FILE',
    14         __FILE__ === realpath(__FILE__)
    15         ? __FILE__ // fallback
    16         : dirname(__DIR__, 1) . '/zenpress.php'
    17     );
     12    $plugin_file = dirname(__DIR__, 2) . '/zenpress.php';
     13    define('ZENPRESS_PLUGIN_FILE', is_file($plugin_file) ? $plugin_file : __FILE__);
    1814}
    1915
  • zenpress/trunk/inc/core/metadata.php

    r3412245 r3448585  
    1818        'subcategory' => '',
    1919        'weight' => 0,
    20         'preset' => []
     20        'preset' => [],
    2121    ];
    2222
     
    3131        'subcategory' => sanitize_text_field($metadata['subcategory']),
    3232        'weight' => (int) $metadata['weight'],
    33         'preset' => array_map('sanitize_text_field', (array) $metadata['preset'])
     33        'preset' => array_map('sanitize_text_field', (array) $metadata['preset']),
    3434    ];
    3535}
  • zenpress/trunk/inc/core/sanitize.php

    r3372200 r3448585  
    1111 * @return array<string> Sanitized base names.
    1212 */
    13 function zenpress_sanitize_snippets_option($value): array {
     13function zenpress_sanitize_snippets_option(mixed $value): array {
    1414    return array_values(
    1515        array_filter(
  • zenpress/trunk/inc/settings/loader.php

    r3372200 r3448585  
    1212 */
    1313function zenpress_load_snippets(string $folder = 'inc/snippets/functions/'): array {
     14    // Prevent path traversal if $folder is ever passed from external input.
     15    if (str_contains($folder, '..')) {
     16        return [];
     17    }
     18
    1419    $path = ZENPRESS_PLUGIN_DIR . rtrim($folder, '/') . '/';
    1520
     
    3641 *  BOOT ZENPRESS PLUGIN
    3742 **************************************/
    38 add_action('init', function() : void {
     43add_action('init', static function (): void {
    3944    zenpress_load_snippets();
    4045});
  • zenpress/trunk/inc/settings/options.php

    r3372200 r3448585  
    2020                'schema' => [
    2121                    'type' => 'array',
    22                     'items' => ['type' => 'string']
    23                 ]
    24             ]
     22                    'items' => ['type' => 'string'],
     23                ],
     24            ],
    2525        ]
    2626    );
  • zenpress/trunk/inc/snippets/functions/block-user-enumeration.php

    r3372200 r3448585  
    77if (!is_admin()) {
    88    // Block enumeration via query string (?author=1).
    9     if (
    10         isset($_SERVER['QUERY_STRING']) &&
    11         preg_match('/author=([0-9]+)/i', sanitize_text_field(wp_unslash($_SERVER['QUERY_STRING'])))
    12     ) {
     9    $query_string = $_SERVER['QUERY_STRING'] ?? '';
     10    if (preg_match('/author=([0-9]+)/i', sanitize_text_field(wp_unslash($query_string)))) {
    1311        wp_die(esc_html__('Access denied.', 'zenpress'), '', ['response' => 403]);
    1412    }
     
    1715    add_filter(
    1816        'redirect_canonical',
    19         static function ($redirect, $request) {
     17        static function (string|false $redirect, string $request): string|false {
    2018            if (preg_match('/\?author=([0-9]+)(\/*)/i', sanitize_text_field(wp_unslash($request)))) {
    2119                wp_die(esc_html__('Access denied.', 'zenpress'), '', ['response' => 403]);
  • zenpress/trunk/inc/snippets/functions/clean-admin-bar.php

    r3372200 r3448585  
    77add_action(
    88    'admin_bar_menu',
    9     static function ($wp_admin_bar) {
    10         if (!($wp_admin_bar instanceof WP_Admin_Bar)) {
    11             return;
    12         }
    13 
     9    static function (WP_Admin_Bar $wp_admin_bar): void {
    1410        /**
    1511         * Backend clean-up.
  • zenpress/trunk/inc/snippets/functions/clean-dashboard-items.php

    r3372200 r3448585  
    77add_action(
    88    'wp_dashboard_setup',
    9     static function () {
     9    static function (): void {
    1010        /**
    1111         * Core widgets.
  • zenpress/trunk/inc/snippets/functions/disable-author-archives.php

    r3372200 r3448585  
    88remove_filter('template_redirect', 'redirect_canonical');
    99
    10 add_action('template_redirect', static function () {
     10add_action('template_redirect', static function (): void {
    1111    if (is_author()) {
    1212        global $wp_query;
  • zenpress/trunk/inc/snippets/functions/disable-dashicons.php

    r3372200 r3448585  
    55}
    66
    7 add_action('wp_enqueue_scripts', static function () {
     7add_action('wp_enqueue_scripts', static function (): void {
    88    if (!is_user_logged_in()) {
    99        wp_dequeue_style('dashicons');
  • zenpress/trunk/inc/snippets/functions/disable-default-pattern-categories.php

    r3412245 r3448585  
    1111 * simplifying the interface and reducing clutter.
    1212 */
    13 add_action('init', static function() {
     13add_action('init', static function (): void {
    1414    if (!class_exists('WP_Block_Pattern_Categories_Registry')) {
    1515        return;
  • zenpress/trunk/inc/snippets/functions/disable-emoji-scripts.php

    r3372200 r3448585  
    2222
    2323// Remove emoji support from TinyMCE editor.
    24 add_filter('tiny_mce_plugins', static function ($plugins) {
     24add_filter('tiny_mce_plugins', static function (array $plugins): array {
    2525    return array_diff($plugins, ['wpemoji']);
    2626});
  • zenpress/trunk/inc/snippets/functions/disable-jquery-migrate.php

    r3372200 r3448585  
    55}
    66
    7 add_action('wp_default_scripts', static function (&$scripts) {
     7add_action('wp_default_scripts', static function (WP_Scripts $scripts): void {
    88    if (!is_admin() && isset($scripts->registered['jquery'])) {
    99        $script = $scripts->registered['jquery'];
  • zenpress/trunk/inc/snippets/functions/disable-oembed.php

    r3372200 r3448585  
    55}
    66
    7 add_action('init', static function () {
     7add_action('init', static function (): void {
    88    // Remove oEmbed from query vars.
    99    global $wp;
     
    2525
    2626    // Remove the wpembed TinyMCE plugin.
    27     add_filter('tiny_mce_plugins', function ($plugins) {
     27    add_filter('tiny_mce_plugins', static function (array $plugins): array {
    2828        return array_diff($plugins, ['wpembed']);
    2929    });
    3030
    3131    // Remove embed-related rewrite rules.
    32     add_filter('rewrite_rules_array', function ($rules) {
     32    add_filter('rewrite_rules_array', static function (array $rules): array {
    3333        foreach ($rules as $rule => $rewrite) {
    34             if (strpos($rewrite, 'embed=true') !== false) {
     34            if (str_contains($rewrite, 'embed=true')) {
    3535                unset($rules[$rule]);
    3636            }
  • zenpress/trunk/inc/snippets/functions/disable-pdf-thumbnails.php

    r3372200 r3448585  
    55}
    66
    7 add_filter('fallback_intermediate_image_sizes', static function () {
     7add_filter('fallback_intermediate_image_sizes', static function (): array {
    88    return [];
    99});
  • zenpress/trunk/inc/snippets/functions/disable-pingback-trackback.php

    r3372200 r3448585  
    66
    77// Remove X-Pingback header from HTTP responses.
    8 add_filter('wp_headers', static function ($headers) {
     8add_filter('wp_headers', static function (array $headers): array {
    99    unset($headers['X-Pingback']);
    1010
     
    1313
    1414// Disable pingbacks and trackbacks for new posts.
    15 add_action('after_setup_theme', static function () {
     15add_action('after_setup_theme', static function (): void {
    1616    update_option('default_ping_status', 'closed');
    1717    update_option('default_pingback_flag', 0);
     
    1919
    2020// Prevent self-pingbacks.
    21 add_action('pre_ping', static function (&$links) {
     21add_action('pre_ping', static function (array &$links): void {
    2222    $home = get_option('home');
    2323    foreach ($links as $l => $link) {
    24         if (strpos($link, $home) === 0) {
     24        if (str_starts_with($link, $home)) {
    2525            unset($links[$l]);
    2626        }
  • zenpress/trunk/inc/snippets/functions/disable-rest-api.php

    r3412245 r3448585  
    1414remove_action('xmlrpc_rsd_apis', 'rest_output_rsd');
    1515
    16 // Disable REST API
    17 if (version_compare(get_bloginfo('version'), '4.7', '>=')) {
    18     add_filter('rest_authentication_errors', 'zenpress_disable_wp_rest_api');
    19 } else {
    20     zenpress_disable_wp_rest_api_legacy();
    21 }
    22 
    23 function zenpress_disable_wp_rest_api($access) {
    24     if (!is_user_logged_in() && !zenpress_disable_wp_rest_api_allow_access()) {
    25         $message = apply_filters('zenpress_disable_wp_rest_api_error', __('REST API restricted to authenticated users.', 'zenpress'));
    26 
    27         return new WP_Error('rest_login_required', $message, ['status' => rest_authorization_required_code()]);
    28     }
    29 
    30     return $access;
    31 }
    32 
    33 function zenpress_disable_wp_rest_api_allow_access() {
     16/**
     17 * Check whether to allow unauthenticated REST API access (bypass).
     18 *
     19 * Filters zenpress_disable_wp_rest_api_post_var and zenpress_disable_wp_rest_api_server_var
     20 * let site owners allow specific POST keys or REQUEST_URI values for webhooks/third-party
     21 * integrations. Security note: only use values that are secret or not guessable (e.g. a
     22 * random token in POST, or a unique path). Using predictable values can re-enable public
     23 * REST API access and weaken the protection provided by the "Disable REST API" snippet.
     24 */
     25$zenpress_disable_wp_rest_api_allow_access = static function (): bool {
    3426    $post_var = apply_filters('zenpress_disable_wp_rest_api_post_var', false);
    3527    $server_var = apply_filters('zenpress_disable_wp_rest_api_server_var', false);
    3628
    3729    if (!empty($post_var)) {
    38         if (is_array($post_var)) {
    39             foreach($post_var as $var) {
    40                 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Intentional: Allows bypass via specific POST vars for webhooks/third-party integrations
    41                 if (isset($_POST[$var]) && !empty($_POST[$var])) {
    42                     return true;
    43                 }
    44             }
    45         } else {
     30        $post_vars = is_array($post_var) ? $post_var : [$post_var];
     31        foreach ($post_vars as $var) {
    4632            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Intentional: Allows bypass via specific POST vars for webhooks/third-party integrations
    47             if (isset($_POST[$post_var]) && !empty($_POST[$post_var])) {
     33            if (!empty($_POST[$var] ?? null)) {
    4834                return true;
    4935            }
     
    5238
    5339    if (!empty($server_var)) {
    54         if (is_array($server_var)) {
    55             foreach($server_var as $var) {
    56                 if (isset($_SERVER['REQUEST_URI']) && $_SERVER['REQUEST_URI'] === $var) {
    57                     return true;
    58                 }
    59             }
    60         } else {
    61             if (isset($_SERVER['REQUEST_URI']) && $_SERVER['REQUEST_URI'] === $server_var) {
     40        $server_vars = is_array($server_var) ? $server_var : [$server_var];
     41        $request_uri = $_SERVER['REQUEST_URI'] ?? '';
     42        foreach ($server_vars as $var) {
     43            if ($request_uri === $var) {
    6244                return true;
    6345            }
     
    6648
    6749    return false;
    68 }
     50};
    6951
    70 function zenpress_disable_wp_rest_api_legacy() {
     52// Disable REST API
     53if (version_compare(get_bloginfo('version'), '4.7', '>=')) {
     54    add_filter('rest_authentication_errors', static function (WP_Error|bool|null $access) use ($zenpress_disable_wp_rest_api_allow_access): WP_Error|bool|null {
     55        if (!is_user_logged_in() && !$zenpress_disable_wp_rest_api_allow_access()) {
     56            $message = apply_filters('zenpress_disable_wp_rest_api_error', __('REST API restricted to authenticated users.', 'zenpress'));
     57
     58            return new WP_Error('rest_login_required', $message, ['status' => rest_authorization_required_code()]);
     59        }
     60
     61        return $access;
     62    });
     63} else {
    7164    // REST API 1.x
    7265    add_filter('json_enabled', '__return_false');
  • zenpress/trunk/inc/snippets/functions/disable-rss.php

    r3372200 r3448585  
    22
    33if (!defined('ABSPATH')) {
     4    exit;
     5}
     6
     7// Redirect all feed requests to homepage.
     8function zenpress_disable_all_feeds(): void {
     9    wp_safe_redirect(home_url(), 301);
    410    exit;
    511}
     
    1723remove_action('wp_head', 'feed_links_extra', 3);
    1824remove_action('wp_head', 'feed_links', 2);
    19 
    20 // Redirect all feed requests to homepage.
    21 function zenpress_disable_all_feeds() {
    22     wp_safe_redirect(home_url(), 301);
    23     exit;
    24 }
  • zenpress/trunk/inc/snippets/functions/disable-woocommerce-cart-fragments.php

    r3372200 r3448585  
    66
    77if (class_exists('WooCommerce')) {
    8     add_action('wp_enqueue_scripts', static function () {
     8    add_action('wp_enqueue_scripts', static function (): void {
    99        wp_dequeue_script('wc-cart-fragments');
    1010    }, 11);
  • zenpress/trunk/inc/snippets/functions/disable-woocommerce-scripts-styles.php

    r3372200 r3448585  
    66
    77if (class_exists('WooCommerce')) {
    8     add_action('wp_enqueue_scripts', static function () {
     8    add_action('wp_enqueue_scripts', static function (): void {
    99        if (!is_woocommerce() && !is_cart() && !is_checkout() && !is_account_page() && !is_product() && !is_product_category() && !is_shop()) {
    1010            // Dequeue WooCommerce Styles
  • zenpress/trunk/inc/snippets/functions/disable-woocommerce-widgets.php

    r3372200 r3448585  
    66
    77if (class_exists('WooCommerce')) {
    8     add_action('widgets_init', static function() {
     8    add_action('widgets_init', static function (): void {
    99        unregister_widget('WC_Widget_Products');
    1010        unregister_widget('WC_Widget_Product_Categories');
  • zenpress/trunk/inc/snippets/functions/hide-woocommerce-version.php

    r3372200 r3448585  
    77if (class_exists('WooCommerce') && !is_admin()) {
    88    // Remove WooCommerce version from HTTP headers
    9     add_filter('wp_headers', static function ($headers) {
    10         if (isset($headers['X-WooCommerce-Version'])) {
    11             unset($headers['X-WooCommerce-Version']);
    12         }
     9    add_filter('wp_headers', static function (array $headers): array {
     10        unset($headers['X-WooCommerce-Version']);
    1311
    1412        return $headers;
     
    1614
    1715    // Remove WooCommerce version from style URLs
    18     add_filter('style_loader_src', static function ($src) {
    19         if (strpos($src, 'ver=') !== false && strpos($src, 'woocommerce') !== false) {
     16    add_filter('style_loader_src', static function (string $src): string {
     17        if (str_contains($src, 'ver=') && str_contains($src, 'woocommerce')) {
    2018            $src = remove_query_arg('ver', $src);
    2119        }
     
    2523
    2624    // Remove WooCommerce version from script URLs
    27     add_filter('script_loader_src', static function ($src) {
    28         if (strpos($src, 'ver=') !== false && strpos($src, 'woocommerce') !== false) {
     25    add_filter('script_loader_src', static function (string $src): string {
     26        if (str_contains($src, 'ver=') && str_contains($src, 'woocommerce')) {
    2927            $src = remove_query_arg('ver', $src);
    3028        }
  • zenpress/trunk/inc/snippets/functions/hide-wordpress-version.php

    r3372200 r3448585  
    99
    1010// Remove WordPress version from generator
    11 add_filter('the_generator', static function () {
     11add_filter('the_generator', static function (): string {
    1212    return '';
    1313});
    1414
    1515// Remove WordPress version from script URLs
    16 add_filter('script_loader_src', static function ($src) {
    17     if (strpos($src, 'ver=' . get_bloginfo('version')) !== false) {
     16add_filter('script_loader_src', static function (string $src): string {
     17    if (str_contains($src, 'ver=' . get_bloginfo('version'))) {
    1818        $src = remove_query_arg('ver', $src);
    1919    }
     
    2323
    2424// Remove WordPress version from style URLs
    25 add_filter('style_loader_src', static function ($src) {
    26     if (strpos($src, 'ver=' . get_bloginfo('version')) !== false) {
     25add_filter('style_loader_src', static function (string $src): string {
     26    if (str_contains($src, 'ver=' . get_bloginfo('version'))) {
    2727        $src = remove_query_arg('ver', $src);
    2828    }
  • zenpress/trunk/inc/snippets/functions/protect-wp-login.php

    r3372200 r3448585  
    66
    77// Remove detailed login errors
    8 add_filter('login_errors', static function () {
     8add_filter('login_errors', static function (): string {
    99    return __('Login error.', 'zenpress');
    1010});
     
    1515    $BLOCK_DURATION = 300; // 5 minutes
    1616
    17     $ipAddress = isset($_SERVER['REMOTE_ADDR'])
    18         ? filter_var(wp_unslash($_SERVER['REMOTE_ADDR']), FILTER_VALIDATE_IP)
    19         : 'unknown';
     17    $ipAddress = filter_var(
     18        wp_unslash($_SERVER['REMOTE_ADDR'] ?? ''),
     19        FILTER_VALIDATE_IP
     20    ) ?: 'unknown';
    2021
    2122    $blockKey = 'zenpress_login_block_' . $ipAddress;
     
    3435        wp_die(
    3536            esc_html__('Too many failed login attempts. Try again later.', 'zenpress'),
    36             403
     37            '',
     38            ['response' => 403]
    3739        );
    3840    }
     
    4749        wp_die(
    4850            esc_html__('Too many failed login attempts. Try again later.', 'zenpress'),
    49             403
     51            '',
     52            ['response' => 403]
    5053        );
    5154    }
  • zenpress/trunk/inc/snippets/functions/remove-gutenberg-unwanted-block-patterns.php

    r3372200 r3448585  
    99
    1010// Remove core block patterns
    11 add_action('after_setup_theme', static function () {
     11add_action('after_setup_theme', static function (): void {
    1212    remove_theme_support('core-block-patterns');
    1313});
  • zenpress/trunk/inc/snippets/functions/remove-woocommerce-patterns.php

    r3412245 r3448585  
    3737});
    3838
    39 add_action('init', static function() {
     39add_action('init', static function (): void {
    4040    if (!class_exists('WP_Block_Patterns_Registry')) {
    4141        return;
     
    4444    $all_patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered();
    4545    foreach ($all_patterns as $pattern) {
    46         if (isset($pattern['name'])) {
    47             // Use str_starts_with() for PHP 8.0+, fallback to strpos() for older versions
    48             $is_woocommerce_pattern = function_exists('str_starts_with')
    49                 ? str_starts_with($pattern['name'], 'woocommerce-blocks')
    50                 : strpos($pattern['name'], 'woocommerce-blocks') === 0;
    51 
    52             if ($is_woocommerce_pattern) {
    53                 unregister_block_pattern($pattern['name']);
    54             }
     46        if (isset($pattern['name']) && str_starts_with($pattern['name'], 'woocommerce-blocks')) {
     47            unregister_block_pattern($pattern['name']);
    5548        }
    5649    }
     
    6356 * sometimes be tied to large transients/caching issues.
    6457 */
    65 add_filter('woocommerce_admin_features', static function($features) {
    66     // Ensure $features is an array
    67     if (!is_array($features)) {
    68         return $features;
    69     }
    70 
     58add_filter('woocommerce_admin_features', static function (array $features): array {
    7159    $feature_to_disable = 'pattern-toolkit-full-composability';
    7260
  • zenpress/trunk/inc/snippets/meta/clean-dashboard-items.meta.php

    r3412245 r3448585  
    1313    'title' => __('Clean up the WordPress Dashboard', 'zenpress'),
    1414    'description' => __(
    15         'Removes unnecessary widgets and ads widgets from the dashboard. Declutters the admin area and improves usability.',
     15        'Removes unnecessary and ad widgets from the dashboard. Declutters the admin area and improves usability.',
    1616        'zenpress'
    1717    ),
  • zenpress/trunk/inc/snippets/meta/disable-application-passwords.meta.php

    r3412245 r3448585  
    1313    'title' => __('Disable application passwords', 'zenpress'),
    1414    'description' => __(
    15         'Disables WordPress application passwords for all users, improving security. Only disable if API access is not needed.',
     15        'Disables WordPress application passwords for all users. Improves security; do not enable if you need API or app-based authentication (e.g. mobile apps, REST clients).',
    1616        'zenpress'
    1717    ),
     
    1919    'subcategory' => __('security', 'zenpress'),
    2020    'weight' => 0,
    21     'preset' => [''],
     21    'preset' => [],
    2222];
    2323
  • zenpress/trunk/inc/snippets/meta/disable-dashicons.meta.php

    r3412245 r3448585  
    1616        'zenpress'
    1717    ),
    18     'category' => __(' core', 'zenpress'),
     18    'category' => __('core', 'zenpress'),
    1919    'subcategory' => __('performance', 'zenpress'),
    2020    'weight' => 0,
  • zenpress/trunk/inc/snippets/meta/disable-default-pattern-categories.meta.php

    r3412245 r3448585  
    1212return [
    1313    'title' => __('Disable default pattern categories in site editor', 'zenpress'),
    14     'description' => __('Removes default pattern categories from the block pattern inserter in the site editor. Disables default categories: featured, about, audio, banner, buttons, call-to-action, columns, contact, footer, gallery, header, media, portfolio, posts, query, services, team, testimonials, text, videos, and any custom categories. This simplifies the interface by hiding category navigation while patterns remain accessible.', 'zenpress'),
     14    'description' => __('Removes default pattern categories from the block pattern inserter in the site editor (e.g. featured, about, audio, banner, buttons, call-to-action, columns, contact, footer, gallery, header, media, portfolio, posts, query, services, team, testimonials, text, videos) and any custom ones. Simplifies the interface; patterns remain accessible.', 'zenpress'),
    1515    'category' => __('gutenberg', 'zenpress'),
    1616    'subcategory' => __('user-interface', 'zenpress'),
  • zenpress/trunk/inc/snippets/meta/disable-dns-prefetch.meta.php

    r3412245 r3448585  
    1313    'title' => __('Disable DNS prefetch', 'zenpress'),
    1414    'description' => __(
    15         'Removes DNS prefetch resource hints from wp_head avoids unnecessary DNS lookups and slightly improve performance on some sites.',
     15        'Removes DNS prefetch resource hints from wp_head. Avoids unnecessary DNS lookups and slightly improves performance on some sites.',
    1616        'zenpress'
    1717    ),
  • zenpress/trunk/inc/snippets/meta/disable-emoji-scripts.meta.php

    r3412245 r3448585  
    1616        'zenpress'
    1717    ),
    18     'category' => __(' core', 'zenpress'),
     18    'category' => __('core', 'zenpress'),
    1919    'subcategory' => __('performance', 'zenpress'),
    2020    'weight' => 0,
  • zenpress/trunk/inc/snippets/meta/disable-jquery-migrate.meta.php

    r3412245 r3448585  
    1616        'zenpress'
    1717    ),
    18     'category' => __(' core', 'zenpress'),
     18    'category' => __('core', 'zenpress'),
    1919    'subcategory' => __('performance', 'zenpress'),
    2020    'weight' => 0,
  • zenpress/trunk/inc/snippets/meta/disable-pdf-thumbnails.meta.php

    r3412245 r3448585  
    1212return [
    1313    'title' => __('Disable PDF thumbnails', 'zenpress'),
    14     'description' => __('Prevents WordPress from generating thumbnails for uploaded PDF files by removing fallback image sizes. saves storage space and improves performance by avoiding unnecessary image generation.', 'zenpress'),
    15     'category' => __(' core', 'zenpress'),
     14    'description' => __('Prevents WordPress from generating thumbnails for uploaded PDF files by removing fallback image sizes. Saves storage space and improves performance by avoiding unnecessary image generation.', 'zenpress'),
     15    'category' => __('core', 'zenpress'),
    1616    'subcategory' => __('performance', 'zenpress'),
    1717    'weight' => 0,
  • zenpress/trunk/inc/snippets/meta/disable-pingback-trackback.meta.php

    r3412245 r3448585  
    1212return [
    1313    'title' => __('Disable pingback and trackback', 'zenpress'),
    14     'description' => __('Removes the X-Pingback header, disables pingbacks and trackbacks on new posts, and prevents self-pingbacks. reduces spam, blocks potential DDoS vectors, and slightly improves performance by avoiding useless requests.', 'zenpress'),
     14    'description' => __('Removes the X-Pingback header, disables pingbacks and trackbacks on new posts, and prevents self-pingbacks. Reduces spam, blocks potential DDoS vectors, and slightly improves performance by avoiding useless requests.', 'zenpress'),
    1515    'category' => __('core', 'zenpress'),
    1616    'subcategory' => __('security', 'zenpress'),
  • zenpress/trunk/inc/snippets/meta/disable-rest-api.meta.php

    r3412245 r3448585  
    1414    'title' => __('Disable REST API for visitors not logged into WordPress', 'zenpress'),
    1515    'description' => __(
    16         'Disable the WP REST API for visitors not logged into WordPress.',
     16        'Restricts the WP REST API to logged-in users only; unauthenticated requests receive an error. Bypass filters (zenpress_disable_wp_rest_api_post_var, zenpress_disable_wp_rest_api_server_var) allow specific integrations (e.g. webhooks); use non-guessable values only.',
    1717        'zenpress'
    1818    ),
  • zenpress/trunk/inc/snippets/meta/protect-wp-login.meta.php

    r3412245 r3448585  
    1212return [
    1313    'title' => __('Protect the wp-login form from brute force attacks', 'zenpress'),
    14     'description' => __('Removes detailed login error messages and limits failed login attempts per IP address. Blocks further attempts for a set duration after too many failures. Improves security by mitigating brute force attacks.', 'zenpress'),
     14    'description' => __('Removes detailed login error messages and limits failed login attempts per IP. Blocks further attempts for 5 minutes after 5 failed tries. Mitigates brute-force attacks.', 'zenpress'),
    1515    'category' => __('tools', 'zenpress'),
    1616    'subcategory' => __('security', 'zenpress'),
  • zenpress/trunk/inc/snippets/meta/remove-gutenberg-unwanted-block-patterns.meta.php

    r3412245 r3448585  
    1313    'title' => __('Remove WordPress default remote block patterns', 'zenpress'),
    1414    'description' => __('Prevents WordPress from loading remote block patterns and removes the built-in core block patterns. Reduces editor clutter and improves performance by avoiding unnecessary data loading.', 'zenpress'),
    15     'category' => __(' gutenberg', 'zenpress'),
     15    'category' => __('gutenberg', 'zenpress'),
    1616    'subcategory' => __('performance', 'zenpress'),
    1717    'weight' => 0,
  • zenpress/trunk/inc/snippets/meta/remove-rest-api-link.meta.php

    r3412245 r3448585  
    1212return [
    1313    'title' => __('Remove REST API links', 'zenpress'),
    14     'description' => __('Prevents WordPress from adding REST API discovery links to the head section of the site. reduces unnecessary HTML output and slightly improves performance while keeping REST API functionality available.', 'zenpress'),
     14    'description' => __('Prevents WordPress from adding REST API discovery links to the head section of the site. Reduces unnecessary HTML output and slightly improves performance while keeping REST API functionality available.', 'zenpress'),
    1515    'category' => __('core', 'zenpress'),
    1616    'subcategory' => __('performance', 'zenpress'),
  • zenpress/trunk/languages/zenpress.pot

    r3412245 r3448585  
    1 # Copyright (C) 2025 Quentin Le Duff
     1# Copyright (C) 2026 Quentin Le Duff
    22# This file is distributed under the GPL v2 or later.
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: ZenPress - Cleaner, Lighter, Faster WP 2.1.0\n"
     5"Project-Id-Version: ZenPress - Cleaner, Lighter, Faster WP 2.2.0\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/zenpress\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
     
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2025-12-05T13:56:04+00:00\n"
     12"POT-Creation-Date: 2026-01-28T10:05:55+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.12.0\n"
     
    142142msgstr ""
    143143
    144 #: inc/snippets/functions/block-user-enumeration.php:13
    145 #: inc/snippets/functions/block-user-enumeration.php:21
     144#: inc/snippets/functions/block-user-enumeration.php:11
     145#: inc/snippets/functions/block-user-enumeration.php:19
    146146msgid "Access denied."
    147147msgstr ""
    148148
    149 #: inc/snippets/functions/disable-rest-api.php:25
     149#: inc/snippets/functions/disable-rest-api.php:56
    150150msgid "REST API restricted to authenticated users."
    151151msgstr ""
     
    155155msgstr ""
    156156
    157 #: inc/snippets/functions/protect-wp-login.php:35
    158 #: inc/snippets/functions/protect-wp-login.php:48
     157#: inc/snippets/functions/protect-wp-login.php:36
     158#: inc/snippets/functions/protect-wp-login.php:50
    159159msgid "Too many failed login attempts. Try again later."
    160160msgstr ""
     
    173173#: inc/snippets/meta/disable-application-passwords.meta.php:18
    174174#: inc/snippets/meta/disable-author-archives.meta.php:18
     175#: inc/snippets/meta/disable-autosave.meta.php:18
     176#: inc/snippets/meta/disable-capital-p-dangit.meta.php:18
     177#: inc/snippets/meta/disable-dashicons.meta.php:18
    175178#: inc/snippets/meta/disable-dns-prefetch.meta.php:18
     179#: inc/snippets/meta/disable-emoji-scripts.meta.php:18
     180#: inc/snippets/meta/disable-jquery-migrate.meta.php:18
    176181#: inc/snippets/meta/disable-login-language-selector.meta.php:15
    177182#: inc/snippets/meta/disable-oembed.meta.php:15
     183#: inc/snippets/meta/disable-password-strength-meter.meta.php:18
     184#: inc/snippets/meta/disable-pdf-thumbnails.meta.php:15
    178185#: inc/snippets/meta/disable-pingback-trackback.meta.php:15
    179186#: inc/snippets/meta/disable-rest-api.meta.php:19
     
    181188#: inc/snippets/meta/disable-shortlink.meta.php:15
    182189#: inc/snippets/meta/disable-wlw-manifest.meta.php:15
     190#: inc/snippets/meta/disable-wordpress-default-lazy-loading.meta.php:18
    183191#: inc/snippets/meta/disable-xmlrpc-rsdlink.meta.php:15
    184192#: inc/snippets/meta/hide-wordpress-version.meta.php:15
     193#: inc/snippets/meta/limit-post-revisions.meta.php:18
     194#: inc/snippets/meta/remove-help-button.meta.php:18
    185195#: inc/snippets/meta/remove-rest-api-link.meta.php:15
     196#: inc/snippets/meta/remove-thanks-for-using-wordpress-in-footer.meta.php:18
     197#: inc/snippets/meta/remove-wordpress-logo.meta.php:18
    186198msgid "core"
    187199msgstr ""
     
    211223#: inc/snippets/meta/disable-default-pattern-categories.meta.php:16
    212224#: inc/snippets/meta/disable-login-language-selector.meta.php:16
     225#: inc/snippets/meta/remove-help-button.meta.php:19
     226#: inc/snippets/meta/remove-thanks-for-using-wordpress-in-footer.meta.php:19
     227#: inc/snippets/meta/remove-wordpress-logo.meta.php:19
    213228msgid "user-interface"
    214229msgstr ""
     
    219234
    220235#: inc/snippets/meta/clean-dashboard-items.meta.php:14
    221 msgid "Removes unnecessary widgets and ads widgets from the dashboard. Declutters the admin area and improves usability."
     236msgid "Removes unnecessary and ad widgets from the dashboard. Declutters the admin area and improves usability."
    222237msgstr ""
    223238
     
    235250
    236251#: inc/snippets/meta/disable-adjacent-posts.meta.php:19
     252#: inc/snippets/meta/disable-autosave.meta.php:19
     253#: inc/snippets/meta/disable-capital-p-dangit.meta.php:19
    237254#: inc/snippets/meta/disable-dashicons.meta.php:19
    238255#: inc/snippets/meta/disable-dns-prefetch.meta.php:19
     
    240257#: inc/snippets/meta/disable-jquery-migrate.meta.php:19
    241258#: inc/snippets/meta/disable-oembed.meta.php:16
     259#: inc/snippets/meta/disable-password-strength-meter.meta.php:19
    242260#: inc/snippets/meta/disable-pdf-thumbnails.meta.php:16
    243261#: inc/snippets/meta/disable-rss.meta.php:16
     
    248266#: inc/snippets/meta/disable-woocommerce-stripe-scripts.meta.php:16
    249267#: inc/snippets/meta/disable-woocommerce-widgets.meta.php:16
     268#: inc/snippets/meta/disable-wordpress-default-lazy-loading.meta.php:19
     269#: inc/snippets/meta/limit-post-revisions.meta.php:19
    250270#: inc/snippets/meta/remove-gutenberg-unwanted-block-patterns.meta.php:16
    251271#: inc/snippets/meta/remove-rest-api-link.meta.php:16
     
    260280
    261281#: inc/snippets/meta/disable-application-passwords.meta.php:14
    262 msgid "Disables WordPress application passwords for all users, improving security. Only disable if API access is not needed."
     282msgid "Disables WordPress application passwords for all users. Improves security; do not enable if you need API or app-based authentication (e.g. mobile apps, REST clients)."
    263283msgstr ""
    264284
     
    271291msgstr ""
    272292
     293#: inc/snippets/meta/disable-autosave.meta.php:13
     294msgid "Disable autosave"
     295msgstr ""
     296
     297#: inc/snippets/meta/disable-autosave.meta.php:14
     298msgid "Stops the classic editor from autosaving drafts periodically. Reduces database writes and heartbeat traffic. The block editor may still use its own autosave; this targets the legacy post editor."
     299msgstr ""
     300
     301#: inc/snippets/meta/disable-capital-p-dangit.meta.php:13
     302msgid "Disable capital_P_dangit filter"
     303msgstr ""
     304
     305#: inc/snippets/meta/disable-capital-p-dangit.meta.php:14
     306msgid "Removes the filter that forces \"Wordpress\" to \"WordPress\" in titles, content, comments, and widget text. Saves a small amount of processing on each page load."
     307msgstr ""
     308
    273309#: inc/snippets/meta/disable-dashicons.meta.php:13
    274310msgid "Disable dashicons"
     
    279315msgstr ""
    280316
    281 #: inc/snippets/meta/disable-dashicons.meta.php:18
    282 #: inc/snippets/meta/disable-emoji-scripts.meta.php:18
    283 #: inc/snippets/meta/disable-jquery-migrate.meta.php:18
    284 #: inc/snippets/meta/disable-pdf-thumbnails.meta.php:15
    285 msgid " core"
    286 msgstr ""
    287 
    288317#: inc/snippets/meta/disable-default-pattern-categories.meta.php:13
    289318msgid "Disable default pattern categories in site editor"
     
    291320
    292321#: inc/snippets/meta/disable-default-pattern-categories.meta.php:14
    293 msgid "Removes default pattern categories from the block pattern inserter in the site editor. Disables default categories: featured, about, audio, banner, buttons, call-to-action, columns, contact, footer, gallery, header, media, portfolio, posts, query, services, team, testimonials, text, videos, and any custom categories. This simplifies the interface by hiding category navigation while patterns remain accessible."
     322msgid "Removes default pattern categories from the block pattern inserter in the site editor (e.g. featured, about, audio, banner, buttons, call-to-action, columns, contact, footer, gallery, header, media, portfolio, posts, query, services, team, testimonials, text, videos) and any custom ones. Simplifies the interface; patterns remain accessible."
    294323msgstr ""
    295324
    296325#: inc/snippets/meta/disable-default-pattern-categories.meta.php:15
     326#: inc/snippets/meta/remove-gutenberg-unwanted-block-patterns.meta.php:15
    297327#: inc/snippets/meta/separate-gutenberg-core-block-styles.meta.php:15
    298328msgid "gutenberg"
     
    304334
    305335#: inc/snippets/meta/disable-dns-prefetch.meta.php:14
    306 msgid "Removes DNS prefetch resource hints from wp_head avoids unnecessary DNS lookups and slightly improve performance on some sites."
     336msgid "Removes DNS prefetch resource hints from wp_head. Avoids unnecessary DNS lookups and slightly improves performance on some sites."
    307337msgstr ""
    308338
     
    339369msgstr ""
    340370
     371#: inc/snippets/meta/disable-password-strength-meter.meta.php:13
     372msgid "Disable Password Strength Meter"
     373msgstr ""
     374
     375#: inc/snippets/meta/disable-password-strength-meter.meta.php:14
     376msgid "Prevents the password strength meter and zxcvbn scripts from loading on profile, login, and similar pages. Saves roughly 400KB and reduces script parsing. Users will not see the strength indicator when choosing passwords."
     377msgstr ""
     378
    341379#: inc/snippets/meta/disable-pdf-thumbnails.meta.php:13
    342380msgid "Disable PDF thumbnails"
     
    344382
    345383#: inc/snippets/meta/disable-pdf-thumbnails.meta.php:14
    346 msgid "Prevents WordPress from generating thumbnails for uploaded PDF files by removing fallback image sizes. saves storage space and improves performance by avoiding unnecessary image generation."
     384msgid "Prevents WordPress from generating thumbnails for uploaded PDF files by removing fallback image sizes. Saves storage space and improves performance by avoiding unnecessary image generation."
    347385msgstr ""
    348386
     
    352390
    353391#: inc/snippets/meta/disable-pingback-trackback.meta.php:14
    354 msgid "Removes the X-Pingback header, disables pingbacks and trackbacks on new posts, and prevents self-pingbacks. reduces spam, blocks potential DDoS vectors, and slightly improves performance by avoiding useless requests."
     392msgid "Removes the X-Pingback header, disables pingbacks and trackbacks on new posts, and prevents self-pingbacks. Reduces spam, blocks potential DDoS vectors, and slightly improves performance by avoiding useless requests."
    355393msgstr ""
    356394
     
    360398
    361399#: inc/snippets/meta/disable-rest-api.meta.php:15
    362 msgid "Disable the WP REST API for visitors not logged into WordPress."
     400msgid "Restricts the WP REST API to logged-in users only; unauthenticated requests receive an error. Bypass filters (zenpress_disable_wp_rest_api_post_var, zenpress_disable_wp_rest_api_server_var) allow specific integrations (e.g. webhooks); use non-guessable values only."
    363401msgstr ""
    364402
     
    428466msgstr ""
    429467
     468#: inc/snippets/meta/disable-wordpress-default-lazy-loading.meta.php:13
     469msgid "Disable WordPress default lazy loading"
     470msgstr ""
     471
     472#: inc/snippets/meta/disable-wordpress-default-lazy-loading.meta.php:14
     473msgid "Stops WordPress from adding loading=\"lazy\" to images and iframes. Use only if you rely on a theme, plugin, or CDN for lazy loading, or if you prefer to load all media immediately."
     474msgstr ""
     475
    430476#: inc/snippets/meta/disable-xmlrpc-rsdlink.meta.php:13
    431477msgid "Disable XML-RPC and remove RSD link"
     
    452498msgstr ""
    453499
     500#: inc/snippets/meta/limit-post-revisions.meta.php:13
     501msgid "Limit post revision to 10"
     502msgstr ""
     503
     504#: inc/snippets/meta/limit-post-revisions.meta.php:14
     505msgid "Keeps at most 10 revisions per post (or page). Older revisions are deleted when new ones are created. Reduces database size and improves performance."
     506msgstr ""
     507
    454508#: inc/snippets/meta/protect-wp-login.meta.php:13
    455509msgid "Protect the wp-login form from brute force attacks"
     
    457511
    458512#: inc/snippets/meta/protect-wp-login.meta.php:14
    459 msgid "Removes detailed login error messages and limits failed login attempts per IP address. Blocks further attempts for a set duration after too many failures. Improves security by mitigating brute force attacks."
     513msgid "Removes detailed login error messages and limits failed login attempts per IP. Blocks further attempts for 5 minutes after 5 failed tries. Mitigates brute-force attacks."
    460514msgstr ""
    461515
     
    472526msgstr ""
    473527
    474 #: inc/snippets/meta/remove-gutenberg-unwanted-block-patterns.meta.php:15
    475 msgid " gutenberg"
     528#: inc/snippets/meta/remove-help-button.meta.php:13
     529msgid "Remove \"Help\" button"
     530msgstr ""
     531
     532#: inc/snippets/meta/remove-help-button.meta.php:14
     533msgid "Hides the Help tab and panel on all admin screens. Reduces clutter for users who do not use the in-app help."
    476534msgstr ""
    477535
     
    481539
    482540#: inc/snippets/meta/remove-rest-api-link.meta.php:14
    483 msgid "Prevents WordPress from adding REST API discovery links to the head section of the site. reduces unnecessary HTML output and slightly improves performance while keeping REST API functionality available."
     541msgid "Prevents WordPress from adding REST API discovery links to the head section of the site. Reduces unnecessary HTML output and slightly improves performance while keeping REST API functionality available."
     542msgstr ""
     543
     544#: inc/snippets/meta/remove-thanks-for-using-wordpress-in-footer.meta.php:13
     545msgid "Remove \"Thanks for using WordPress\" in footer"
     546msgstr ""
     547
     548#: inc/snippets/meta/remove-thanks-for-using-wordpress-in-footer.meta.php:14
     549msgid "Removes the \"Thank you for creating with WordPress\" message from the admin footer. Cleans up the interface."
    484550msgstr ""
    485551
     
    490556#: inc/snippets/meta/remove-woocommerce-patterns.meta.php:14
    491557msgid "Removes all WooCommerce block patterns to avoid unnecessary pattern registration in the editor."
     558msgstr ""
     559
     560#: inc/snippets/meta/remove-wordpress-logo.meta.php:13
     561msgid "Remove WordPress logo"
     562msgstr ""
     563
     564#: inc/snippets/meta/remove-wordpress-logo.meta.php:14
     565msgid "Removes the WordPress logo and its dropdown from the admin bar. Cleans up the interface for a more neutral or branded look."
    492566msgstr ""
    493567
  • zenpress/trunk/readme.txt

    r3412245 r3448585  
    55Requires at least: 6.0
    66Tested up to: 6.9
    7 Stable tag: 2.1.0
    8 Requires PHP: 7.4
     7Stable tag: 2.2.0
     8Requires PHP: 8.3
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html/
     
    3434
    3535= Settings subpage 🧰 =
    36 * Organized interface with vertical tabs for easy navigation between categories (Core, Gutenberg, WooCommerce, Tools).
     36* Organized interface with vertical tabs for easy navigation between categories (Core, Gutenberg, WooCommerce, Ads-blocker, Tools).
    3737* Features grouped by subcategories (Performance, Security, User Interface) with visual icons for quick identification.
    3838* Three ready-to-use presets: Corporate website, Blog, and E-commerce - each optimized for specific site types.
     
    5353* Disable WordPress shortlink.
    5454* Disable WLW link.
    55 * Remove WordPress default remote block patterns.
    5655* Remove REST API links.
    57 * Separate loading of core block styles.
     56* Disable capital_P_dangit filter.
     57* Disable autosave.
     58* Limit post revision to 10.
     59* Disable Password Strength Meter.
     60* Disable WordPress default lazy loading.
    5861
    5962= Core - Security =
     
    6669* Disable XML-RPC and remove RSD link.
    6770* Hide WordPress version.
    68 * Protect the wp-login form from brute force attacks.
    6971
    7072= Core - User Interface =
    7173
    7274* Clean up the WordPress admin bar.
    73 * Clean up the WordPress Dashboard.
    7475* Disable the login language selector.
     76* Remove WordPress logo.
     77* Remove "Help" button.
     78* Remove "Thanks for using WordPress" in footer.
    7579
    7680= WooCommerce - Performance =
     
    9195* Disable default pattern categories in site editor.
    9296
     97= Ads-blocker - User Interface =
     98* Clean up the WordPress Dashboard.
     99
     100= Tools - Security =
     101* Protect the wp-login form from brute force attacks.
     102
    93103= Presets =
    94104* Corporate website / Portfolio: Optimized for business sites and portfolios. Focuses on security, performance, and removing unnecessary features like RSS feeds and author archives.
     
    112122* Manage Heartbeat API (frontend + backend + admin whitelist).
    113123
    114 = Performance =
    115 * Disable capital_P_dangit filter.
    116 * Disable autosave.
    117 * Disable post revision.
    118 * Disable Password Strength Meter.
    119 * Disable WordPress default lazy loading.
    120 
    121124= User Interface =
    122 * Remove "howdy" from admin bar.
    123 * Remove WordPress logo.
    124 * Remove "Help button".
    125 * Remove "Thanks for using WordPress" in footer.
    126125* Remove "site health" page.
    127126* Remove "Privacy tools".
     
    175174In addition, if you like the plugin then I'd love for you to [leave a review](https://wordpress.org/support/plugin/zenpress/reviews/). Tell all your friends about it too!
    176175
     176= Does ZenPress work with my existing caching / optimization plugins? =
     177
     178Yes. ZenPress focuses on disabling unnecessary core features and plugin bloat; it does not handle page caching, minification, or image optimization. It is designed to work alongside plugins like Cache Enabler, Autoptimize, and most object-cache solutions. If a performance feature overlaps, simply disable it in one of the tools.
     179
     180= Can ZenPress break my theme or plugins? =
     181
     182Potentially, yes—especially if your theme or plugins rely on features you disable (RSS feeds, oEmbed, REST API, emojis, WooCommerce assets, etc.). That’s why each snippet includes clear descriptions and categories (performance, security, UI). Always test changes on a staging site first and enable snippets gradually.
     183
     184= How do I know which snippets are safe to enable? =
     185
     186If you are unsure, start with a preset (Corporate, Blog, E‑commerce) and then adjust. For manual tuning, prefer UI and performance snippets first (dashicons, emojis, dashboard/admin-bar cleanup) before more invasive ones (REST API, XML‑RPC, RSS). After each change, check: frontend pages, login, editor, and (if used) WooCommerce flows.
     187
     188= What happens if I disable the REST API? =
     189
     190Unauthenticated REST requests will be blocked except for any explicit bypasses configured via the filters documented in the snippet (`zenpress_disable_wp_rest_api_post_var`, `zenpress_disable_wp_rest_api_server_var`). Core features and plugins that depend on public REST endpoints (some blocks, headless/front-end apps, third-party integrations) may stop working until you whitelist the required routes.
     191
     192**Important**: If you don't know how to configure bypass filters or whitelist specific routes, don't activate this snippet. It can break functionality that relies on public REST API access.
     193
     194= Does ZenPress store any personal data or phone home? =
     195
     196No. ZenPress does not collect, store, or transmit any personal data. It does not contact external services or include third‑party trackers. All settings are stored in standard WordPress options and remain on your site only.
     197
     198= Is ZenPress multisite compatible? =
     199
     200ZenPress can be network‑activated or activated per site. Settings are stored per site, so each site in a network can have different snippets enabled. As with any optimization/security plugin, test network‑wide changes carefully, especially REST API and XML‑RPC related snippets.
     201
    177202= I have a suggestion =
    178203
    179204Nice ! If you can't find anything in the roadmap, feel free to submit your suggestion on the support page! If you know how to code, you can even contribute on GitHub.
    180205
    181 = Does this plugin work with PHP 8 =
    182 
    183 Yes, it 's been tested actively and works from PHP 7.4 to PHP 8.4.
    184 
    185206== Changelog ==
     207
     208= 2.2.0 =
     209- Global: Dropped PHP 7.4 support and aligned minimum PHP requirement with the currently recommended WordPress version.
     210- Global: Replaced strpos() with str_contains() and str_starts_with() throughout all snippets.
     211- Snippets: Converted snippets to use direct execution pattern where applicable for better performance.
     212- Security: Protect wp-login: fix wp_die() so blocked login responses return HTTP 403 instead of 200.
     213- Security: Loader: guard against path traversal in zenpress_load_snippets() when the folder argument contains '..'.
     214- Security: Disable REST API: document in-code that bypass filters (zenpress_disable_wp_rest_api_post_var, zenpress_disable_wp_rest_api_server_var) should use non-guessable values only.
     215- New actionable function: Disable autosave.
     216- New actionable function: Disable capital_P_dangit filter.
     217- New actionable function: Disable Password Strength Meter.
     218- New actionable function: Disable WordPress default lazy loading.
     219- New actionable function: Limit post revision to 10.
     220- New actionable function: Remove "Help" button.
     221- New actionable function: Remove "Thanks for using WordPress" in footer.
     222- New actionable function: Remove WordPress logo.
    186223
    187224= 2.1.0 =
     
    296333== Upgrade Notice ==
    297334
     335= 2.2.0 =
     336- Breaking: PHP 8.3 is now required (PHP 7.4 support dropped). Major code modernization with improved type safety and performance.
     337- Security: HTTP 403 on login block, path traversal guard in snippet loader, and in-code docs for REST API bypass filters.
     338
    298339= 1.0.0.1 =
    299340
  • zenpress/trunk/zenpress.php

    r3412245 r3448585  
    1212 * Plugin Name: ZenPress - Cleaner, Lighter, Faster WP
    1313 * Description: Easily speed up and strengthen your WordPress site by cleaning out unnecessary features and protecting weak points.
    14  * Version: 2.1.0
     14 * Version: 2.2.0
    1515 * Plugin URI: https://wordpress.org/plugins/zenpress/
    1616 * Author: Quentin Le Duff
     
    2020 * Requires at least: 6.0
    2121 * Tested up to: 6.9
    22  * Requires PHP: 7.4
     22 * Requires PHP: 8.3
    2323 * License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html/
    2424 * License: GPL v2 or later
Note: See TracChangeset for help on using the changeset viewer.