@automattic/newspack-blocks 4.30.0-alpha.1 → 4.30.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -207,7 +207,7 @@ REST controllers are instantiated via wrapper functions hooked to `rest_api_init
207
207
  - **Functions**: `newspack_blocks_*` prefix (e.g., `newspack_blocks_render_block_homepage_articles`)
208
208
  - **Classes**: `Newspack_Blocks*` or `WP_REST_Newspack_*` for REST controllers
209
209
  - **Constants**: `NEWSPACK_BLOCKS__*` (double underscore)
210
- - **Hooks**: `newspack_blocks_*` prefix; modal-specific use `newspack_modal_checkout_*`
210
+ - **Hooks**: `newspack_blocks_*` prefix; modal-specific use `newspack_blocks_modal_checkout_*`
211
211
  - **Text domain**: `newspack-blocks`
212
212
  - **Block names**: Short slugs in `block.json`, namespaced as `newspack-blocks/<slug>` in PHP/JS
213
213
 
@@ -268,7 +268,7 @@ Partial adoption. Some blocks use `.ts`/`.tsx` (homepage-articles `edit.tsx`, do
268
268
  The plugin exposes many hooks. Rather than listing them all here (they change over time), use `grep -r 'apply_filters\|do_action' src/ includes/` to find current hooks. The key hook prefixes are:
269
269
 
270
270
  - **`newspack_blocks_*`**: General block hooks (query building, deduplication, registration, author data)
271
- - **`newspack_modal_checkout_*`**: Modal checkout specific hooks (labels, gateways, billing fields, HTML output)
271
+ - **`newspack_blocks_modal_checkout_*`**: Modal checkout specific hooks (labels, gateways, billing fields, HTML output)
272
272
 
273
273
  ## CI/CD
274
274
 
package/CHANGELOG.md CHANGED
@@ -1,23 +1,16 @@
1
- # @automattic/newspack-blocks [4.30.0-alpha.1](https://github.com/Automattic/newspack-workspace/compare/newspack-blocks@4.29.1...newspack-blocks@4.30.0-alpha.1) (2026-08-06)
1
+ # @automattic/newspack-blocks [4.30.0-alpha.2](https://github.com/Automattic/newspack-workspace/compare/newspack-blocks@4.30.0-alpha.1...newspack-blocks@4.30.0-alpha.2) (2026-08-07)
2
2
 
3
3
 
4
4
  ### Bug Fixes
5
5
 
6
- * **blocks:** APCA contrast picker and derived accent-contrast color ([#730](https://github.com/Automattic/newspack-workspace/issues/730)) ([b05a658](https://github.com/Automattic/newspack-workspace/commit/b05a6588f4d9ebdeb3dbdbc17b5c06c901012ca7))
7
- * **newspack-blocks:** skip article-block image crops on wpcom uploads ([#576](https://github.com/Automattic/newspack-workspace/issues/576)) ([451272c](https://github.com/Automattic/newspack-workspace/commit/451272c3989cb3ed5f25daaa1e647e1b5dcbf453))
6
+ * **blocks:** input validation hardening for author, iframe and checkout surfaces ([#782](https://github.com/Automattic/newspack-workspace/issues/782)) ([20230c1](https://github.com/Automattic/newspack-workspace/commit/20230c12471e327e8cbdf6d3ffe7d9823a28b00b))
8
7
 
9
-
10
- ### Features
11
-
12
- * **performance:** defer reader-facing JS assets (NPPM-3037) ([#722](https://github.com/Automattic/newspack-workspace/issues/722)) ([2855574](https://github.com/Automattic/newspack-workspace/commit/2855574099c4f416a984cda8565b269e5a9f0fb0))
8
+ ## @automattic/newspack-blocks [4.29.2](https://github.com/Automattic/newspack-workspace/compare/newspack-blocks@4.29.1...newspack-blocks@4.29.2) (2026-08-07)
13
9
 
14
10
 
15
- ### Dependencies
11
+ ### Bug Fixes
16
12
 
17
- * **newspack-colors:** upgraded to 1.1.2-alpha.1
18
- * **newspack-icons:** upgraded to 1.1.1-alpha.1
19
- * **newspack-components:** upgraded to 4.6.1-alpha.1
20
- * **newspack-scripts:** upgraded to 5.11.0-alpha.1
13
+ * **blocks:** input validation hardening for author, iframe and checkout surfaces ([#782](https://github.com/Automattic/newspack-workspace/issues/782)) ([20230c1](https://github.com/Automattic/newspack-workspace/commit/20230c12471e327e8cbdf6d3ffe7d9823a28b00b))
21
14
 
22
15
  ## @automattic/newspack-blocks [4.29.1](https://github.com/Automattic/newspack-workspace/compare/newspack-blocks@4.29.0...newspack-blocks@4.29.1) (2026-08-04)
23
16
 
@@ -1 +1 @@
1
- <?php return array('dependencies' => array(), 'version' => 'a25d06a462dfabdbe12a');
1
+ <?php return array('dependencies' => array(), 'version' => '1c1712bf74d293308889');
package/dist/modal.js CHANGED
@@ -1 +1 @@
1
- (()=>{"use strict";function e(e,t=!1){const n=e.querySelectorAll('button, [href], input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])');if(0===n.length)return!1;const o=n?.[0];let c;o.focus(),t?(document.addEventListener("keydown",function(e){if(("Tab"===e.key||9===e.keyCode)&&e.shiftKey&&document.activeElement===o){const n=t.contentWindow.document,o=n.getElementById("checkout-after-success");c=null!==o?o:n.getElementById("checkout_cancel"),c.focus(),e.preventDefault()}}),document.getElementById("newspack-a11y-last-element").addEventListener("focus",()=>{o.focus()})):(c=n[n.length-1],document.addEventListener("keydown",function(e){("Tab"===e.key||9===e.keyCode)&&(e.shiftKey?document.activeElement===o&&(c.focus(),e.preventDefault()):document.activeElement===c&&(o.focus(),e.preventDefault()))}))}const t=(e,t={})=>({...t,action:e}),n=["action","action_type","amount","currency","product_id","product_type","variation_id","variation_ids","is_variable","is_grouped","child_ids","price_summary","newspack_popup_id","prompt_title","gate_post_id","recurrence","referrer"],o=(e,t="np_modal_checkout_interaction")=>{if("function"==typeof window.gtag&&e){const o={};for(const t of n)e[t]&&("boolean"==typeof e[t]?e[t]=e[t]?"yes":"no":"true"===e[t]?e[t]="yes":"false"===e[t]&&(e[t]="no"),o[t]=e[t].toString());window.gtag("event",t,o)}},c=e=>{if("function"!=typeof window.gtag)return;let n="opened";const{amount:c="",is_variable:a="",is_grouped:r="",price:i="",variation_id:s=""}=e,l={...e};(c||i)&&(l.amount=c||i),a&&!s&&(n="opened_variations"),r&&(n="opened_grouped");const u=t(n,l);o(u)};function a(e,t=null){const n=document.createElement("input");return n.type="hidden",n.name=e,t&&(n.value=t),n}function r(e){e.requestSubmit(e.querySelector('button[type="submit"]'))}function i(e){const t="string"==typeof e?document.getElementById(e):e;if(!t)return console.warn("No container found for checkout data"),{};let n={};if("FORM"===t.tagName){const e=new FormData(t);n=Object.fromEntries(e)}const o=t.dataset.checkout;if(!o)return console.warn("No checkout data found"),n;try{n={...n,...JSON.parse(o)}}catch(e){console.warn("Error parsing checkout data")}const c=new URL(t.ownerDocument.defaultView.location.href);return c.searchParams.get("action_type")&&(n.action_type=c.searchParams.get("action_type")),n}const s=e=>{if("function"!=typeof window.gtag)return;e=e||i("newspack_modal_checkout");const n=t("dismissed",e);o(n)};function l(e){const t=e&&e.dataset?e.dataset.checkout:null;if(!t)return null;try{return JSON.parse(t)}catch(e){return null}}function u(e,t,n=null){const o=e.querySelectorAll(".wp-block-newspack-blocks-checkout-button"),c=null!=n&&""!==String(n);let a=null;return o.forEach(e=>{if(a)return;const o=e.querySelector("form"),r=l(o);r&&String(r.product_id)===String(t)&&(c&&String(r.variation_id)!==String(n)||(a=o))}),a}const d=["after_success_behavior","after_success_url","after_success_button_label","gate_post_id","newspack_popup_id","prompt_title","coupon"];const p={gate:"gate_post_id",prompt:"newspack_popup_id"};const m=newspackBlocksModal.newspack_class_prefix,h="newspack_modal_checkout_iframe",_="newspack_modal_checkout_container",f=`${m}__modal`,w="newspack-blocks__modal-variation",y=`${m}__processing-payment-text`,k=newspackBlocksModal.processing_payment_messages;let v=!1,g=newspackBlocksModal.labels.checkout_modal_title,b=null;const S=e=>{e.overlayId&&window.newspackReaderActivation?.overlays&&window.newspackReaderActivation?.overlays.remove(e.overlayId),e.setAttribute("data-state","closed"),document.body.style.overflow="auto"};window.onpageshow=e=>{e.persisted&&(document.querySelectorAll(".modal-processing").forEach(e=>e.classList.remove("modal-processing")),document.querySelectorAll(".non-modal-checkout-loading").forEach(e=>e.classList.remove("non-modal-checkout-loading")),document.querySelectorAll(`.${f}-container`).forEach(e=>S(e)))};const E=new CustomEvent("checkout-closed");var q;window.newspackRAS=window.newspackRAS||[],q=()=>{const t=document.querySelector("#newspack_modal_checkout");if(!t)return;const n=t.querySelector(`.${f}__content`),o=a("modal_checkout","1"),q=n.querySelector(`.${m}__spinner`);let A=document.querySelector(".newspack-reader__account-link")?.[0];const L="600px",$=document.createElement("iframe");$.name=h,$.style.height=L,$.style.visibility="hidden";const R=e=>{P.observe(e),"none"!==q.style.display&&(q.style.display="none"),"visible"!==$.style.visibility&&($.style.visibility="visible"),$._ready=!0};!function(e,t,n){e._ready=!1,e._readyTimer&&clearTimeout(e._readyTimer);let o=!1,c="";function a(){o||(o=!0,clearTimeout(e._readyTimer),t.call(this))}function r(){"complete"===this.readyState&&a.call(this)}function i(){e._ready=!1,n&&n(),e._readyTimer&&clearTimeout(e._readyTimer);const t=e.contentDocument||e.contentWindow?.document;t&&(t.removeEventListener("DOMContentLoaded",a),t.removeEventListener("readystatechange",r))}function s(){if(e._ready)return void i();const t=e.contentDocument||e.contentWindow?.document;t&&0!==t.URL.indexOf("about:")?"complete"===t?.readyState?a.call(t):(t.addEventListener("DOMContentLoaded",a),t.addEventListener("readystatechange",r)):e._readyTimer=setTimeout(s,10)}e._observer||(e._observer=new MutationObserver(e=>{e.forEach(e=>{"attributes"===e.type&&"src"===e.attributeName&&(o=!1,i(),s())})}),e._observer.observe(e,{attributes:!0})),e._locationObserver||(e._locationObserver=setInterval(function(){const t=e.contentDocument||e.contentWindow?.document;t&&t.URL!==c&&(c=t.URL,o=!1,i(),s())},50)),s()}($,function(){const n=$.contentWindow?.location;if(window.newspackReaderActivation&&n?.href?.includes("order-received")){const e=window.newspackReaderActivation,t=new Proxy(new URLSearchParams(n.search),{get:(e,t)=>e.get(t)});t.email&&(e.setReaderEmail(t.email),e.setAuthenticated(!0))}const o=$?.contentDocument?.querySelector(`#${_}`);if(!o)return;const c=i(o.querySelector("#modal-checkout-product-details")),a=document.createElement("p");a.classList.add(y);let r=[];const s=()=>{r.forEach(e=>clearTimeout(e)),r=[]},l=()=>{q.style.display="none",s(),q.querySelectorAll(`.${y}`).forEach(e=>e.remove())};!function(e,t){e.checkoutCancel?t():e.addEventListener("checkout-cancel",t)}(o,()=>{x()}),function(e,t){e.checkoutPlaceOrderStart?t():e.addEventListener("checkout-place-order-start",t)}(o,e=>{q.querySelectorAll(`.${y}`).forEach(e=>e.remove()),q.style.display="flex",s(),a.textContent=k[0]?.text??"",k.slice(1).forEach(({text:t,delay:n})=>{const o=setTimeout(()=>{e.target.dispatchEvent(new CustomEvent("checkout-place-order-processing")),a.textContent=t},n);r.push(o)}),q.appendChild(a)}),function(e,t){e.checkoutPlaceOrderError?t():e.addEventListener("checkout-place-order-error",t)}(o,l),function(e,t){e.checkoutReady?t():e.addEventListener("checkout-ready",t)}(o,()=>{if(t.initialErrors){const e=document.createElement("div");e.classList.add("woocommerce-error"),e.textContent=t.initialErrors,o.prepend(e),delete t.initialErrors}I(),N(g),$.contentWindow?.newspackBlocksModalCheckout?.checkout_nonce&&(t.checkout_nonce=$.contentWindow.newspackBlocksModalCheckout.checkout_nonce),R(o)}),function(e,t){e.checkoutComplete?t():e.addEventListener("checkout-complete",t)}(o,()=>{window.newspackRAS.push(["checkout_completed",c]),window?.newspackReaderActivation?.refreshNewslettersSignupModal&&window?.newspackReaderActivation?.getReader()?.email&&window.newspackReaderActivation.refreshNewslettersSignupModal(window.newspackReaderActivation.getReader().email),I("small"),N(newspackBlocksModal.labels.thankyou_modal_title),R(o),e(t.querySelector(`.${f}`)),l()}),function(e,t){e.checkoutPlaceOrderCriticalError?t():e.addEventListener("checkout-place-order-critical-error",t)}(o,()=>{var e;(e=b)&&(x(),q.style.display="none",t.initialErrors=newspackBlocksModal.labels.critical_error,e.requestSubmit(e.querySelector('button[type="submit"]')),l())})},()=>{q.style.display="flex"});const M=e=>new Promise((t,n)=>{const o=new URLSearchParams(e);o.append("action","modal_checkout_request"),fetch(newspackBlocksModal.ajax_url+"?"+o.toString()).then(e=>{e.ok||n(e),e.json().then(e=>{t(e.url)}).catch(n)}).catch(n)}),C=()=>"undefined"!=typeof newspack_ras_config&&!newspack_ras_config?.is_logged_in&&!window?.newspackReaderActivation?.getReader?.()?.authenticated&&newspackBlocksModal?.is_registration_required&&window?.newspackReaderActivation?.openAuthModal,B=t=>{const n=!newspackBlocksModal.has_unsupported_payment_gateway;n||t.preventDefault();const o=t.target;o.classList.add("modal-processing"),function(e){if(!e||"function"!=typeof e.querySelector)return!1;if(e.closest(".newspack-content-gate__gate"))return!1;const t=function(){let e,t;try{e=window.sessionStorage.getItem("newspack_cta_attribution")}catch(e){return null}if(!e)return null;try{t=JSON.parse(e)}catch(e){return null}return t&&1===t.v&&p[t.type]&&t.id?"number"!=typeof t.ts||Date.now()-t.ts>18e5?null:t:null}();if(!t)return!1;const n=p[t.type];if(e.querySelector(`input[name="${n}"]`))return!1;const o=document.createElement("input");o.type="hidden",o.name=n,o.value=t.id,e.prepend(o)}(o);const u=i(o);if(u.newspack_donate){const e=u.donation_frequency,t=[...o.querySelectorAll(`.donation-tier__${e}, .donation-frequency__${e}`)],n=u.donation_tier_index;let c,a;n?(c=t[n],a=u[`donation_value_${e}`]):(c=t[0],u[`donation_value_${e}_untiered`]?a=u[`donation_value_${e}_untiered`]:(a=u[`donation_value_${e}`],"other"===a&&(a=u[`donation_value_${e}_other`])));const r=i(c);for(const e in r)u[e]=r[e];u.amount=a,u.price_summary=u.summary_template.replace("{{PRICE}}",function(e,t="USD"){return parseFloat(e).toLocaleString(document.documentElement.lang,{style:"currency",currency:t,currencyDisplay:"narrowSymbol"})}(u.amount,u.currency))}u&&Object.keys(u).forEach(e=>{0===o.querySelectorAll('input[name="'+e+'"]').length&&o.prepend(a(e,u[e]))}),u.variation_id||(A=t.submitter);const m=document.querySelectorAll(`.${w}`);if(m.forEach(e=>{(C()||n)&&S(e)}),u.is_grouped||u.is_variable&&!u.variation_id){const n=[...m].find(e=>e.dataset.productId===u.product_id);if(n)return n.querySelectorAll(`form[target="${h}"]`).forEach(e=>{!function(e,t,n=d){if(!e||!t)return;const o=e.ownerDocument;n.forEach(n=>{e.querySelectorAll(`input[name="${n}"]`).forEach(e=>e.remove());const c=t[n],a=null==c?"":String(c);if(!a)return;const r=o.createElement("input");r.type="hidden",r.name=n,r.value=a,e.prepend(r)})}(e,u);const t=l(e);t&&Object.keys(t).forEach(n=>{0===e.querySelectorAll('input[name="'+n+'"]').length&&e.prepend(a(n,t[n]))})}),t.preventDefault(),o.classList.remove("modal-processing"),T(n),e(n,!1),v||c(u),void document.getElementById("newspack_modal_checkout").setAttribute("data-checkout",JSON.stringify(u))}if(n||C()){if(o.classList.remove("modal-processing"),v||c(u),v=!0,C()){t.preventDefault();const e=M(u);e.then(e=>{window.newspackReaderActivation?.setPendingCheckout?.(e)}),window.newspackReaderActivation.openAuthModal({title:newspackBlocksModal.labels.auth_modal_title,onSuccess:(t,o)=>{e.then(e=>{o?.registered&&n&&(e+=`&${newspackBlocksModal.checkout_registration_flag}=1`),n?r(D(e)):M(u).then(window.location.href=e)}).catch(e=>{console.warn("Unable to generate cart:",e),x()})},onError:()=>{x()},onDismiss:()=>{s(u),v=!1,document.getElementById("newspack_modal_checkout").removeAttribute("data-checkout")},skipSuccess:!0,skipNewslettersSignup:!0,labels:{signin:{title:newspackBlocksModal.labels.signin_modal_title},register:{title:newspackBlocksModal.labels.register_modal_title}},trigger:t.submitter,closeOnSuccess:n})}else O(),document.getElementById("newspack_modal_checkout").setAttribute("data-checkout",JSON.stringify(u));b=o}else M(u).then(e=>{window.location.href=e}),(!u.is_variable||u.variation_id)&&o.querySelectorAll("button[type=submit]:focus").forEach(e=>{e.classList.add("non-modal-checkout-loading");const t=e.innerHTML;e.innerHTML="<span>"+t+"</span>"})},D=e=>{const t=document.createElement("form");t.method="POST",t.action=e,t.target=h,t.style.display="none";const n=document.createElement("button");return n.setAttribute("type","submit"),t.appendChild(n),document.body.appendChild(t),t.addEventListener("submit",B),t},P=new ResizeObserver(e=>{if(!e||!e.length)return;if(!$.contentDocument)return;const o=e[0].contentRect;if(o){const e=.01*Math.max(document.documentElement.clientHeight,window.innerHeight||0)*90-(t.querySelector(`.${f}__header`)?.offsetHeight||0),c=o.top+o.bottom,a=Math.min(c,e);if(0===a)return void($.style.visibility="hidden");n.style.height=a+"px",$.style.height=a+"px"}}),x=()=>{const e=$?.contentDocument?.querySelector(`#${_}`),o=e?.querySelector('input[name="after_success_url"]'),c=e?.querySelector('input[name="after_success_behavior"]'),a=document?.querySelector(".newspack-newsletters-signup-modal"),r=i(e?.querySelector("#modal-checkout-product-details"));e?.checkoutComplete||(async()=>{const e=new FormData;newspackBlocksModal.has_unsupported_payment_gateway||e.append("modal_checkout","1"),e.append("action","abandon_modal_checkout"),e.append("_wpnonce",t.checkout_nonce),t.checkout_nonce=null;try{await fetch(newspackBlocksModal.ajax_url,{method:"POST",body:e})}catch(e){console.warn("Unable to empty cart:",e)}})();const l=!($.contentDocument&&o&&c&&e?.checkoutComplete);if((l||a)&&(q.style.display="flex",$&&n.contains($)&&($._ready=!1,$.src="about:blank",$.style.height=L,$.style.visibility="hidden",n.style.height=L,n.removeChild($)),P&&P.disconnect(),document.querySelectorAll(`.${f}-container`).forEach(e=>S(e)),A&&A.focus(),document.dispatchEvent(E)),e?.checkoutComplete){const e=()=>{if(o&&c){const e=o.getAttribute("value"),t=c.getAttribute("value");if("custom"===t)window.location.href=e;else if("referrer"===t){const e=document.referrer;let t=!1;if(e)try{new URL(e).origin===window.location.origin&&(window.location.href=e,t=!0)}catch(e){}t||window.history.back()}}window?.newspackReaderActivation?.setPendingCheckout?.(),v=!1};"subscription_switch"!==r.action_type&&window?.newspackReaderActivation?.openNewslettersSignupModal?window.newspackReaderActivation.openNewslettersSignupModal({onSuccess:e,onError:e,closeOnSuccess:l}):e(),l&&(g=newspackBlocksModal.labels.checkout_modal_title,I(),N(g))}else window?.newspackReaderActivation?.setPendingCheckout?.(),s(),v=!1,document.getElementById("newspack_modal_checkout").removeAttribute("data-checkout");document.removeEventListener("keydown",U)},O=o=>{o&&($.src=o),q.style.display="flex",T(t),n.appendChild($),t.addEventListener("click",e=>{e.target===t&&x()}),e(t,$),document.addEventListener("keydown",U)},T=e=>{window.newspackReaderActivation?.overlays&&(e.overlayId=window.newspackReaderActivation?.overlays.add()),e.setAttribute("data-state","open"),document.body.style.overflow="hidden"},N=e=>{const n=t.querySelector(`.${f}__header h2`);n&&(n.innerText=e)},I=(e="default")=>{const n=t.querySelector(`.${f}`);n&&("small"===e?n.classList.add(`${f}--small`):n.classList.remove(`${f}--small`))};t.querySelectorAll(`.${f}__close`).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault(),x()})}),document.querySelectorAll(".newspack-blocks__modal-variation").forEach(e=>{e.addEventListener("click",t=>{t.target===e&&x()}),e.querySelectorAll(`.${f}__close`).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault(),x()})})});const U=e=>{"Escape"===e.key&&x()};document.querySelectorAll(".wpbnbd.wpbnbd--platform-wc, .wp-block-newspack-blocks-checkout-button, .newspack-blocks__modal-variation").forEach(e=>{e.querySelectorAll("form").forEach(e=>{newspackBlocksModal.has_unsupported_payment_gateway||e.prepend(o.cloneNode()),e.target=h,e.addEventListener("submit",B)})}),(()=>{const e=new URLSearchParams(window.location.search);if(!e.has("checkout"))return;let t=!0;const n=e.get("type");if("donate"===n){const t=e.get("layout"),n=e.get("frequency"),o=e.get("amount"),c=e.get("other");t&&n&&o&&((e,t,n,o=null)=>{let c;document.querySelectorAll(".wpbnbd.wpbnbd--platform-wc form").forEach(a=>{const r=a.querySelector(`input[name="donation_frequency"][value="${t}"]`);if(r)if("tiered"===e){const e=document.querySelector(`button[data-frequency-slug="${t}"]`);if(!e)return;e.click();const o=a.querySelector(`button[type="submit"][name="donation_value_${t}"][value="${n}"]`);if(!o)return;o.click()}else{const i="untiered"===e?a.querySelector(`input[name="donation_value_${t}_untiered"]`):a.querySelector(`input[name="donation_value_${t}"][value="${n}"]`);if(r&&i){if(r.checked=!0,"untiered"===e)i.value=n;else if("other"===n){i.click();const e=a.querySelector(`input[name="donation_value_${t}_other"]`);e&&o&&(e.value=o)}else i.checked=!0;c=a}}}),c&&r(c)})(t,n,o,c)}else if("checkout_button"===n){const n=e.get("product_id"),o=e.get("variation_id");n?t=((e,t=null)=>{const n=function(e,t,n,o={}){const{allowProductOnlyFallback:c=!1}=o;if(null==n||""===String(n))return u(e,t,null);const a=u(e,t,n);if(a)return a;const r=function(e,t,n,o={}){const{variationModalClassPrefix:c,iframeName:a}=o,r=[...e.querySelectorAll(`.${c}`)].find(e=>String(e.dataset.productId)===String(t));if(!r)return null;const i=r.querySelectorAll("form"),s=a?[...i].find(e=>e.getAttribute("target")===a):i[0];if(!s)return null;const l=[...s.querySelectorAll('input[type="radio"][name="product_id"]')].find(e=>String(e.value)===String(n));return l?(l.checked=!0,s):null}(e,t,n,o);return r?(function(e,t,n=d){if(!e||!t)return;const o=t.ownerDocument,c=new FormData(e);n.forEach(e=>{if(t.querySelector(`input[name="${e}"]`))return;const n=c.getAll(e).filter(e=>"string"==typeof e&&e);if(!n.length)return;const a=o.createElement("input");a.type="hidden",a.name=e,a.value=n[n.length-1],t.prepend(a)})}(function(e,t){const n=e.querySelectorAll(".wp-block-newspack-blocks-checkout-button");let o=null,c=null;return n.forEach(e=>{if(c)return;const n=e.querySelector("form"),a=l(n);a&&String(a.product_id)===String(t)&&(a.variation_id?o=o||n:c=n)}),c||o}(e,t),r),r):c?u(e,t,null):null}(document,e,t,{variationModalClassPrefix:w,iframeName:h});if(n)return r(n),!0;const o=`Newspack modal checkout: no checkout form found for product_id "${e}"`+(t?` and variation_id "${t}"`:"")+". The checkout was not triggered.";return console.warn(o),!1})(n,o):(t=!1,console.warn("Newspack modal checkout: checkout_button trigger is missing product_id. The checkout was not triggered."))}else{const e=window.newspackReaderActivation?.getPendingCheckout?.();e&&r(D(e))}t&&window.history.replaceState(null,null,window.location.pathname)})(),window.newspackOpenModalCheckout=({url:e=null,title:t=null,actionType:n=null,afterSuccess:o={},onCheckoutComplete:c=null,onClose:a=null})=>{if(g=t||newspackBlocksModal.labels.checkout_modal_title,N(g),(e=new URL(e||newspackBlocksModal.checkout_url)).searchParams.has("my_account_checkout")&&e.searchParams.delete("my_account_checkout"),e.searchParams.has("modal_checkout")||e.searchParams.set("modal_checkout","1"),n&&e.searchParams.set("action_type",n),o?.url&&e.searchParams.set("after_success_url",o.url),(o?.behavior||o?.url)&&e.searchParams.set("after_success_behavior",o.behavior||"custom"),o?.buttonLabel&&e.searchParams.set("after_success_button_label",o.buttonLabel),c){const e=({detail:{action:e,data:t}})=>{"checkout_completed"===e&&c(t)};window.newspackRAS.push(t=>{t.on("activity",e);const n=()=>{t.off("activity",e),document.removeEventListener("checkout-closed",n)};document.addEventListener("checkout-closed",n)})}if(a){const e=()=>{a(),document.removeEventListener("checkout-closed",e)};document.addEventListener("checkout-closed",e)}O(e.toString())},window.newspackCloseModalCheckout=x},"undefined"!=typeof document&&("complete"!==document.readyState&&"interactive"!==document.readyState?document.addEventListener("DOMContentLoaded",q):q())})();
1
+ (()=>{"use strict";function e(e,t=!1){const n=e.querySelectorAll('button, [href], input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])');if(0===n.length)return!1;const o=n?.[0];let c;o.focus(),t?(document.addEventListener("keydown",function(e){if(("Tab"===e.key||9===e.keyCode)&&e.shiftKey&&document.activeElement===o){const n=t.contentWindow.document,o=n.getElementById("checkout-after-success");c=null!==o?o:n.getElementById("checkout_cancel"),c.focus(),e.preventDefault()}}),document.getElementById("newspack-a11y-last-element").addEventListener("focus",()=>{o.focus()})):(c=n[n.length-1],document.addEventListener("keydown",function(e){("Tab"===e.key||9===e.keyCode)&&(e.shiftKey?document.activeElement===o&&(c.focus(),e.preventDefault()):document.activeElement===c&&(o.focus(),e.preventDefault()))}))}const t=(e,t={})=>({...t,action:e}),n=["action","action_type","amount","currency","product_id","product_type","variation_id","variation_ids","is_variable","is_grouped","child_ids","price_summary","newspack_popup_id","prompt_title","gate_post_id","recurrence","referrer"],o=(e,t="np_modal_checkout_interaction")=>{if("function"==typeof window.gtag&&e){const o={};for(const t of n)e[t]&&("boolean"==typeof e[t]?e[t]=e[t]?"yes":"no":"true"===e[t]?e[t]="yes":"false"===e[t]&&(e[t]="no"),o[t]=e[t].toString());window.gtag("event",t,o)}},c=e=>{if("function"!=typeof window.gtag)return;let n="opened";const{amount:c="",is_variable:a="",is_grouped:r="",price:i="",variation_id:s=""}=e,l={...e};(c||i)&&(l.amount=c||i),a&&!s&&(n="opened_variations"),r&&(n="opened_grouped");const u=t(n,l);o(u)};function a(e,t=null){const n=document.createElement("input");return n.type="hidden",n.name=e,t&&(n.value=t),n}function r(e){e.requestSubmit(e.querySelector('button[type="submit"]'))}function i(e){const t="string"==typeof e?document.getElementById(e):e;if(!t)return console.warn("No container found for checkout data"),{};let n={};if("FORM"===t.tagName){const e=new FormData(t);n=Object.fromEntries(e)}const o=t.dataset.checkout;if(!o)return console.warn("No checkout data found"),n;try{n={...n,...JSON.parse(o)}}catch(e){console.warn("Error parsing checkout data")}const c=new URL(t.ownerDocument.defaultView.location.href);return c.searchParams.get("action_type")&&(n.action_type=c.searchParams.get("action_type")),n}const s=e=>{if("function"!=typeof window.gtag)return;e=e||i("newspack_modal_checkout");const n=t("dismissed",e);o(n)};function l(e){const t=e&&e.dataset?e.dataset.checkout:null;if(!t)return null;try{return JSON.parse(t)}catch(e){return null}}function u(e,t,n=null){const o=e.querySelectorAll(".wp-block-newspack-blocks-checkout-button"),c=null!=n&&""!==String(n);let a=null;return o.forEach(e=>{if(a)return;const o=e.querySelector("form"),r=l(o);r&&String(r.product_id)===String(t)&&(c&&String(r.variation_id)!==String(n)||(a=o))}),a}const d=["after_success_behavior","after_success_url","after_success_button_label","after_success_token","gate_post_id","newspack_popup_id","prompt_title","coupon"];const p={gate:"gate_post_id",prompt:"newspack_popup_id"};const m=newspackBlocksModal.newspack_class_prefix,h="newspack_modal_checkout_iframe",_="newspack_modal_checkout_container",f=`${m}__modal`,w="newspack-blocks__modal-variation",y=`${m}__processing-payment-text`,k=newspackBlocksModal.processing_payment_messages;let v=!1,g=newspackBlocksModal.labels.checkout_modal_title,b=null;const S=e=>{e.overlayId&&window.newspackReaderActivation?.overlays&&window.newspackReaderActivation?.overlays.remove(e.overlayId),e.setAttribute("data-state","closed"),document.body.style.overflow="auto"};window.onpageshow=e=>{e.persisted&&(document.querySelectorAll(".modal-processing").forEach(e=>e.classList.remove("modal-processing")),document.querySelectorAll(".non-modal-checkout-loading").forEach(e=>e.classList.remove("non-modal-checkout-loading")),document.querySelectorAll(`.${f}-container`).forEach(e=>S(e)))};const E=new CustomEvent("checkout-closed");var q;window.newspackRAS=window.newspackRAS||[],q=()=>{const t=document.querySelector("#newspack_modal_checkout");if(!t)return;const n=t.querySelector(`.${f}__content`),o=a("modal_checkout","1"),q=n.querySelector(`.${m}__spinner`);let A=document.querySelector(".newspack-reader__account-link")?.[0];const L="600px",$=document.createElement("iframe");$.name=h,$.style.height=L,$.style.visibility="hidden";const R=e=>{P.observe(e),"none"!==q.style.display&&(q.style.display="none"),"visible"!==$.style.visibility&&($.style.visibility="visible"),$._ready=!0};!function(e,t,n){e._ready=!1,e._readyTimer&&clearTimeout(e._readyTimer);let o=!1,c="";function a(){o||(o=!0,clearTimeout(e._readyTimer),t.call(this))}function r(){"complete"===this.readyState&&a.call(this)}function i(){e._ready=!1,n&&n(),e._readyTimer&&clearTimeout(e._readyTimer);const t=e.contentDocument||e.contentWindow?.document;t&&(t.removeEventListener("DOMContentLoaded",a),t.removeEventListener("readystatechange",r))}function s(){if(e._ready)return void i();const t=e.contentDocument||e.contentWindow?.document;t&&0!==t.URL.indexOf("about:")?"complete"===t?.readyState?a.call(t):(t.addEventListener("DOMContentLoaded",a),t.addEventListener("readystatechange",r)):e._readyTimer=setTimeout(s,10)}e._observer||(e._observer=new MutationObserver(e=>{e.forEach(e=>{"attributes"===e.type&&"src"===e.attributeName&&(o=!1,i(),s())})}),e._observer.observe(e,{attributes:!0})),e._locationObserver||(e._locationObserver=setInterval(function(){const t=e.contentDocument||e.contentWindow?.document;t&&t.URL!==c&&(c=t.URL,o=!1,i(),s())},50)),s()}($,function(){const n=$.contentWindow?.location;if(window.newspackReaderActivation&&n?.href?.includes("order-received")){const e=window.newspackReaderActivation,t=new Proxy(new URLSearchParams(n.search),{get:(e,t)=>e.get(t)});t.email&&(e.setReaderEmail(t.email),e.setAuthenticated(!0))}const o=$?.contentDocument?.querySelector(`#${_}`);if(!o)return;const c=i(o.querySelector("#modal-checkout-product-details")),a=document.createElement("p");a.classList.add(y);let r=[];const s=()=>{r.forEach(e=>clearTimeout(e)),r=[]},l=()=>{q.style.display="none",s(),q.querySelectorAll(`.${y}`).forEach(e=>e.remove())};!function(e,t){e.checkoutCancel?t():e.addEventListener("checkout-cancel",t)}(o,()=>{x()}),function(e,t){e.checkoutPlaceOrderStart?t():e.addEventListener("checkout-place-order-start",t)}(o,e=>{q.querySelectorAll(`.${y}`).forEach(e=>e.remove()),q.style.display="flex",s(),a.textContent=k[0]?.text??"",k.slice(1).forEach(({text:t,delay:n})=>{const o=setTimeout(()=>{e.target.dispatchEvent(new CustomEvent("checkout-place-order-processing")),a.textContent=t},n);r.push(o)}),q.appendChild(a)}),function(e,t){e.checkoutPlaceOrderError?t():e.addEventListener("checkout-place-order-error",t)}(o,l),function(e,t){e.checkoutReady?t():e.addEventListener("checkout-ready",t)}(o,()=>{if(t.initialErrors){const e=document.createElement("div");e.classList.add("woocommerce-error"),e.textContent=t.initialErrors,o.prepend(e),delete t.initialErrors}I(),N(g),$.contentWindow?.newspackBlocksModalCheckout?.checkout_nonce&&(t.checkout_nonce=$.contentWindow.newspackBlocksModalCheckout.checkout_nonce),R(o)}),function(e,t){e.checkoutComplete?t():e.addEventListener("checkout-complete",t)}(o,()=>{window.newspackRAS.push(["checkout_completed",c]),window?.newspackReaderActivation?.refreshNewslettersSignupModal&&window?.newspackReaderActivation?.getReader()?.email&&window.newspackReaderActivation.refreshNewslettersSignupModal(window.newspackReaderActivation.getReader().email),I("small"),N(newspackBlocksModal.labels.thankyou_modal_title),R(o),e(t.querySelector(`.${f}`)),l()}),function(e,t){e.checkoutPlaceOrderCriticalError?t():e.addEventListener("checkout-place-order-critical-error",t)}(o,()=>{var e;(e=b)&&(x(),q.style.display="none",t.initialErrors=newspackBlocksModal.labels.critical_error,e.requestSubmit(e.querySelector('button[type="submit"]')),l())})},()=>{q.style.display="flex"});const M=e=>new Promise((t,n)=>{const o=new URLSearchParams(e);o.append("action","modal_checkout_request"),fetch(newspackBlocksModal.ajax_url+"?"+o.toString()).then(e=>{e.ok||n(e),e.json().then(e=>{t(e.url)}).catch(n)}).catch(n)}),C=()=>"undefined"!=typeof newspack_ras_config&&!newspack_ras_config?.is_logged_in&&!window?.newspackReaderActivation?.getReader?.()?.authenticated&&newspackBlocksModal?.is_registration_required&&window?.newspackReaderActivation?.openAuthModal,B=t=>{const n=!newspackBlocksModal.has_unsupported_payment_gateway;n||t.preventDefault();const o=t.target;o.classList.add("modal-processing"),function(e){if(!e||"function"!=typeof e.querySelector)return!1;if(e.closest(".newspack-content-gate__gate"))return!1;const t=function(){let e,t;try{e=window.sessionStorage.getItem("newspack_cta_attribution")}catch(e){return null}if(!e)return null;try{t=JSON.parse(e)}catch(e){return null}return t&&1===t.v&&p[t.type]&&t.id?"number"!=typeof t.ts||Date.now()-t.ts>18e5?null:t:null}();if(!t)return!1;const n=p[t.type];if(e.querySelector(`input[name="${n}"]`))return!1;const o=document.createElement("input");o.type="hidden",o.name=n,o.value=t.id,e.prepend(o)}(o);const u=i(o);if(u.newspack_donate){const e=u.donation_frequency,t=[...o.querySelectorAll(`.donation-tier__${e}, .donation-frequency__${e}`)],n=u.donation_tier_index;let c,a;n?(c=t[n],a=u[`donation_value_${e}`]):(c=t[0],u[`donation_value_${e}_untiered`]?a=u[`donation_value_${e}_untiered`]:(a=u[`donation_value_${e}`],"other"===a&&(a=u[`donation_value_${e}_other`])));const r=i(c);for(const e in r)u[e]=r[e];u.amount=a,u.price_summary=u.summary_template.replace("{{PRICE}}",function(e,t="USD"){return parseFloat(e).toLocaleString(document.documentElement.lang,{style:"currency",currency:t,currencyDisplay:"narrowSymbol"})}(u.amount,u.currency))}u&&Object.keys(u).forEach(e=>{0===o.querySelectorAll('input[name="'+e+'"]').length&&o.prepend(a(e,u[e]))}),u.variation_id||(A=t.submitter);const m=document.querySelectorAll(`.${w}`);if(m.forEach(e=>{(C()||n)&&S(e)}),u.is_grouped||u.is_variable&&!u.variation_id){const n=[...m].find(e=>e.dataset.productId===u.product_id);if(n)return n.querySelectorAll(`form[target="${h}"]`).forEach(e=>{!function(e,t,n=d){if(!e||!t)return;const o=e.ownerDocument;n.forEach(n=>{e.querySelectorAll(`input[name="${n}"]`).forEach(e=>e.remove());const c=t[n],a=null==c?"":String(c);if(!a)return;const r=o.createElement("input");r.type="hidden",r.name=n,r.value=a,e.prepend(r)})}(e,u);const t=l(e);t&&Object.keys(t).forEach(n=>{0===e.querySelectorAll('input[name="'+n+'"]').length&&e.prepend(a(n,t[n]))})}),t.preventDefault(),o.classList.remove("modal-processing"),T(n),e(n,!1),v||c(u),void document.getElementById("newspack_modal_checkout").setAttribute("data-checkout",JSON.stringify(u))}if(n||C()){if(o.classList.remove("modal-processing"),v||c(u),v=!0,C()){t.preventDefault();const e=M(u);e.then(e=>{window.newspackReaderActivation?.setPendingCheckout?.(e)}),window.newspackReaderActivation.openAuthModal({title:newspackBlocksModal.labels.auth_modal_title,onSuccess:(t,o)=>{e.then(e=>{o?.registered&&n&&(e+=`&${newspackBlocksModal.checkout_registration_flag}=1`),n?r(D(e)):M(u).then(window.location.href=e)}).catch(e=>{console.warn("Unable to generate cart:",e),x()})},onError:()=>{x()},onDismiss:()=>{s(u),v=!1,document.getElementById("newspack_modal_checkout").removeAttribute("data-checkout")},skipSuccess:!0,skipNewslettersSignup:!0,labels:{signin:{title:newspackBlocksModal.labels.signin_modal_title},register:{title:newspackBlocksModal.labels.register_modal_title}},trigger:t.submitter,closeOnSuccess:n})}else O(),document.getElementById("newspack_modal_checkout").setAttribute("data-checkout",JSON.stringify(u));b=o}else M(u).then(e=>{window.location.href=e}),(!u.is_variable||u.variation_id)&&o.querySelectorAll("button[type=submit]:focus").forEach(e=>{e.classList.add("non-modal-checkout-loading");const t=e.innerHTML;e.innerHTML="<span>"+t+"</span>"})},D=e=>{const t=document.createElement("form");t.method="POST",t.action=e,t.target=h,t.style.display="none";const n=document.createElement("button");return n.setAttribute("type","submit"),t.appendChild(n),document.body.appendChild(t),t.addEventListener("submit",B),t},P=new ResizeObserver(e=>{if(!e||!e.length)return;if(!$.contentDocument)return;const o=e[0].contentRect;if(o){const e=.01*Math.max(document.documentElement.clientHeight,window.innerHeight||0)*90-(t.querySelector(`.${f}__header`)?.offsetHeight||0),c=o.top+o.bottom,a=Math.min(c,e);if(0===a)return void($.style.visibility="hidden");n.style.height=a+"px",$.style.height=a+"px"}}),x=()=>{const e=$?.contentDocument?.querySelector(`#${_}`),o=e?.querySelector('input[name="after_success_url"]'),c=e?.querySelector('input[name="after_success_behavior"]'),a=document?.querySelector(".newspack-newsletters-signup-modal"),r=i(e?.querySelector("#modal-checkout-product-details"));e?.checkoutComplete||(async()=>{const e=new FormData;newspackBlocksModal.has_unsupported_payment_gateway||e.append("modal_checkout","1"),e.append("action","abandon_modal_checkout"),e.append("_wpnonce",t.checkout_nonce),t.checkout_nonce=null;try{await fetch(newspackBlocksModal.ajax_url,{method:"POST",body:e})}catch(e){console.warn("Unable to empty cart:",e)}})();const l=!($.contentDocument&&o&&c&&e?.checkoutComplete);if((l||a)&&(q.style.display="flex",$&&n.contains($)&&($._ready=!1,$.src="about:blank",$.style.height=L,$.style.visibility="hidden",n.style.height=L,n.removeChild($)),P&&P.disconnect(),document.querySelectorAll(`.${f}-container`).forEach(e=>S(e)),A&&A.focus(),document.dispatchEvent(E)),e?.checkoutComplete){const e=()=>{if(o&&c){const e=o.getAttribute("value"),t=c.getAttribute("value");if("custom"===t)window.location.href=e;else if("referrer"===t){const e=document.referrer;let t=!1;if(e)try{new URL(e).origin===window.location.origin&&(window.location.href=e,t=!0)}catch(e){}t||window.history.back()}}window?.newspackReaderActivation?.setPendingCheckout?.(),v=!1};"subscription_switch"!==r.action_type&&window?.newspackReaderActivation?.openNewslettersSignupModal?window.newspackReaderActivation.openNewslettersSignupModal({onSuccess:e,onError:e,closeOnSuccess:l}):e(),l&&(g=newspackBlocksModal.labels.checkout_modal_title,I(),N(g))}else window?.newspackReaderActivation?.setPendingCheckout?.(),s(),v=!1,document.getElementById("newspack_modal_checkout").removeAttribute("data-checkout");document.removeEventListener("keydown",U)},O=o=>{o&&($.src=o),q.style.display="flex",T(t),n.appendChild($),t.addEventListener("click",e=>{e.target===t&&x()}),e(t,$),document.addEventListener("keydown",U)},T=e=>{window.newspackReaderActivation?.overlays&&(e.overlayId=window.newspackReaderActivation?.overlays.add()),e.setAttribute("data-state","open"),document.body.style.overflow="hidden"},N=e=>{const n=t.querySelector(`.${f}__header h2`);n&&(n.innerText=e)},I=(e="default")=>{const n=t.querySelector(`.${f}`);n&&("small"===e?n.classList.add(`${f}--small`):n.classList.remove(`${f}--small`))};t.querySelectorAll(`.${f}__close`).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault(),x()})}),document.querySelectorAll(".newspack-blocks__modal-variation").forEach(e=>{e.addEventListener("click",t=>{t.target===e&&x()}),e.querySelectorAll(`.${f}__close`).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault(),x()})})});const U=e=>{"Escape"===e.key&&x()};document.querySelectorAll(".wpbnbd.wpbnbd--platform-wc, .wp-block-newspack-blocks-checkout-button, .newspack-blocks__modal-variation").forEach(e=>{e.querySelectorAll("form").forEach(e=>{newspackBlocksModal.has_unsupported_payment_gateway||e.prepend(o.cloneNode()),e.target=h,e.addEventListener("submit",B)})}),(()=>{const e=new URLSearchParams(window.location.search);if(!e.has("checkout"))return;let t=!0;const n=e.get("type");if("donate"===n){const t=e.get("layout"),n=e.get("frequency"),o=e.get("amount"),c=e.get("other");t&&n&&o&&((e,t,n,o=null)=>{let c;document.querySelectorAll(".wpbnbd.wpbnbd--platform-wc form").forEach(a=>{const r=a.querySelector(`input[name="donation_frequency"][value="${t}"]`);if(r)if("tiered"===e){const e=document.querySelector(`button[data-frequency-slug="${t}"]`);if(!e)return;e.click();const o=a.querySelector(`button[type="submit"][name="donation_value_${t}"][value="${n}"]`);if(!o)return;o.click()}else{const i="untiered"===e?a.querySelector(`input[name="donation_value_${t}_untiered"]`):a.querySelector(`input[name="donation_value_${t}"][value="${n}"]`);if(r&&i){if(r.checked=!0,"untiered"===e)i.value=n;else if("other"===n){i.click();const e=a.querySelector(`input[name="donation_value_${t}_other"]`);e&&o&&(e.value=o)}else i.checked=!0;c=a}}}),c&&r(c)})(t,n,o,c)}else if("checkout_button"===n){const n=e.get("product_id"),o=e.get("variation_id");n?t=((e,t=null)=>{const n=function(e,t,n,o={}){const{allowProductOnlyFallback:c=!1}=o;if(null==n||""===String(n))return u(e,t,null);const a=u(e,t,n);if(a)return a;const r=function(e,t,n,o={}){const{variationModalClassPrefix:c,iframeName:a}=o,r=[...e.querySelectorAll(`.${c}`)].find(e=>String(e.dataset.productId)===String(t));if(!r)return null;const i=r.querySelectorAll("form"),s=a?[...i].find(e=>e.getAttribute("target")===a):i[0];if(!s)return null;const l=[...s.querySelectorAll('input[type="radio"][name="product_id"]')].find(e=>String(e.value)===String(n));return l?(l.checked=!0,s):null}(e,t,n,o);return r?(function(e,t,n=d){if(!e||!t)return;const o=t.ownerDocument,c=new FormData(e);n.forEach(e=>{if(t.querySelector(`input[name="${e}"]`))return;const n=c.getAll(e).filter(e=>"string"==typeof e&&e);if(!n.length)return;const a=o.createElement("input");a.type="hidden",a.name=e,a.value=n[n.length-1],t.prepend(a)})}(function(e,t){const n=e.querySelectorAll(".wp-block-newspack-blocks-checkout-button");let o=null,c=null;return n.forEach(e=>{if(c)return;const n=e.querySelector("form"),a=l(n);a&&String(a.product_id)===String(t)&&(a.variation_id?o=o||n:c=n)}),c||o}(e,t),r),r):c?u(e,t,null):null}(document,e,t,{variationModalClassPrefix:w,iframeName:h});if(n)return r(n),!0;const o=`Newspack modal checkout: no checkout form found for product_id "${e}"`+(t?` and variation_id "${t}"`:"")+". The checkout was not triggered.";return console.warn(o),!1})(n,o):(t=!1,console.warn("Newspack modal checkout: checkout_button trigger is missing product_id. The checkout was not triggered."))}else{const e=window.newspackReaderActivation?.getPendingCheckout?.();e&&r(D(e))}t&&window.history.replaceState(null,null,window.location.pathname)})(),window.newspackOpenModalCheckout=({url:e=null,title:t=null,actionType:n=null,afterSuccess:o={},onCheckoutComplete:c=null,onClose:a=null})=>{if(g=t||newspackBlocksModal.labels.checkout_modal_title,N(g),(e=new URL(e||newspackBlocksModal.checkout_url)).searchParams.has("my_account_checkout")&&e.searchParams.delete("my_account_checkout"),e.searchParams.has("modal_checkout")||e.searchParams.set("modal_checkout","1"),n&&e.searchParams.set("action_type",n),o?.url&&e.searchParams.set("after_success_url",o.url),(o?.behavior||o?.url)&&e.searchParams.set("after_success_behavior",o.behavior||"custom"),o?.buttonLabel&&e.searchParams.set("after_success_button_label",o.buttonLabel),c){const e=({detail:{action:e,data:t}})=>{"checkout_completed"===e&&c(t)};window.newspackRAS.push(t=>{t.on("activity",e);const n=()=>{t.off("activity",e),document.removeEventListener("checkout-closed",n)};document.addEventListener("checkout-closed",n)})}if(a){const e=()=>{a(),document.removeEventListener("checkout-closed",e)};document.addEventListener("checkout-closed",e)}O(e.toString())},window.newspackCloseModalCheckout=x},"undefined"!=typeof document&&("complete"!==document.readyState&&"interactive"!==document.readyState?document.addEventListener("DOMContentLoaded",q):q())})();
@@ -383,6 +383,7 @@ final class Modal_Checkout {
383
383
  $after_success_behavior = filter_input( INPUT_GET, 'after_success_behavior', FILTER_SANITIZE_SPECIAL_CHARS );
384
384
  $after_success_url = filter_input( INPUT_GET, 'after_success_url', FILTER_SANITIZE_URL );
385
385
  $after_success_button_label = filter_input( INPUT_GET, 'after_success_button_label', FILTER_SANITIZE_SPECIAL_CHARS );
386
+ $after_success_token = filter_input( INPUT_GET, 'after_success_token', FILTER_SANITIZE_SPECIAL_CHARS );
386
387
 
387
388
  if ( $variation_id ) {
388
389
  $product_id = $variation_id;
@@ -401,7 +402,7 @@ final class Modal_Checkout {
401
402
  wp_parse_str( $parsed_url['query'], $params );
402
403
  }
403
404
 
404
- $params = array_merge( $params, compact( 'after_success_behavior', 'after_success_url', 'after_success_button_label' ) );
405
+ $params = array_merge( $params, compact( 'after_success_behavior', 'after_success_url', 'after_success_button_label', 'after_success_token' ) );
405
406
 
406
407
  if ( function_exists( 'wpcom_vip_url_to_postid' ) ) {
407
408
  $referer_post_id = wpcom_vip_url_to_postid( $referer );
@@ -1238,6 +1239,346 @@ final class Modal_Checkout {
1238
1239
  ob_end_flush();
1239
1240
  }
1240
1241
 
1242
+ /**
1243
+ * Behaviors that have somewhere to go once checkout finishes.
1244
+ *
1245
+ * Anything else leaves the reader in a modal that won't move and won't close, so it is
1246
+ * treated as "close the modal" instead.
1247
+ *
1248
+ * @var string[]
1249
+ */
1250
+ const AFTER_SUCCESS_BEHAVIORS = [ 'custom', 'referrer' ];
1251
+
1252
+ /**
1253
+ * Reduce a post-checkout destination to a single comparable form.
1254
+ *
1255
+ * Signing and verifying have to agree on the exact string, and the value passes through
1256
+ * sanitising on its way between them, so both sides normalise here rather than each
1257
+ * doing their own.
1258
+ *
1259
+ * @param string $url The requested destination.
1260
+ *
1261
+ * @return string The normalised destination, or an empty string.
1262
+ */
1263
+ public static function normalize_after_success_url( $url ) {
1264
+ $url = sanitize_url( (string) $url );
1265
+
1266
+ if ( empty( $url ) ) {
1267
+ return '';
1268
+ }
1269
+
1270
+ // Core compares hosts case-sensitively; host names aren't. Rebuild from the parsed
1271
+ // parts rather than replacing the host in the string: the host can appear again
1272
+ // inside a query parameter, and a `user@host` authority never matches `://host` at
1273
+ // all, so a string replacement either rewrites too much or nothing.
1274
+ $parts = wp_parse_url( $url );
1275
+ if ( ! empty( $parts['host'] ) && strtolower( $parts['host'] ) !== $parts['host'] ) {
1276
+ $parts['host'] = strtolower( $parts['host'] );
1277
+ $url = self::build_url( $parts );
1278
+ }
1279
+
1280
+ return $url;
1281
+ }
1282
+
1283
+ /**
1284
+ * Reassemble a URL from `wp_parse_url()` parts.
1285
+ *
1286
+ * @param array $parts Parsed URL parts.
1287
+ *
1288
+ * @return string
1289
+ */
1290
+ private static function build_url( $parts ) {
1291
+ $user = $parts['user'] ?? '';
1292
+ $pass = isset( $parts['pass'] ) ? ':' . $parts['pass'] : '';
1293
+ $auth = $user ? $user . $pass . '@' : '';
1294
+
1295
+ // A destination with a host but no scheme is protocol-relative. Losing the `//` here
1296
+ // would turn it into a relative path, which changes where the reader lands and makes
1297
+ // this function non-idempotent — and the token signs a normalised value at mint and
1298
+ // normalises again at read, so idempotence is what makes it verify.
1299
+ $prefix = isset( $parts['scheme'] )
1300
+ ? $parts['scheme'] . '://'
1301
+ : ( isset( $parts['host'] ) ? '//' : '' );
1302
+
1303
+ return $prefix
1304
+ . $auth
1305
+ . ( $parts['host'] ?? '' )
1306
+ . ( isset( $parts['port'] ) ? ':' . $parts['port'] : '' )
1307
+ . ( $parts['path'] ?? '' )
1308
+ . ( isset( $parts['query'] ) ? '?' . $parts['query'] : '' )
1309
+ . ( isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '' );
1310
+ }
1311
+
1312
+ /**
1313
+ * Whether a post is published, read from the stored status.
1314
+ *
1315
+ * `get_post_status()` reports an attachment as `publish` whatever its stored status, so a
1316
+ * token could be minted from one and could not be revoked by unpublishing. Reading the
1317
+ * field directly returns the real status, and also steps around the `get_post_status`
1318
+ * filter, which would otherwise let any plugin move this gate.
1319
+ *
1320
+ * @param int $post_id The post to check.
1321
+ *
1322
+ * @return boolean
1323
+ */
1324
+ private static function is_published_post( $post_id ) {
1325
+ return 'publish' === get_post_field( 'post_status', (int) $post_id );
1326
+ }
1327
+
1328
+ /**
1329
+ * The post a block is being rendered for.
1330
+ *
1331
+ * `get_the_ID()` is empty outside the loop, which is where a block in a widget or a footer
1332
+ * template part renders. The queried object covers that, but only when it is a post: on an
1333
+ * archive it is a term, whose ID would otherwise be read as an unrelated post's.
1334
+ *
1335
+ * @return int Post ID, or 0.
1336
+ */
1337
+ private static function get_after_success_post_id() {
1338
+ $post_id = (int) get_the_ID();
1339
+
1340
+ if ( $post_id ) {
1341
+ return $post_id;
1342
+ }
1343
+
1344
+ $queried = get_queried_object();
1345
+
1346
+ return $queried instanceof WP_Post ? (int) $queried->ID : 0;
1347
+ }
1348
+
1349
+ /**
1350
+ * Sign a value for the post-checkout destination it belongs to.
1351
+ *
1352
+ * Namespaced so this never produces the same digest as another use of the same key —
1353
+ * `wp_hash()` with the auth salt also backs the magic-link and email-change secrets, and
1354
+ * these signatures are published in page HTML.
1355
+ *
1356
+ * @param string $payload The value to sign.
1357
+ *
1358
+ * @return string
1359
+ */
1360
+ private static function sign_after_success_payload( $payload ) {
1361
+ // The namespace prefix is what separates this from every other use of the same key and
1362
+ // algorithm. `Reader_Registration::get_frontend_registration_key()` also produces
1363
+ // `hash_hmac( 'sha256', …, wp_salt( 'auth' ) )` and publishes the result to the front
1364
+ // end, so nothing but this prefix distinguishes the two. Never register an integration
1365
+ // ID that begins with it.
1366
+ //
1367
+ // The blog ID is included because `wp_salt()` is network-scoped: without it, a token
1368
+ // minted on one site of a network verifies on every other site in that network.
1369
+ $namespace = 'newspack_blocks_after_success|' . get_current_blog_id() . '|';
1370
+
1371
+ return hash_hmac( 'sha256', $namespace . $payload, wp_salt( 'auth' ) );
1372
+ }
1373
+
1374
+ /**
1375
+ * Mint a token vouching for a post-checkout destination a block was configured with.
1376
+ *
1377
+ * Minted where the block renders, which is the last point this site knows the destination
1378
+ * came from its own content rather than from the request. Two properties matter:
1379
+ *
1380
+ * The destination travels *inside* the token rather than beside it. The token's alphabet
1381
+ * is URL-safe base64 plus a dot, so the sanitising the value meets in transit — which
1382
+ * variously deletes percent-encoded octets, drops non-ASCII bytes and HTML-encodes `&`
1383
+ * and `'` — cannot alter it. Signing the bare URL and hoping every transit point leaves
1384
+ * it alone is what this avoids.
1385
+ *
1386
+ * The token is bound to the post being rendered, and only published posts are signed, so
1387
+ * a token cannot be minted through the block-renderer REST route or a draft preview by
1388
+ * someone who can edit posts but not publish them.
1389
+ *
1390
+ * `hash_hmac` rather than a nonce: the token has to survive page caching and a reader who
1391
+ * signs in partway through checkout, and a nonce is bound to a user and a time window.
1392
+ *
1393
+ * @param string $url The destination to vouch for.
1394
+ * @param int $post_id The post being rendered. Defaults to the current post.
1395
+ *
1396
+ * @return string The token, or an empty string if there is nothing to vouch for.
1397
+ */
1398
+ public static function get_after_success_token( $url, $post_id = 0 ) {
1399
+ $url = self::normalize_after_success_url( $url );
1400
+
1401
+ if ( '' === $url ) {
1402
+ return '';
1403
+ }
1404
+
1405
+ $post_id = $post_id ? (int) $post_id : self::get_after_success_post_id();
1406
+
1407
+ if ( ! $post_id || ! self::is_published_post( $post_id ) ) {
1408
+ return '';
1409
+ }
1410
+
1411
+ $payload = self::encode_after_success_payload(
1412
+ [
1413
+ 'u' => $url,
1414
+ 'p' => $post_id,
1415
+ ]
1416
+ );
1417
+
1418
+ return $payload . '.' . self::sign_after_success_payload( $payload );
1419
+ }
1420
+
1421
+ /**
1422
+ * Read a destination back out of a token, if this site vouched for it.
1423
+ *
1424
+ * @param string $token The token offered with the request.
1425
+ *
1426
+ * @return string The destination, or an empty string if the token is not this site's.
1427
+ */
1428
+ public static function parse_after_success_token( $token ) {
1429
+ $token = (string) $token;
1430
+
1431
+ if ( '' === $token || false === strpos( $token, '.' ) ) {
1432
+ return '';
1433
+ }
1434
+
1435
+ list( $payload, $signature ) = explode( '.', $token, 2 );
1436
+
1437
+ if ( '' === $payload || '' === $signature ) {
1438
+ return '';
1439
+ }
1440
+
1441
+ if ( ! hash_equals( self::sign_after_success_payload( $payload ), $signature ) ) {
1442
+ return '';
1443
+ }
1444
+
1445
+ $claims = self::decode_after_success_payload( $payload );
1446
+
1447
+ if ( empty( $claims['u'] ) || empty( $claims['p'] ) ) {
1448
+ return '';
1449
+ }
1450
+
1451
+ // Checked at read time as well as at mint time: a post that has since been
1452
+ // unpublished should stop vouching for its destination.
1453
+ if ( ! self::is_published_post( (int) $claims['p'] ) ) {
1454
+ return '';
1455
+ }
1456
+
1457
+ return self::normalize_after_success_url( $claims['u'] );
1458
+ }
1459
+
1460
+ /**
1461
+ * Encode token claims into a form transit cannot alter.
1462
+ *
1463
+ * @param array $claims The claims to encode.
1464
+ *
1465
+ * @return string URL-safe base64.
1466
+ */
1467
+ private static function encode_after_success_payload( $claims ) {
1468
+ return rtrim( strtr( base64_encode( (string) wp_json_encode( $claims ) ), '+/', '-_' ), '=' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1469
+ }
1470
+
1471
+ /**
1472
+ * Decode token claims.
1473
+ *
1474
+ * @param string $payload URL-safe base64.
1475
+ *
1476
+ * @return array The claims, or an empty array if the payload is unreadable.
1477
+ */
1478
+ private static function decode_after_success_payload( $payload ) {
1479
+ $json = base64_decode( strtr( $payload, '-_', '+/' ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
1480
+
1481
+ if ( false === $json ) {
1482
+ return [];
1483
+ }
1484
+
1485
+ $claims = json_decode( $json, true );
1486
+
1487
+ return is_array( $claims ) ? $claims : [];
1488
+ }
1489
+
1490
+ /**
1491
+ * Reduce a post-checkout destination to one this site is willing to send readers to.
1492
+ *
1493
+ * The destination reaches the thank-you page through the request, whether it came from
1494
+ * the block's own settings or from the link the reader followed, and by that point the
1495
+ * two look alike. A signature tells them apart: one is minted when a block renders a
1496
+ * destination a publisher configured, so a destination carrying a valid one is this
1497
+ * site's own and is honoured wherever it points.
1498
+ *
1499
+ * Everything else falls to core's redirect validation, which accepts what
1500
+ * `allowed_redirect_hosts` reports — this site's own host, plus whatever a publisher
1501
+ * adds through that filter, plus anything another plugin has added (`Newspack\NRH`
1502
+ * registers one). A destination arriving in a link with no signature has to clear that
1503
+ * bar, which is what keeps a crafted link from choosing where a reader lands.
1504
+ *
1505
+ * @param string $url The requested destination.
1506
+ * @param string $token Token offered with the request, if any.
1507
+ *
1508
+ * @return string The destination, or an empty string if it is not allowed.
1509
+ */
1510
+ public static function sanitize_after_success_url( $url, $token = '' ) {
1511
+ // A token carries its own destination, so a mutated `after_success_url` alongside it
1512
+ // is ignored rather than compared against.
1513
+ $vouched = self::parse_after_success_token( $token );
1514
+
1515
+ if ( '' !== $vouched ) {
1516
+ // Returned as verified. `wp_sanitize_redirect()` is deliberately not applied to
1517
+ // this branch: it strips `'` and percent-encodes non-ASCII, so it would rewrite a
1518
+ // destination this site has already established is the publisher's own — the same
1519
+ // drift the token exists to prevent. The untrusted branch below still gets it, via
1520
+ // `wp_validate_redirect()`. Both have been through `sanitize_url()` in normalising.
1521
+ return $vouched;
1522
+ }
1523
+
1524
+ $url = self::normalize_after_success_url( $url );
1525
+
1526
+ if ( empty( $url ) ) {
1527
+ return '';
1528
+ }
1529
+
1530
+ $validated = (string) wp_validate_redirect( $url, '' );
1531
+
1532
+ if ( '' === $validated ) {
1533
+ /**
1534
+ * Fires when a post-checkout destination is refused.
1535
+ *
1536
+ * The reader is sent nowhere and the modal simply closes, which looks the same
1537
+ * as a publisher not configuring a destination at all. This is the hook to
1538
+ * watch if you need to tell those two apart.
1539
+ *
1540
+ * @param string $url The refused destination.
1541
+ */
1542
+ do_action( 'newspack_blocks_modal_checkout_after_success_url_rejected', $url );
1543
+ }
1544
+
1545
+ return $validated;
1546
+ }
1547
+
1548
+ /**
1549
+ * Drop an after-success behavior the reader can't act on.
1550
+ *
1551
+ * A behavior with nowhere to go renders a modal that neither navigates nor closes, so
1552
+ * the destination, the behavior and its button label all fall away together and the
1553
+ * reader gets the ordinary "close" button instead of one labelled for a page they
1554
+ * won't be taken to.
1555
+ *
1556
+ * @param array $params After-success params.
1557
+ *
1558
+ * @return array The params, less any the reader can't act on.
1559
+ */
1560
+ public static function filter_after_success_params( $params ) {
1561
+ $behavior = $params['after_success_behavior'] ?? '';
1562
+
1563
+ if ( '' === $behavior ) {
1564
+ return $params;
1565
+ }
1566
+
1567
+ $is_unknown = ! in_array( $behavior, self::AFTER_SUCCESS_BEHAVIORS, true );
1568
+ $has_nowhere = 'custom' === $behavior && empty( $params['after_success_url'] );
1569
+
1570
+ if ( $is_unknown || $has_nowhere ) {
1571
+ unset(
1572
+ $params['after_success_behavior'],
1573
+ $params['after_success_url'],
1574
+ $params['after_success_button_label'],
1575
+ $params['after_success_token']
1576
+ );
1577
+ }
1578
+
1579
+ return $params;
1580
+ }
1581
+
1241
1582
  /**
1242
1583
  * Get after success button params.
1243
1584
  */
@@ -1251,14 +1592,19 @@ final class Modal_Checkout {
1251
1592
  \wp_parse_str( $referrer_query, $request_params );
1252
1593
  }
1253
1594
  }
1254
- return array_filter(
1595
+ $token = isset( $request_params['after_success_token'] ) ? sanitize_text_field( wp_unslash( $request_params['after_success_token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1596
+
1597
+ $params = array_filter(
1255
1598
  [
1256
1599
  'after_success_behavior' => isset( $request_params['after_success_behavior'] ) ? sanitize_text_field( wp_unslash( $request_params['after_success_behavior'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1257
- 'after_success_url' => isset( $request_params['after_success_url'] ) ? sanitize_url( wp_unslash( $request_params['after_success_url'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1600
+ 'after_success_url' => isset( $request_params['after_success_url'] ) ? self::sanitize_after_success_url( wp_unslash( $request_params['after_success_url'] ), $token ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1258
1601
  'after_success_button_label' => isset( $request_params['after_success_button_label'] ) ? sanitize_text_field( wp_unslash( $request_params['after_success_button_label'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1602
+ 'after_success_token' => $token,
1259
1603
  'action_type' => isset( $request_params['action_type'] ) ? sanitize_text_field( wp_unslash( $request_params['action_type'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1260
1604
  ]
1261
1605
  );
1606
+
1607
+ return self::filter_after_success_params( $params );
1262
1608
  }
1263
1609
 
1264
1610
  /**
@@ -7,7 +7,7 @@
7
7
  * Author URI: https://newspack.com/
8
8
  * Text Domain: newspack-blocks
9
9
  * Domain Path: /languages
10
- * Version: 4.30.0-alpha.1
10
+ * Version: 4.30.0-alpha.2
11
11
  *
12
12
  * @package Newspack_Blocks
13
13
  */
@@ -15,7 +15,7 @@
15
15
  define( 'NEWSPACK_BLOCKS__PLUGIN_FILE', __FILE__ );
16
16
  define( 'NEWSPACK_BLOCKS__BLOCKS_DIRECTORY', 'dist/' );
17
17
  define( 'NEWSPACK_BLOCKS__PLUGIN_DIR', plugin_dir_path( NEWSPACK_BLOCKS__PLUGIN_FILE ) );
18
- define( 'NEWSPACK_BLOCKS__VERSION', '4.30.0-alpha.1' );
18
+ define( 'NEWSPACK_BLOCKS__VERSION', '4.30.0-alpha.2' );
19
19
 
20
20
  require_once NEWSPACK_BLOCKS__PLUGIN_DIR . 'includes/class-newspack-blocks.php';
21
21
  require_once NEWSPACK_BLOCKS__PLUGIN_DIR . 'includes/class-newspack-blocks-api.php';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/newspack-blocks",
3
- "version": "4.30.0-alpha.1",
3
+ "version": "4.30.0-alpha.2",
4
4
  "author": "Automattic",
5
5
  "description": "=== Newspack Blocks === Contributors: (this should be a list of wordpress.org userid's) Donate link: https://example.com/ Tags: comments, spam Requires at least: 4.5 Tested up to: 5.1.1 Stable tag: 0.1.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html",
6
6
  "repository": {
@@ -12,6 +12,13 @@
12
12
  class WP_REST_Newspack_Author_List_Controller extends WP_REST_Newspack_Authors_Controller {
13
13
  // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound
14
14
 
15
+ /**
16
+ * Fields returned when a caller doesn't ask for a specific set.
17
+ *
18
+ * @var string[]
19
+ */
20
+ const DEFAULT_FIELDS = [ 'id', 'name', 'bio', 'email', 'social', 'avatar', 'url' ];
21
+
15
22
  /**
16
23
  * Constructs the controller.
17
24
  *
@@ -136,6 +143,10 @@ class WP_REST_Newspack_Author_List_Controller extends WP_REST_Newspack_Authors_C
136
143
  $options['fields'] = explode( ',', $request->get_param( 'fields' ) );
137
144
  }
138
145
 
146
+ // Restricted here rather than in get_all_authors(), which also renders the block on
147
+ // the front end for visitors and must keep returning whatever a publisher publishes.
148
+ $options['fields'] = self::restrict_fields( $options['fields'] ?? self::DEFAULT_FIELDS );
149
+
139
150
  $combined_authors = $this->get_all_authors( $options );
140
151
  $response = new \WP_REST_Response( $combined_authors );
141
152
  $response->header( 'x-wp-total', count( $combined_authors ) );
@@ -157,7 +168,7 @@ class WP_REST_Newspack_Author_List_Controller extends WP_REST_Newspack_Authors_C
157
168
  'avatar_hide_default' => false,
158
169
  'exclude' => [], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
159
170
  'exclude_empty' => false,
160
- 'fields' => [ 'id', 'name', 'bio', 'email', 'social', 'avatar', 'url' ],
171
+ 'fields' => self::DEFAULT_FIELDS,
161
172
  'per_page' => 10,
162
173
  ];
163
174
  $options = wp_parse_args( $options, $default_options );
@@ -57,7 +57,7 @@ function newspack_blocks_render_block_author_list( $attributes ) {
57
57
  'author_type' => $author_type,
58
58
  'author_roles' => $author_roles,
59
59
  'exclude' => $attributes['exclude'], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
60
- 'fields' => [ 'id', 'name', 'bio', 'email', 'social', 'avatar', 'url' ],
60
+ 'fields' => WP_REST_Newspack_Author_List_Controller::DEFAULT_FIELDS,
61
61
  ];
62
62
 
63
63
  if ( $exclude_empty ) {
@@ -12,6 +12,14 @@
12
12
  class WP_REST_Newspack_Authors_Controller extends WP_REST_Controller {
13
13
  // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound
14
14
 
15
+ /**
16
+ * Fields carrying contact details that WordPress reserves for people who manage users.
17
+ *
18
+ * These endpoints are open to anyone who can edit posts, which is a much wider audience
19
+ * than core allows to read an address.
20
+ */
21
+ const USER_MANAGEMENT_FIELDS = [ 'email' ];
22
+
15
23
  /**
16
24
  * Constructs the controller.
17
25
  *
@@ -22,6 +30,41 @@ class WP_REST_Newspack_Authors_Controller extends WP_REST_Controller {
22
30
  $this->rest_base = 'authors';
23
31
  }
24
32
 
33
+ /**
34
+ * Whether the current user may read author contact details.
35
+ *
36
+ * Matches the capabilities core requires before exposing a user's address.
37
+ *
38
+ * @return boolean
39
+ */
40
+ public static function can_read_user_management_fields() {
41
+ // Keep `list_users` first: on multisite, core maps `edit_users` through
42
+ // `manage_network_users`, so a site administrator passes only on `list_users`.
43
+ return current_user_can( 'list_users' ) || current_user_can( 'edit_users' );
44
+ }
45
+
46
+ /**
47
+ * Drop any requested fields the current user is not allowed to read.
48
+ *
49
+ * Applied per request rather than inside the formatting helpers, which also render the
50
+ * front-end author blocks for visitors and must keep returning whatever a publisher has
51
+ * chosen to publish there.
52
+ *
53
+ * `false` is the helpers' "every field" convention, used by the front-end blocks; it is
54
+ * passed through untouched so those renders are unaffected.
55
+ *
56
+ * @param array|false $fields Requested fields, or false for every field.
57
+ *
58
+ * @return array|false Fields the current user may receive.
59
+ */
60
+ public static function restrict_fields( $fields ) {
61
+ if ( ! is_array( $fields ) || self::can_read_user_management_fields() ) {
62
+ return $fields;
63
+ }
64
+
65
+ return array_values( array_diff( $fields, self::USER_MANAGEMENT_FIELDS ) );
66
+ }
67
+
25
68
  /**
26
69
  * Registers the necessary REST API routes.
27
70
  *
@@ -84,6 +127,7 @@ class WP_REST_Newspack_Authors_Controller extends WP_REST_Controller {
84
127
  $per_page = ! empty( $request->get_param( 'perPage' ) ) ? $request->get_param( 'perPage' ) : 10; // Number of results to return per page. This is applied to each query, so the actual number of results returned may be up to 2x this number.
85
128
  $avatar_hide_default = ! empty( $request->get_param( 'avatar_hide_default' ) ) ? true : false; // Hide the default avatar if the user has no custom avatar.
86
129
  $fields = ! empty( $request->get_param( 'fields' ) ) ? explode( ',', $request->get_param( 'fields' ) ) : [ 'id' ]; // Fields to get. Will return at least id.
130
+ $fields = self::restrict_fields( $fields );
87
131
  $include = ! empty( $request->get_param( 'include' ) ) ? explode( ',', $request->get_param( 'include' ) ) : null; // Fetch authors by multiple IDs.
88
132
  $post_id = ! empty( $request->get_param( 'post_id' ) ) ? $request->get_param( 'post_id' ) : 0; // Fetch authors for a specific post (contextual mode).
89
133
 
@@ -146,6 +146,10 @@ function render_callback( $attributes ) {
146
146
  $hidden_fields .= $after_success_behavior ? '<input type="hidden" name="after_success_behavior" value="' . esc_attr( $after_success_behavior ) . '" />' : '';
147
147
  $hidden_fields .= $after_success_button_label ? '<input type="hidden" name="after_success_button_label" value="' . esc_attr( $after_success_button_label ) . '" />' : '';
148
148
  $hidden_fields .= $after_success_url ? '<input type="hidden" name="after_success_url" value="' . esc_attr( $after_success_url ) . '" />' : '';
149
+ // Vouched for here because this is the last point the destination is known to come
150
+ // from the block's own settings rather than from the request.
151
+ $after_success_token = $after_success_url ? Modal_Checkout::get_after_success_token( $after_success_url ) : '';
152
+ $hidden_fields .= $after_success_token ? '<input type="hidden" name="after_success_token" value="' . esc_attr( $after_success_token ) . '" />' : '';
149
153
  }
150
154
  // Always emit the coupon field (not gated on the gateway check): it is
151
155
  // applied server-side for both the modal and the redirect checkout flows.
@@ -212,6 +212,16 @@ abstract class Newspack_Blocks_Donate_Renderer_Base {
212
212
  <input type='hidden' name='<?php echo esc_attr( $param_name ); ?>' value='<?php echo esc_attr( $value ); ?>' />
213
213
  <?php
214
214
  }
215
+
216
+ // Vouched for here because this is the last point the destination is known to come
217
+ // from the block's own settings rather than from the request.
218
+ $after_success_url = isset( $attributes['afterSuccessURL'] ) ? $attributes['afterSuccessURL'] : '';
219
+ $after_success_token = $after_success_url ? \Newspack_Blocks\Modal_Checkout::get_after_success_token( $after_success_url ) : '';
220
+ if ( $after_success_token ) {
221
+ ?>
222
+ <input type='hidden' name='after_success_token' value='<?php echo esc_attr( $after_success_token ); ?>' />
223
+ <?php
224
+ }
215
225
  }
216
226
  return ob_get_clean();
217
227
  }
@@ -246,6 +246,100 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
246
246
  return in_array( $mime_type, array_keys( self::iframe_archive_accepted_file_mimes() ), true );
247
247
  }
248
248
 
249
+ /**
250
+ * Reduce a caller-supplied name to a single safe path segment.
251
+ *
252
+ * Used for names this plugin generates but builds out of caller-supplied parts, such as
253
+ * the destination folder derived from an attachment title.
254
+ *
255
+ * @param string $segment The name to reduce.
256
+ *
257
+ * @return string|false The safe segment, or false if nothing usable is left.
258
+ */
259
+ private function sanitize_path_segment( $segment ) {
260
+ $segment = sanitize_file_name( basename( str_replace( '\\', '/', (string) $segment ) ) );
261
+
262
+ if ( '' === $segment || '.' === $segment || '..' === $segment ) {
263
+ return false;
264
+ }
265
+
266
+ return $segment;
267
+ }
268
+
269
+ /**
270
+ * Reduce a caller-supplied relative path to safe segments, keeping subdirectories.
271
+ *
272
+ * Archive entries may legitimately sit in subdirectories and are referenced by name from
273
+ * the archive's own markup, so the segments are checked rather than rewritten: renaming
274
+ * them would break those references. Anything that could point outside the destination —
275
+ * an upward segment, an absolute path, a drive letter or a null byte — is refused
276
+ * outright rather than stripped, since stripping is what makes layered payloads such as
277
+ * `....//` work.
278
+ *
279
+ * @param string $path The relative path to reduce.
280
+ *
281
+ * @return string|false The safe relative path, or false if it is not usable.
282
+ */
283
+ private function sanitize_relative_path( $path ) {
284
+ $path = str_replace( '\\', '/', (string) $path );
285
+
286
+ if ( '' === $path || false !== strpos( $path, "\0" ) ) {
287
+ return false;
288
+ }
289
+
290
+ // Absolute paths, including Windows drive letters.
291
+ if ( '/' === $path[0] || preg_match( '#^[a-zA-Z]:#', $path ) ) {
292
+ return false;
293
+ }
294
+
295
+ $segments = [];
296
+ foreach ( explode( '/', $path ) as $segment ) {
297
+ if ( '' === $segment || '.' === $segment ) {
298
+ continue;
299
+ }
300
+ if ( '..' === $segment ) {
301
+ return false;
302
+ }
303
+ $segments[] = $segment;
304
+ }
305
+
306
+ return empty( $segments ) ? false : implode( '/', $segments );
307
+ }
308
+
309
+ /**
310
+ * Confirm that a destination resolves inside a directory.
311
+ *
312
+ * A last check on the resolved paths, so that a symlink or a gap in the checks above
313
+ * still cannot place a file outside the directory. The destination itself need not
314
+ * exist yet: the deepest part of it that does exist is what gets resolved, which is
315
+ * what lets this run before anything is created.
316
+ *
317
+ * @param string $directory Absolute path to the directory the write must stay inside.
318
+ * @param string $destination Absolute path to the intended destination.
319
+ *
320
+ * @return boolean
321
+ */
322
+ private function is_inside_directory( $directory, $destination ) {
323
+ $directory = realpath( $directory );
324
+
325
+ if ( ! $directory ) {
326
+ return false;
327
+ }
328
+
329
+ $existing = dirname( $destination );
330
+ while ( ! file_exists( $existing ) && dirname( $existing ) !== $existing ) {
331
+ $existing = dirname( $existing );
332
+ }
333
+
334
+ $resolved = realpath( $existing );
335
+
336
+ if ( ! $resolved ) {
337
+ return false;
338
+ }
339
+
340
+ return 0 === strpos( trailingslashit( $resolved ), trailingslashit( $directory ) );
341
+ }
342
+
249
343
  /**
250
344
  * Receive the iframe content, handle it, and returns the URL and the folder name.
251
345
  *
@@ -363,7 +457,11 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
363
457
  * @return WP_REST_Response
364
458
  */
365
459
  private function process_iframe_document( $request, $document_filename, $media_source_path ) {
366
- if ( ! $this->validate_file_extension( $document_filename, false ) ) {
460
+ // The filename is built from the source attachment's title, so it is only a name once
461
+ // it has been reduced to a single segment.
462
+ $document_filename = $this->sanitize_path_segment( $document_filename );
463
+
464
+ if ( ! $document_filename || ! $this->validate_file_extension( $document_filename, false ) ) {
367
465
  return new WP_Error(
368
466
  'newspack_blocks',
369
467
  sprintf(
@@ -393,6 +491,14 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
393
491
  }
394
492
  }
395
493
 
494
+ if ( ! $this->is_inside_directory( $iframe_upload_dir, $iframe_path ) ) {
495
+ return new WP_Error(
496
+ 'newspack_blocks',
497
+ __( 'Could not upload your document.', 'newspack-blocks' ),
498
+ [ 'status' => '400' ]
499
+ );
500
+ }
501
+
396
502
  // Copy uploaded document file to destination.
397
503
  if ( copy( $media_source_path, $iframe_path ) ) {
398
504
  return [
@@ -434,14 +540,25 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
434
540
  }
435
541
  global $wp_filesystem;
436
542
 
543
+ // The folder is built from the source attachment's title, so it is only a name once it
544
+ // has been reduced to a single segment.
545
+ $iframe_folder = $this->sanitize_path_segment( $iframe_folder );
546
+ if ( ! $iframe_folder ) {
547
+ return $invalid_file_error;
548
+ }
549
+
437
550
  $wp_upload_dir = wp_upload_dir();
438
551
  $iframe_upload_dir = $wp_upload_dir['path'] . self::IFRAME_UPLOAD_DIR;
439
552
  $iframe_path = trailingslashit( $iframe_upload_dir . $iframe_folder );
440
553
  $data = $request->get_body_params();
441
554
 
442
- // create iframe directory if not existing.
443
- if ( ! file_exists( $iframe_upload_dir ) ) {
444
- wp_mkdir_p( $iframe_upload_dir );
555
+ // Create this archive's own directory up front, so each entry's destination can be
556
+ // resolved against it before anything is written.
557
+ if ( ! file_exists( $iframe_path ) ) {
558
+ wp_mkdir_p( $iframe_path );
559
+ }
560
+ if ( ! $this->is_inside_directory( $iframe_upload_dir, $iframe_path ) ) {
561
+ return $invalid_file_error;
445
562
  }
446
563
 
447
564
  // Extract files from archive.
@@ -464,10 +581,24 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
464
581
  }
465
582
  $stat = $zip->statIndex( $file_index );
466
583
  if ( $stat && isset( $stat['name'] ) ) {
467
- if ( ! file_exists( dirname( $iframe_path . $stat['name'] ) ) ) {
468
- wp_mkdir_p( dirname( $iframe_path . $stat['name'] ) );
584
+ // An entry names its own destination, so it decides where the write lands.
585
+ $entry_relative_path = $this->sanitize_relative_path( $stat['name'] );
586
+ if ( ! $entry_relative_path ) {
587
+ return $invalid_file_error;
588
+ }
589
+
590
+ $destination = $iframe_path . $entry_relative_path;
591
+
592
+ // Checked before anything is created, so a destination outside this archive's
593
+ // own folder never gets a directory made for it.
594
+ if ( ! $this->is_inside_directory( $iframe_path, $destination ) ) {
595
+ return $invalid_file_error;
469
596
  }
470
- $put = $wp_filesystem->put_contents( $iframe_path . $stat['name'], $contents );
597
+ if ( ! file_exists( dirname( $destination ) ) ) {
598
+ wp_mkdir_p( dirname( $destination ) );
599
+ }
600
+
601
+ $put = $wp_filesystem->put_contents( $destination, $contents );
471
602
  if ( ! $put ) {
472
603
  return $invalid_file_error;
473
604
  }
@@ -145,6 +145,7 @@ export const PICKER_CONTEXT_FIELDS = [
145
145
  'after_success_behavior',
146
146
  'after_success_url',
147
147
  'after_success_button_label',
148
+ 'after_success_token',
148
149
  'gate_post_id',
149
150
  'newspack_popup_id',
150
151
  'prompt_title',
@@ -366,6 +366,8 @@ domReady( () => {
366
366
  // Fill in the hidden params in the variation modal. The picker is
367
367
  // shared by every button for this product and is never reset, so
368
368
  // this overwrites the previous open's context rather than adding to it.
369
+ // The shared PICKER_CONTEXT_FIELDS list carries `after_success_token`, so
370
+ // the signed destination propagates through the picker with everything else.
369
371
  applyContextFields( singleVariationForm, checkoutData );
370
372
 
371
373
  // Append the product data hidden inputs.
@@ -47,9 +47,25 @@ if ( ! $is_valid ) {
47
47
 
48
48
  $is_success = ! $order->has_status( 'failed' );
49
49
  $after_success_behavior = isset( $_GET['after_success_behavior'] ) ? \sanitize_text_field( \wp_unslash( $_GET['after_success_behavior'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
50
- $after_success_url = isset( $_GET['after_success_url'] ) ? esc_url( \sanitize_url( \wp_unslash( $_GET['after_success_url'] ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
50
+ $after_success_token = isset( $_GET['after_success_token'] ) ? \sanitize_text_field( \wp_unslash( $_GET['after_success_token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
51
+ // Not escaped here: the value is escaped where it is output, and escaping it into the
52
+ // variable would leave it holding something other than the destination that was verified.
53
+ $after_success_url = isset( $_GET['after_success_url'] ) ? \Newspack_Blocks\Modal_Checkout::sanitize_after_success_url( \sanitize_url( \wp_unslash( $_GET['after_success_url'] ) ), $after_success_token ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
51
54
  $after_success_label = isset( $_GET['after_success_button_label'] ) ? \sanitize_text_field( \wp_unslash( $_GET['after_success_button_label'] ) ) : \Newspack_Blocks\Modal_Checkout::get_modal_checkout_labels( 'after_success' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
52
55
  $checkout_data = Checkout_Data::get_checkout_data( $order );
56
+
57
+ // A behavior the reader can't act on falls back to closing the modal, so they get a
58
+ // finished checkout rather than a button that goes nowhere.
59
+ $after_success_params = \Newspack_Blocks\Modal_Checkout::filter_after_success_params(
60
+ [
61
+ 'after_success_behavior' => $after_success_behavior,
62
+ 'after_success_url' => $after_success_url,
63
+ 'after_success_button_label' => $after_success_label,
64
+ ]
65
+ );
66
+ $after_success_behavior = $after_success_params['after_success_behavior'] ?? '';
67
+ $after_success_url = $after_success_params['after_success_url'] ?? '';
68
+ $after_success_label = $after_success_params['after_success_button_label'] ?? \Newspack_Blocks\Modal_Checkout::get_modal_checkout_labels( 'after_success' );
53
69
  ?>
54
70
  <div class="woocommerce-order">
55
71
  <?php if ( $is_success ) : ?>
@@ -3,7 +3,7 @@
3
3
  'name' => 'automattic/newspack-blocks',
4
4
  'pretty_version' => 'dev-main',
5
5
  'version' => 'dev-main',
6
- 'reference' => 'a3e37c3b8a2384695552e701c8b3ad38315ea641',
6
+ 'reference' => '676b21e16de65c4c31fba7e48dcf23dd93dfcd1c',
7
7
  'type' => 'wordpress-plugin',
8
8
  'install_path' => __DIR__ . '/../../',
9
9
  'aliases' => array(),
@@ -13,7 +13,7 @@
13
13
  'automattic/newspack-blocks' => array(
14
14
  'pretty_version' => 'dev-main',
15
15
  'version' => 'dev-main',
16
- 'reference' => 'a3e37c3b8a2384695552e701c8b3ad38315ea641',
16
+ 'reference' => '676b21e16de65c4c31fba7e48dcf23dd93dfcd1c',
17
17
  'type' => 'wordpress-plugin',
18
18
  'install_path' => __DIR__ . '/../../',
19
19
  'aliases' => array(),