Plugin Directory

Changeset 3767752


Ignore:
Timestamp:
10/27/2026 04:42:52 AM (3 hours ago)
Author:
softminal
Message:

Release 1.1.1: multisite security fixes for the CDN address and the shared Cloudflare token

Location:
softminal-cache/trunk
Files:
10 edited

Legend:

Unmodified
Added
Removed
  • softminal-cache/trunk/app/Cache/Config.php

    r3763679 r3767752  
    33namespace SoftminalCache\App\Cache;
    44
     5use SoftminalCache\App\Modules\Cdn;
    56use SoftminalCache\App\Core\Compat;
    67use SoftminalCache\App\Core\Modules;
    … …  
    243244            $merged['modules'] = Modules::sanitize($input['modules'], $before['modules']);
    244245        }
    245         $settings = self::sanitize($merged);
     246        $settings = self::keepCdnUrls($before, self::sanitize($merged));
    246247        update_option(self::OPTION, $settings);
    247248        Snapshots::record($before, $settings, $label);
    … …  
    262263    {
    263264        $before   = self::get();
    264         $settings = self::sanitize($settings);
     265        $settings = self::keepCdnUrls($before, self::sanitize($settings));
    265266        update_option(self::OPTION, $settings);
    266267        Assets::sync($before, $settings);
    267268        self::writeRuntime();
    268269        ServerRules::sync($settings);
     270
     271        return $settings;
     272    }
     273
     274    /**
     275     * The CDN addresses stay as they were when the current user may not change them (see
     276     * Cdn::mayChangeUrls()), whichever way the change came: the screen, a profile, an import, Undo.
     277     *
     278     * @param Settings $before
     279     * @param Settings $settings
     280     * @return Settings
     281     */
     282    private static function keepCdnUrls(array $before, array $settings): array
     283    {
     284        if (!Cdn::mayChangeUrls()) {
     285            $settings['modules']['cdn']['urls'] = $before['modules']['cdn']['urls'] ?? [];
     286        }
    269287
    270288        return $settings;
  • softminal-cache/trunk/app/Edge/Edge.php

    r3767725 r3767752  
    8484    public static function connect(string $token): array
    8585    {
     86        if (!self::mayManage()) {
     87            return ['ok' => false, 'error' => self::networkOnly()];
     88        }
    8689        $fromConstant = self::fromConstant();
    8790        $token        = $fromConstant ? (string) self::token() : trim($token);
    … …  
    103106
    104107        return ['ok' => true];
     108    }
     109
     110    /**
     111     * Whether the current user may connect, disconnect, switch purging or clear the whole zone. A token
     112     * set in wp-config.php is shared by every site of a network, so there only a network admin may
     113     * (WP-CLI and cron count as the server; an anonymous web request does not).
     114     */
     115    public static function mayManage(): bool
     116    {
     117        return !self::fromConstant() || !is_multisite() || self::serverContext() || current_user_can('manage_network_options');
     118    }
     119
     120    /** WP-CLI or a cron run: the server acting, not a visitor. An anonymous web request is not one. */
     121    public static function serverContext(): bool
     122    {
     123        return (defined('WP_CLI') && WP_CLI) || wp_doing_cron();
     124    }
     125
     126    public static function networkOnly(): string
     127    {
     128        return __('On a network, only a network admin can use the Cloudflare token set in wp-config.php.', 'softminal-cache');
    105129    }
    106130
    … …  
    136160        }
    137161        if (array_key_exists('cloudflare_purge', $input)) {
    138             $data['cloudflare']['purge'] = (bool) filter_var($input['cloudflare_purge'], FILTER_VALIDATE_BOOLEAN);
     162            $purge = (bool) filter_var($input['cloudflare_purge'], FILTER_VALIDATE_BOOLEAN);
     163            if ($purge !== $data['cloudflare']['purge'] && !self::mayManage()) {
     164                return ['ok' => false, 'error' => self::networkOnly()];
     165            }
     166            $data['cloudflare']['purge'] = $purge;
    139167        }
    140168        self::put($data);
    … …  
    158186                'unreadable'   => $data['cloudflare']['zone_id'] !== '' && self::token() === null,
    159187                'constant'     => self::fromConstant(),
     188                'canManage'    => self::mayManage(),
    160189                'zone'         => $data['cloudflare']['zone_name'],
    161190                'purge'        => $data['cloudflare']['purge'],
    … …  
    202231            $token = (string) self::token();
    203232            $zone  = $data['cloudflare']['zone_id'];
    204             $reply = $all ? Cloudflare::purgeAll($token, $zone) : Cloudflare::purgeUrls($token, $zone, $urls);
    205             $data['cloudflare']['last'] = self::result($reply, $all, count($urls));
     233            // A shared (network) token clears the whole zone only for a network admin; for anyone else
     234            // "everything" means this site's homepage plus what changed, never the other sites' pages.
     235            $zoneWide = $all && self::mayManage();
     236            $files    = $all && !$zoneWide ? array_values(array_unique(array_merge([home_url('/')], $urls))) : $urls;
     237            $reply    = $zoneWide ? Cloudflare::purgeAll($token, $zone) : Cloudflare::purgeUrls($token, $zone, $files);
     238            $data['cloudflare']['last'] = self::result($reply, $zoneWide, count($files));
    206239        }
    207240        if (self::varnishOn()) {
  • softminal-cache/trunk/app/Http/Controllers/CacheController.php

    r3767725 r3767752  
    306306                'css'   => ['overlap' => Compat::overlaps('css')],
    307307                'html'  => ['overlap' => Compat::overlaps('html')],
    308                 'cdn'   => ['overlap' => Compat::overlaps('cdn'), 'hosts' => Config::hosts(), 'types' => array_keys(Cdn::TYPES), 'max' => Cdn::MAX_URLS],
     308                'cdn'   => ['overlap' => Compat::overlaps('cdn'), 'canEditUrls' => Cdn::mayChangeUrls(), 'hosts' => Config::hosts(), 'types' => array_keys(Cdn::TYPES), 'max' => Cdn::MAX_URLS],
    309309                'script-manager' => ['overlap' => Compat::overlaps('script-manager'), 'seen' => Seen::all(), 'protected' => ScriptRules::PROTECTED, 'homeUrl' => home_url('/'), 'maxPages' => ScriptRules::MAX_PAGES],
    310310                'fonts' => ['overlap' => Compat::overlaps('fonts')] + GoogleFonts::status(),
  • softminal-cache/trunk/app/Http/Controllers/EdgeController.php

    r3767725 r3767752  
    2929    public function disconnect(\WP_REST_Request $request): \WP_REST_Response
    3030    {
     31        if (!Edge::mayManage()) {
     32            return $this->error(Edge::networkOnly(), 403);
     33        }
    3134        Edge::disconnect();
    3235
    … …  
    5255    public function purge(\WP_REST_Request $request): \WP_REST_Response
    5356    {
     57        if (!Edge::mayManage()) {
     58            return $this->error(Edge::networkOnly(), 403);
     59        }
    5460        Edge::queueAll();
    5561        Edge::flush();
  • softminal-cache/trunk/app/Modules/Cdn.php

    r3767725 r3767752  
    66use SoftminalCache\App\Core\Compat;
    77use SoftminalCache\App\Core\Module;
     8use SoftminalCache\App\Edge\Edge;
    89use SoftminalCache\App\Rewriter\HtmlRewriter;
    910
    … …  
    156157    }
    157158
     159    /**
     160     * Whether the current user may change the CDN addresses. Every script and style of the site loads
     161     * from them, so on a network that takes the same trust as adding scripts (`unfiltered_html`, which
     162     * site admins there do not have). WP-CLI and cron have server access anyway; an anonymous web
     163     * request does not count as either.
     164     */
     165    public static function mayChangeUrls(): bool
     166    {
     167        if (!is_multisite() || Edge::serverContext()) {
     168            return true;
     169        }
     170
     171        return current_user_can('unfiltered_html');
     172    }
     173
    158174    /** `host` or `host:port` of an address, the way Config::hosts() writes the site's own. */
    159175    public static function host(string $url): string
  • softminal-cache/trunk/assets/admin/index.js

    r3767725 r3767752  
    2424/** translators: %s: name of another CDN plugin, e.g. CDN Enabler */const _=(0,e.__)("%s already rewrites file addresses for a CDN, so this is left off here.","softminal-cache");
    2525/** translators: %d: how many CDN addresses can be entered */const w=(0,e.__)("From your CDN, like https://cdn.example.com. Up to %d, one per line; files are shared out between them.","softminal-cache");
    26 /** translators: %s: a web address the user typed */const k=(0,e.__)("%s is this site’s own address. Enter the address your CDN gave you.","softminal-cache");return(0,Yn.jsxs)(ea,{children:[(0,Yn.jsx)(ta,{title:(0,e.__)("CDN","softminal-cache"),description:(0,e.__)("Serve images, styles and scripts from servers close to your visitors.","softminal-cache")}),(0,Yn.jsxs)("div",{className:"border-t border-border",children:[g&&(0,Yn.jsxs)("p",{className:"m-0 flex items-start gap-2 border-b border-border bg-surface-tint px-6 py-3 text-xs text-foreground",children:[(0,Yn.jsx)(ke,{className:"mt-px size-3.5 shrink-0 text-primary","aria-hidden":"true"}),(0,e.sprintf)(_,d.overlap)]}),(0,Yn.jsx)(ka,{id:"smc-cdn-enabled",label:(0,e.__)("Serve files from a CDN","softminal-cache"),term:(0,e.__)("(CDN URL rewrite)","softminal-cache"),description:(0,e.__)("Your CDN copies each file from this site the first time someone asks for it. Nothing is uploaded.","softminal-cache"),warning:(0,e.__)("May break: images and styles, if the CDN does not copy from this site","softminal-cache"),checked:r.enabled,onChange:p("enabled"),disabled:g}),(0,Yn.jsxs)(za,{id:"smc-cdn-urls",label:(0,e.__)("CDN address","softminal-cache"),hint:(0,e.sprintf)(w,d.max),children:[(0,Yn.jsx)("textarea",{id:"smc-cdn-urls",rows:2,value:r.urls,onChange:e=>{p("urls")(e.target.value),o(null)},placeholder:"https://cdn.example.com",spellCheck:!1,disabled:g,className:Ca}),v&&(0,Yn.jsxs)("p",{className:"m-0 flex items-start gap-1.5 text-xs font-semibold text-warning",children:[(0,Yn.jsx)(R,{className:"mt-px size-3.5 shrink-0","aria-hidden":"true"}),(0,e.sprintf)(k,v)]}),(0,Yn.jsxs)("div",{className:"flex flex-wrap items-center gap-3",children:[(0,Yn.jsxs)(Kn,{size:"sm",onClick:()=>{c(!0),o(null),Kt("cdn/test",{method:"POST",body:{url:x[0]}}).then(e=>o(e.data)).catch(e=>o({ok:!1,message:e.message})).finally(()=>c(!1))},disabled:g||i||0===x.length||Boolean(v),children:[i&&(0,Yn.jsx)(Ee,{className:"animate-spin","aria-hidden":"true"}),i?(0,e.__)("Testing…","softminal-cache"):(0,e.__)("Test","softminal-cache")]}),l&&(0,Yn.jsxs)("span",{className:"flex items-start gap-1.5 text-xs font-semibold "+(l.ok?"text-good":"text-bad"),role:"status",children:[l.ok?(0,Yn.jsx)(U,{className:"mt-px size-3.5 shrink-0","aria-hidden":"true"}):(0,Yn.jsx)(R,{className:"mt-px size-3.5 shrink-0","aria-hidden":"true"}),l.message]}),!l&&!i&&x.length>1&&(0,Yn.jsx)("span",{className:"text-xs text-muted-foreground",children:(0,e.__)("Tests the first address.","softminal-cache")})]})]}),(0,Yn.jsxs)("div",{className:"flex flex-col gap-3 border-b border-border px-6 py-4",children:[(0,Yn.jsx)("span",{className:"text-sm font-semibold text-foreground",children:(0,e.__)("Files to serve from the CDN","softminal-cache")}),(0,Yn.jsx)("div",{className:"grid grid-cols-1 gap-x-6 gap-y-3 sm:grid-cols-2",children:d.types.map(e=>(0,Yn.jsxs)("label",{htmlFor:`smc-cdn-type-${e}`,className:"flex cursor-pointer items-center gap-3 text-sm text-foreground",children:[(0,Yn.jsx)(Zn,{id:`smc-cdn-type-${e}`,size:"sm",checked:r.types.includes(e),onChange:y(e),label:b[e],disabled:g}),b[e]]},e))})]}),(0,Yn.jsx)(za,{id:"smc-cdn-exclusions",label:(0,e.__)("Keep these files off the CDN","softminal-cache"),hint:(0,e.__)("File names or part of the address, one per line.","softminal-cache"),children:(0,Yn.jsx)("textarea",{id:"smc-cdn-exclusions",rows:2,value:r.exclusions,onChange:e=>p("exclusions")(e.target.value),placeholder:"logo.svg\n/uploads/private/",spellCheck:!1,disabled:g,className:Ca})}),(0,Yn.jsxs)("div",{className:"flex items-center gap-3 px-6 py-4",children:[(0,Yn.jsxs)(Kn,{variant:"primary",onClick:()=>n({modules:{[Sa]:r}}),disabled:!m||a,children:[a&&(0,Yn.jsx)(Ee,{className:"animate-spin","aria-hidden":"true"}),a?(0,e.__)("Saving…","softminal-cache"):(0,e.__)("Save changes","softminal-cache")]}),m&&!a&&(0,Yn.jsx)(Kn,{variant:"ghost",onClick:()=>s(f),children:(0,e.__)("Cancel","softminal-cache")})]})]})]})}var Ta="h-9 w-full rounded-lg border border-border bg-card px-3 font-mono text-[13px] text-foreground focus-visible:outline-2 focus-visible:outline-ring";function Pa({last:e,now:t}){if(!e)return null;const n=e.ok?U:R;return(0,Yn.jsxs)("p",{className:"m-0 flex items-start gap-1.5 px-6 pb-4 text-xs "+(e.ok?"text-muted-foreground":"font-semibold text-bad"),role:"status",children:[(0,Yn.jsx)(n,{className:"mt-px size-3.5 shrink-0 "+(e.ok?"text-good":""),"aria-hidden":"true"}),(0,Yn.jsxs)("span",{children:[qn(e.at,t)," · ",e.message]})]})}function Oa({status:t,notify:n}){const[a,r]=(0,h.useState)(null);const[s,l]=(0,h.useState)("");const[o,i]=(0,h.useState)(null);const[c,u]=(0,h.useState)("");const d=e=>({cloudflare_purge:e.cloudflare.purge,varnish_enabled:e.varnish.enabled,varnish_target:e.varnish.target.replace(/^http:\/\//,"")});if((0,h.useEffect)(()=>{t?.edge&&(r(t.edge),i(d(t.edge)))},[t]),!a||!o)return(0,Yn.jsxs)(ea,{children:[(0,Yn.jsx)(ta,{title:(0,e.__)("Cloudflare and Varnish","softminal-cache")}),(0,Yn.jsx)("p",{className:"m-0 px-6 pb-6 text-sm text-muted-foreground",children:(0,e.__)("Loading…","softminal-cache")})]});const f=a.cloudflare;const m=t.edge.overlaps||{};const p=JSON.stringify(o)!==JSON.stringify(d(a));const g=e=>t=>i({...o,[e]:t});const x=(e,t,a)=>(u(a),Kt(e,{method:"POST",body:t}).then(e=>(r(e.data),i(d(e.data)),e.message&&n(e.message),e.data)).catch(e=>n(e.message,"bad")).finally(()=>u("")));
     26/** translators: %s: a web address the user typed */const k=(0,e.__)("%s is this site’s own address. Enter the address your CDN gave you.","softminal-cache");return(0,Yn.jsxs)(ea,{children:[(0,Yn.jsx)(ta,{title:(0,e.__)("CDN","softminal-cache"),description:(0,e.__)("Serve images, styles and scripts from servers close to your visitors.","softminal-cache")}),(0,Yn.jsxs)("div",{className:"border-t border-border",children:[g&&(0,Yn.jsxs)("p",{className:"m-0 flex items-start gap-2 border-b border-border bg-surface-tint px-6 py-3 text-xs text-foreground",children:[(0,Yn.jsx)(ke,{className:"mt-px size-3.5 shrink-0 text-primary","aria-hidden":"true"}),(0,e.sprintf)(_,d.overlap)]}),(0,Yn.jsx)(ka,{id:"smc-cdn-enabled",label:(0,e.__)("Serve files from a CDN","softminal-cache"),term:(0,e.__)("(CDN URL rewrite)","softminal-cache"),description:(0,e.__)("Your CDN copies each file from this site the first time someone asks for it. Nothing is uploaded.","softminal-cache"),warning:(0,e.__)("May break: images and styles, if the CDN does not copy from this site","softminal-cache"),checked:r.enabled,onChange:p("enabled"),disabled:g}),(0,Yn.jsxs)(za,{id:"smc-cdn-urls",label:(0,e.__)("CDN address","softminal-cache"),hint:(0,e.sprintf)(w,d.max),children:[(0,Yn.jsx)("textarea",{id:"smc-cdn-urls",rows:2,value:r.urls,onChange:e=>{p("urls")(e.target.value),o(null)},placeholder:"https://cdn.example.com",spellCheck:!1,disabled:g||!d.canEditUrls,className:Ca}),!d.canEditUrls&&(0,Yn.jsx)("p",{className:"m-0 text-xs text-muted-foreground",children:(0,e.__)("On a network, only a network admin can change the CDN address.","softminal-cache")}),v&&(0,Yn.jsxs)("p",{className:"m-0 flex items-start gap-1.5 text-xs font-semibold text-warning",children:[(0,Yn.jsx)(R,{className:"mt-px size-3.5 shrink-0","aria-hidden":"true"}),(0,e.sprintf)(k,v)]}),(0,Yn.jsxs)("div",{className:"flex flex-wrap items-center gap-3",children:[(0,Yn.jsxs)(Kn,{size:"sm",onClick:()=>{c(!0),o(null),Kt("cdn/test",{method:"POST",body:{url:x[0]}}).then(e=>o(e.data)).catch(e=>o({ok:!1,message:e.message})).finally(()=>c(!1))},disabled:g||i||0===x.length||Boolean(v),children:[i&&(0,Yn.jsx)(Ee,{className:"animate-spin","aria-hidden":"true"}),i?(0,e.__)("Testing…","softminal-cache"):(0,e.__)("Test","softminal-cache")]}),l&&(0,Yn.jsxs)("span",{className:"flex items-start gap-1.5 text-xs font-semibold "+(l.ok?"text-good":"text-bad"),role:"status",children:[l.ok?(0,Yn.jsx)(U,{className:"mt-px size-3.5 shrink-0","aria-hidden":"true"}):(0,Yn.jsx)(R,{className:"mt-px size-3.5 shrink-0","aria-hidden":"true"}),l.message]}),!l&&!i&&x.length>1&&(0,Yn.jsx)("span",{className:"text-xs text-muted-foreground",children:(0,e.__)("Tests the first address.","softminal-cache")})]})]}),(0,Yn.jsxs)("div",{className:"flex flex-col gap-3 border-b border-border px-6 py-4",children:[(0,Yn.jsx)("span",{className:"text-sm font-semibold text-foreground",children:(0,e.__)("Files to serve from the CDN","softminal-cache")}),(0,Yn.jsx)("div",{className:"grid grid-cols-1 gap-x-6 gap-y-3 sm:grid-cols-2",children:d.types.map(e=>(0,Yn.jsxs)("label",{htmlFor:`smc-cdn-type-${e}`,className:"flex cursor-pointer items-center gap-3 text-sm text-foreground",children:[(0,Yn.jsx)(Zn,{id:`smc-cdn-type-${e}`,size:"sm",checked:r.types.includes(e),onChange:y(e),label:b[e],disabled:g}),b[e]]},e))})]}),(0,Yn.jsx)(za,{id:"smc-cdn-exclusions",label:(0,e.__)("Keep these files off the CDN","softminal-cache"),hint:(0,e.__)("File names or part of the address, one per line.","softminal-cache"),children:(0,Yn.jsx)("textarea",{id:"smc-cdn-exclusions",rows:2,value:r.exclusions,onChange:e=>p("exclusions")(e.target.value),placeholder:"logo.svg\n/uploads/private/",spellCheck:!1,disabled:g,className:Ca})}),(0,Yn.jsxs)("div",{className:"flex items-center gap-3 px-6 py-4",children:[(0,Yn.jsxs)(Kn,{variant:"primary",onClick:()=>n({modules:{[Sa]:r}}),disabled:!m||a,children:[a&&(0,Yn.jsx)(Ee,{className:"animate-spin","aria-hidden":"true"}),a?(0,e.__)("Saving…","softminal-cache"):(0,e.__)("Save changes","softminal-cache")]}),m&&!a&&(0,Yn.jsx)(Kn,{variant:"ghost",onClick:()=>s(f),children:(0,e.__)("Cancel","softminal-cache")})]})]})]})}var Ta="h-9 w-full rounded-lg border border-border bg-card px-3 font-mono text-[13px] text-foreground focus-visible:outline-2 focus-visible:outline-ring";function Pa({last:e,now:t}){if(!e)return null;const n=e.ok?U:R;return(0,Yn.jsxs)("p",{className:"m-0 flex items-start gap-1.5 px-6 pb-4 text-xs "+(e.ok?"text-muted-foreground":"font-semibold text-bad"),role:"status",children:[(0,Yn.jsx)(n,{className:"mt-px size-3.5 shrink-0 "+(e.ok?"text-good":""),"aria-hidden":"true"}),(0,Yn.jsxs)("span",{children:[qn(e.at,t)," · ",e.message]})]})}function Oa({status:t,notify:n}){const[a,r]=(0,h.useState)(null);const[s,l]=(0,h.useState)("");const[o,i]=(0,h.useState)(null);const[c,u]=(0,h.useState)("");const d=e=>({cloudflare_purge:e.cloudflare.purge,varnish_enabled:e.varnish.enabled,varnish_target:e.varnish.target.replace(/^http:\/\//,"")});if((0,h.useEffect)(()=>{t?.edge&&(r(t.edge),i(d(t.edge)))},[t]),!a||!o)return(0,Yn.jsxs)(ea,{children:[(0,Yn.jsx)(ta,{title:(0,e.__)("Cloudflare and Varnish","softminal-cache")}),(0,Yn.jsx)("p",{className:"m-0 px-6 pb-6 text-sm text-muted-foreground",children:(0,e.__)("Loading…","softminal-cache")})]});const f=a.cloudflare;const m=t.edge.overlaps||{};const p=JSON.stringify(o)!==JSON.stringify(d(a));const g=e=>t=>i({...o,[e]:t});const x=(e,t,a)=>(u(a),Kt(e,{method:"POST",body:t}).then(e=>(r(e.data),i(d(e.data)),e.message&&n(e.message),e.data)).catch(e=>n(e.message,"bad")).finally(()=>u("")));
    2727/** translators: %s: name of another plugin, e.g. Proxy Cache Purge */const v=(0,e.__)("%s is active too. Let one of them clear that cache, not both.","softminal-cache");
    28 /** translators: %s: Cloudflare zone (domain name), e.g. example.com */const b=(0,e.__)("Connected to %s","softminal-cache");return(0,Yn.jsxs)(ea,{children:[(0,Yn.jsx)(ta,{title:(0,e.__)("Cloudflare and Varnish","softminal-cache"),description:(0,e.__)("Clears the caches in front of your site whenever this plugin clears its own.","softminal-cache")}),(0,Yn.jsxs)("div",{className:"border-t border-border",children:[[m.cloudflare,m.varnish].filter(Boolean).map(t=>(0,Yn.jsxs)("p",{className:"m-0 flex items-start gap-2 border-b border-border bg-surface-tint px-6 py-3 text-xs text-foreground",children:[(0,Yn.jsx)(ke,{className:"mt-px size-3.5 shrink-0 text-primary","aria-hidden":"true"}),(0,e.sprintf)(v,t)]},t)),(0,Yn.jsxs)("div",{className:"flex flex-col gap-3 border-b border-border px-6 py-4",children:[(0,Yn.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[(0,Yn.jsxs)("span",{className:"flex items-center gap-2 text-sm font-semibold text-foreground",children:[(0,Yn.jsx)(K,{className:"size-4 text-muted-foreground","aria-hidden":"true"}),(0,e.__)("Cloudflare","softminal-cache")]}),f.connected&&(0,Yn.jsxs)("div",{className:"flex items-center gap-2",children:[(0,Yn.jsx)(na,{tone:"good",icon:U,children:(0,e.sprintf)(b,f.zone)}),!f.constant&&(0,Yn.jsx)(Kn,{size:"sm",variant:"ghost",onClick:()=>x("edge/cloudflare/disconnect",{},"disconnect"),disabled:Boolean(c),children:(0,e.__)("Disconnect","softminal-cache")})]})]}),!f.connected&&(0,Yn.jsxs)(Yn.Fragment,{children:[f.unreadable&&(0,Yn.jsxs)("p",{className:"m-0 flex items-start gap-1.5 text-xs font-semibold text-warning",children:[(0,Yn.jsx)(R,{className:"mt-px size-3.5 shrink-0","aria-hidden":"true"}),(0,e.__)("The saved token can’t be read any more because the site’s security keys changed. Connect again.","softminal-cache")]}),f.constant?(0,Yn.jsx)("p",{className:"m-0 text-xs text-muted-foreground",children:(0,e.__)("Uses the token set in wp-config.php (SOFTMINAL_CACHE_CLOUDFLARE_TOKEN).","softminal-cache")}):(0,Yn.jsxs)("label",{htmlFor:"smc-cf-token",className:"flex flex-col gap-1",children:[(0,Yn.jsx)("span",{className:"text-xs text-muted-foreground",children:(0,e.__)("Create an API token in Cloudflare (My Profile → API Tokens) with “Zone → Cache Purge” and “Zone → Zone → Read” for this domain. It is stored encrypted and never shown again.","softminal-cache")}),(0,Yn.jsx)("input",{id:"smc-cf-token",type:"password",autoComplete:"off",value:s,onChange:e=>l(e.target.value),placeholder:(0,e.__)("API token","softminal-cache"),spellCheck:!1,className:Ta})]}),(0,Yn.jsx)("div",{children:(0,Yn.jsxs)(Kn,{size:"sm",onClick:()=>x("edge/cloudflare/connect",{token:s},"connect").then(e=>e&&l("")),disabled:Boolean(c)||!f.constant&&!s.trim(),children:["connect"===c&&(0,Yn.jsx)(Ee,{className:"animate-spin","aria-hidden":"true"}),"connect"===c?(0,e.__)("Connecting…","softminal-cache"):(0,e.__)("Connect","softminal-cache")]})})]})]}),f.connected&&(0,Yn.jsxs)(Yn.Fragment,{children:[(0,Yn.jsx)(ka,{id:"smc-cf-purge",label:(0,e.__)("Clear Cloudflare with this cache","softminal-cache"),description:(0,e.__)("Changed pages are cleared by address; “Purge” clears everything.","softminal-cache"),checked:o.cloudflare_purge,onChange:g("cloudflare_purge")}),(0,Yn.jsx)(Pa,{last:f.last,now:t.now})]}),(0,Yn.jsx)(ka,{id:"smc-varnish",label:(0,e.__)("Clear Varnish with this cache","softminal-cache"),description:(0,e.__)("Only if your host runs Varnish in front of the site.","softminal-cache"),checked:o.varnish_enabled,onChange:g("varnish_enabled")}),o.varnish_enabled&&(0,Yn.jsxs)("div",{className:"flex flex-col gap-1 border-b border-border px-6 py-4",children:[(0,Yn.jsx)("label",{htmlFor:"smc-varnish-target",className:"text-sm font-semibold text-foreground",children:(0,e.__)("Varnish address","softminal-cache")}),(0,Yn.jsx)("span",{className:"text-xs text-muted-foreground",children:(0,e.__)("Leave empty to use this site’s own address, or enter host:port, like 127.0.0.1:6081.","softminal-cache")}),(0,Yn.jsx)("input",{id:"smc-varnish-target",type:"text",value:o.varnish_target,onChange:e=>g("varnish_target")(e.target.value),placeholder:a.siteHost,spellCheck:!1,className:`${Ta} mt-1 max-w-xs`})]}),a.varnish.enabled&&(0,Yn.jsx)(Pa,{last:a.varnish.last,now:t.now}),(0,Yn.jsxs)("div",{className:"flex flex-wrap items-center gap-3 px-6 py-4",children:[(0,Yn.jsxs)(Kn,{variant:"primary",onClick:()=>x("edge/settings",o,"save"),disabled:!p||Boolean(c),children:["save"===c&&(0,Yn.jsx)(Ee,{className:"animate-spin","aria-hidden":"true"}),"save"===c?(0,e.__)("Saving…","softminal-cache"):(0,e.__)("Save changes","softminal-cache")]}),p&&!c&&(0,Yn.jsx)(Kn,{variant:"ghost",onClick:()=>i(d(a)),children:(0,e.__)("Cancel","softminal-cache")}),!p&&(f.connected&&f.purge||a.varnish.enabled)&&(0,Yn.jsxs)(Kn,{onClick:()=>x("edge/purge",{},"purge"),disabled:Boolean(c),children:["purge"===c&&(0,Yn.jsx)(Ee,{className:"animate-spin","aria-hidden":"true"}),(0,e.__)("Clear them now","softminal-cache")]})]})]})]})}var Ma="w-full resize-y rounded-lg border border-border bg-card p-3 font-mono text-[13px] text-foreground focus-visible:outline-2 focus-visible:outline-ring";function La({id:e,label:t,hint:n,children:a}){return(0,Yn.jsxs)("div",{className:"flex flex-col gap-2 border-b border-border px-6 py-4",children:[(0,Yn.jsxs)("label",{htmlFor:e,className:"flex flex-col gap-0.5",children:[(0,Yn.jsx)("span",{className:"text-sm font-semibold text-foreground",children:t}),(0,Yn.jsx)("span",{className:"text-xs text-muted-foreground",children:n})]}),a]})}function Aa({patterns:t}){const{result:n,checking:a}=function(e){const[t,n]=(0,h.useState)(null);const[a,r]=(0,h.useState)(!1);const s=(0,h.useRef)(0);return(0,h.useEffect)(()=>{const t=++s.current;if(!e.trim())return n(null),void r(!1);r(!0);const a=setTimeout(()=>{Kt("exclusions/preview",{method:"POST",body:{exclusions:e}}).then(e=>t===s.current&&n(e.data)).catch(()=>t===s.current&&n(null)).finally(()=>t===s.current&&r(!1))},400);return()=>clearTimeout(a)},[e]),{result:t,checking:a}}(t);if(!t.trim())return null;if(!n)return a?(0,Yn.jsxs)("p",{className:"m-0 flex items-center gap-2 text-xs text-muted-foreground",children:[(0,Yn.jsx)(Ee,{className:"size-3.5 animate-spin","aria-hidden":"true"}),(0,e.__)("Checking your pages…","softminal-cache")]}):null;
     28/** translators: %s: Cloudflare zone (domain name), e.g. example.com */const b=(0,e.__)("Connected to %s","softminal-cache");return(0,Yn.jsxs)(ea,{children:[(0,Yn.jsx)(ta,{title:(0,e.__)("Cloudflare and Varnish","softminal-cache"),description:(0,e.__)("Clears the caches in front of your site whenever this plugin clears its own.","softminal-cache")}),(0,Yn.jsxs)("div",{className:"border-t border-border",children:[[m.cloudflare,m.varnish].filter(Boolean).map(t=>(0,Yn.jsxs)("p",{className:"m-0 flex items-start gap-2 border-b border-border bg-surface-tint px-6 py-3 text-xs text-foreground",children:[(0,Yn.jsx)(ke,{className:"mt-px size-3.5 shrink-0 text-primary","aria-hidden":"true"}),(0,e.sprintf)(v,t)]},t)),(0,Yn.jsxs)("div",{className:"flex flex-col gap-3 border-b border-border px-6 py-4",children:[(0,Yn.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[(0,Yn.jsxs)("span",{className:"flex items-center gap-2 text-sm font-semibold text-foreground",children:[(0,Yn.jsx)(K,{className:"size-4 text-muted-foreground","aria-hidden":"true"}),(0,e.__)("Cloudflare","softminal-cache")]}),f.connected&&(0,Yn.jsxs)("div",{className:"flex items-center gap-2",children:[(0,Yn.jsx)(na,{tone:"good",icon:U,children:(0,e.sprintf)(b,f.zone)}),!f.constant&&f.canManage&&(0,Yn.jsx)(Kn,{size:"sm",variant:"ghost",onClick:()=>x("edge/cloudflare/disconnect",{},"disconnect"),disabled:Boolean(c),children:(0,e.__)("Disconnect","softminal-cache")})]})]}),!f.connected&&(0,Yn.jsxs)(Yn.Fragment,{children:[f.unreadable&&(0,Yn.jsxs)("p",{className:"m-0 flex items-start gap-1.5 text-xs font-semibold text-warning",children:[(0,Yn.jsx)(R,{className:"mt-px size-3.5 shrink-0","aria-hidden":"true"}),(0,e.__)("The saved token can’t be read any more because the site’s security keys changed. Connect again.","softminal-cache")]}),f.constant?(0,Yn.jsx)("p",{className:"m-0 text-xs text-muted-foreground",children:(0,e.__)("Uses the token set in wp-config.php (SOFTMINAL_CACHE_CLOUDFLARE_TOKEN).","softminal-cache")}):(0,Yn.jsxs)("label",{htmlFor:"smc-cf-token",className:"flex flex-col gap-1",children:[(0,Yn.jsx)("span",{className:"text-xs text-muted-foreground",children:(0,e.__)("Create an API token in Cloudflare (My Profile → API Tokens) with “Zone → Cache Purge” and “Zone → Zone → Read” for this domain. It is stored encrypted and never shown again.","softminal-cache")}),(0,Yn.jsx)("input",{id:"smc-cf-token",type:"password",autoComplete:"off",value:s,onChange:e=>l(e.target.value),placeholder:(0,e.__)("API token","softminal-cache"),spellCheck:!1,className:Ta})]}),!f.canManage&&(0,Yn.jsx)("p",{className:"m-0 text-xs text-muted-foreground",children:(0,e.__)("On a network, only a network admin can use the Cloudflare token set in wp-config.php.","softminal-cache")}),(0,Yn.jsx)("div",{className:f.canManage?"":"hidden",children:(0,Yn.jsxs)(Kn,{size:"sm",onClick:()=>x("edge/cloudflare/connect",{token:s},"connect").then(e=>e&&l("")),disabled:Boolean(c)||!f.constant&&!s.trim(),children:["connect"===c&&(0,Yn.jsx)(Ee,{className:"animate-spin","aria-hidden":"true"}),"connect"===c?(0,e.__)("Connecting…","softminal-cache"):(0,e.__)("Connect","softminal-cache")]})})]})]}),f.connected&&(0,Yn.jsxs)(Yn.Fragment,{children:[(0,Yn.jsx)(ka,{id:"smc-cf-purge",label:(0,e.__)("Clear Cloudflare with this cache","softminal-cache"),description:(0,e.__)("Changed pages are cleared by address; “Purge” clears everything.","softminal-cache"),checked:o.cloudflare_purge,onChange:g("cloudflare_purge"),disabled:!f.canManage}),(0,Yn.jsx)(Pa,{last:f.last,now:t.now})]}),(0,Yn.jsx)(ka,{id:"smc-varnish",label:(0,e.__)("Clear Varnish with this cache","softminal-cache"),description:(0,e.__)("Only if your host runs Varnish in front of the site.","softminal-cache"),checked:o.varnish_enabled,onChange:g("varnish_enabled")}),o.varnish_enabled&&(0,Yn.jsxs)("div",{className:"flex flex-col gap-1 border-b border-border px-6 py-4",children:[(0,Yn.jsx)("label",{htmlFor:"smc-varnish-target",className:"text-sm font-semibold text-foreground",children:(0,e.__)("Varnish address","softminal-cache")}),(0,Yn.jsx)("span",{className:"text-xs text-muted-foreground",children:(0,e.__)("Leave empty to use this site’s own address, or enter host:port, like 127.0.0.1:6081.","softminal-cache")}),(0,Yn.jsx)("input",{id:"smc-varnish-target",type:"text",value:o.varnish_target,onChange:e=>g("varnish_target")(e.target.value),placeholder:a.siteHost,spellCheck:!1,className:`${Ta} mt-1 max-w-xs`})]}),a.varnish.enabled&&(0,Yn.jsx)(Pa,{last:a.varnish.last,now:t.now}),(0,Yn.jsxs)("div",{className:"flex flex-wrap items-center gap-3 px-6 py-4",children:[(0,Yn.jsxs)(Kn,{variant:"primary",onClick:()=>x("edge/settings",o,"save"),disabled:!p||Boolean(c),children:["save"===c&&(0,Yn.jsx)(Ee,{className:"animate-spin","aria-hidden":"true"}),"save"===c?(0,e.__)("Saving…","softminal-cache"):(0,e.__)("Save changes","softminal-cache")]}),p&&!c&&(0,Yn.jsx)(Kn,{variant:"ghost",onClick:()=>i(d(a)),children:(0,e.__)("Cancel","softminal-cache")}),!p&&f.canManage&&(f.connected&&f.purge||a.varnish.enabled)&&(0,Yn.jsxs)(Kn,{onClick:()=>x("edge/purge",{},"purge"),disabled:Boolean(c),children:["purge"===c&&(0,Yn.jsx)(Ee,{className:"animate-spin","aria-hidden":"true"}),(0,e.__)("Clear them now","softminal-cache")]})]})]})]})}var Ma="w-full resize-y rounded-lg border border-border bg-card p-3 font-mono text-[13px] text-foreground focus-visible:outline-2 focus-visible:outline-ring";function La({id:e,label:t,hint:n,children:a}){return(0,Yn.jsxs)("div",{className:"flex flex-col gap-2 border-b border-border px-6 py-4",children:[(0,Yn.jsxs)("label",{htmlFor:e,className:"flex flex-col gap-0.5",children:[(0,Yn.jsx)("span",{className:"text-sm font-semibold text-foreground",children:t}),(0,Yn.jsx)("span",{className:"text-xs text-muted-foreground",children:n})]}),a]})}function Aa({patterns:t}){const{result:n,checking:a}=function(e){const[t,n]=(0,h.useState)(null);const[a,r]=(0,h.useState)(!1);const s=(0,h.useRef)(0);return(0,h.useEffect)(()=>{const t=++s.current;if(!e.trim())return n(null),void r(!1);r(!0);const a=setTimeout(()=>{Kt("exclusions/preview",{method:"POST",body:{exclusions:e}}).then(e=>t===s.current&&n(e.data)).catch(()=>t===s.current&&n(null)).finally(()=>t===s.current&&r(!1))},400);return()=>clearTimeout(a)},[e]),{result:t,checking:a}}(t);if(!t.trim())return null;if(!n)return a?(0,Yn.jsxs)("p",{className:"m-0 flex items-center gap-2 text-xs text-muted-foreground",children:[(0,Yn.jsx)(Ee,{className:"size-3.5 animate-spin","aria-hidden":"true"}),(0,e.__)("Checking your pages…","softminal-cache")]}):null;
    2929/** translators: 1: number of pages that match, 2: number of pages that were checked */const r=(0,e.sprintf)((0,e.__)("Matches %1$d of %2$d pages checked","softminal-cache"),n.matches,n.scanned);
    3030/** translators: %d: number of matching pages that are not listed */const s=(0,e.sprintf)((0,e.__)("…and %d more","softminal-cache"),n.matches-n.urls.length);
  • softminal-cache/trunk/config/constants.php

    r3767725 r3767752  
    66
    77// Plugin Basic Information
    8 define('SOFTMINAL_CACHE_VERSION', '1.1.0');
     8define('SOFTMINAL_CACHE_VERSION', '1.1.1');
    99define('SOFTMINAL_CACHE_DIR', plugin_dir_path(dirname(__FILE__)));
    1010define('SOFTMINAL_CACHE_URL', plugin_dir_url(dirname(__FILE__)));
  • softminal-cache/trunk/readme.txt

    r3767725 r3767752  
    66Tested up to: 7.1
    77Requires PHP: 7.4
    8 Stable tag: 1.1.0
     8Stable tag: 1.1.1
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    148148== Upgrade Notice ==
    149149
     150= 1.1.1 =
     151* Security fix for multisite networks: only a network admin can set the CDN address or use a shared Cloudflare token.
     152
    150153= 1.1.0 =
    151154* New: CDN for images, styles, scripts and fonts
    … …  
    160163
    161164== Changelog ==
     165
     166= 1.1.1 =
     167* Security (multisite only): only a network admin can change the CDN address, because every script of the site loads from it. Site admins can still choose the file types and exclusions.
     168* Security (multisite only): a Cloudflare token set in wp-config.php is shared by the whole network, so only a network admin can connect, disconnect, switch purging or clear the whole zone with it. A purge started by a site admin clears only that site's pages.
     169* Single sites are not affected.
    162170
    163171= 1.1.0 =
  • softminal-cache/trunk/softminal-cache.php

    r3767725 r3767752  
    55 * Plugin URI:        https://softminalcache.com
    66 * Description:       Page cache, minify, lazy load, WebP, local fonts, database cleanup and a built-in speed audit. 100% free – no Pro version, no upsells, no ads.
    7  * Version:           1.1.0
     7 * Version:           1.1.1
    88 * Author:            Softminal
    99 * Author URI:        https://softminal.com
  • softminal-cache/trunk/vendor/composer/installed.php

    r3767725 r3767752  
    44        'pretty_version' => 'dev-master',
    55        'version' => 'dev-master',
    6         'reference' => '3cc9c80f982aa8e47c3591b589c89497879f8501',
     6        'reference' => 'cd6787103fa2f7fd6077d560151f75183af52be4',
    77        'type' => 'wordpress-plugin',
    88        'install_path' => __DIR__ . '/../../',
    … …  
    1414            'pretty_version' => 'dev-master',
    1515            'version' => 'dev-master',
    16             'reference' => '3cc9c80f982aa8e47c3591b589c89497879f8501',
     16            'reference' => 'cd6787103fa2f7fd6077d560151f75183af52be4',
    1717            'type' => 'wordpress-plugin',
    1818            'install_path' => __DIR__ . '/../../',
Note: See TracChangeset for help on using the changeset viewer.