diff --git a/plugin/plan-your-day/assets/js/plan.js b/plugin/plan-your-day/assets/js/plan.js index fb4b8ce..be56978 100644 --- a/plugin/plan-your-day/assets/js/plan.js +++ b/plugin/plan-your-day/assets/js/plan.js @@ -1008,14 +1008,19 @@ browse: config.initialData?.browse || {}, route: config.initialData?.route || {}, }; + const canBootstrapEndpointToken = + typeof config.rest?.bootstrapUrl === 'string' && + config.rest.bootstrapUrl !== ''; + let endpointToken = typeof config.rest?.endpointToken === 'string' ? config.rest.endpointToken : ''; + let hasBootstrappedEndpointToken = false; + let endpointTokenRequest = null; const hasRestConfig = refs.form instanceof HTMLFormElement && typeof config.rest?.browseUrl === 'string' && config.rest.browseUrl !== '' && typeof config.rest?.routeUrl === 'string' && config.rest.routeUrl !== '' && - typeof config.rest?.endpointToken === 'string' && - config.rest.endpointToken !== ''; + (endpointToken !== '' || canBootstrapEndpointToken); const shouldHydrateOnLoad = Boolean(config.hydration?.shouldHydrateOnLoad); const colorModeDefault = normalizeColorModeDefault( config.colorModeDefault || root.getAttribute('data-plan-color-mode-default') @@ -1190,6 +1195,58 @@ animateStartPanel(false); }; + const ensureEndpointToken = async () => { + if (!canBootstrapEndpointToken) { + return endpointToken; + } + + if (hasBootstrappedEndpointToken && endpointToken !== '') { + return endpointToken; + } + + if (endpointTokenRequest) { + return endpointTokenRequest; + } + + endpointTokenRequest = fetch(config.rest.bootstrapUrl, { + method: 'POST', + credentials: 'same-origin', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }) + .then(async (response) => { + const responseBody = await response.json().catch(() => ({})); + debugLog(config, response.ok ? 'info' : 'warn', 'request:bootstrap', { + status: response.status, + ok: response.ok, + body: responseBody, + }); + + if (!response.ok) { + throw new Error(responseBody?.message || strings.requestFailed || ''); + } + + const freshToken = String(responseBody?.endpointToken || ''); + + if (freshToken === '') { + throw new Error(strings.requestFailed || ''); + } + + endpointToken = freshToken; + hasBootstrappedEndpointToken = true; + + return endpointToken; + }) + .finally(() => { + endpointTokenRequest = null; + }); + + return endpointTokenRequest; + }; + const sendRequest = async (endpointKey, payload, requestOptions = {}) => { if (!hasRestConfig) { return 'unsupported'; @@ -1259,9 +1316,15 @@ }); try { + const requestEndpointToken = await ensureEndpointToken(); + + if (requestEndpointToken === '') { + throw new Error(strings.requestFailed || ''); + } + const requestBody = { ...payload, - endpoint_token: config.rest.endpointToken, + endpoint_token: requestEndpointToken, }; if (endpointKey === 'browse') { diff --git a/plugin/plan-your-day/assets/js/plan.min.js b/plugin/plan-your-day/assets/js/plan.min.js index b622135..46565b4 100644 --- a/plugin/plan-your-day/assets/js/plan.min.js +++ b/plugin/plan-your-day/assets/js/plan.min.js @@ -1 +1 @@ -(()=>{const t="planYourDayEnhanced",e="planYourDayColorMode",a="light",n="dark",r="system",o=[a,n],s=[...o,r],i="checking",l="found",u="not_found",d=[i,l,u],c=t=>String(t??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"),p=(t,e={})=>{let a=String(t||"");return Object.entries(e||{}).forEach((([t,e])=>{a=a.split(`{${t}}`).join(String(e??""))})),a},y=(t,e="")=>Array.isArray(t)?t.map((t=>y(t))):t&&"object"==typeof t?Object.fromEntries(Object.entries(t).map((([t,e])=>{const a=String(t).toLowerCase();return[t,a.includes("token")||a.includes("api_key")||a.includes("authorization")||a.includes("cookie")||a.includes("secret")?"[redacted]":y(e,t)]}))):"string"==typeof t?String(e).toLowerCase().includes("token")?"[redacted]":t.replace(/([?&](?:key|api_key|token)=)[^&]+/gi,"$1[redacted]"):t,g=(t,e,a,n={})=>{if(!t?.debug||"undefined"==typeof console)return;("function"==typeof console[e]?console[e]:console.log).call(console,`[plan-your-day] ${a}`,y(n))},m=t=>Array.isArray(t)?t.map((t=>String(t??""))).filter(Boolean):[],S=(t,e)=>{t&&e&&(t.textContent="",window.requestAnimationFrame((()=>{t.textContent=e})))},f=t=>o.includes(String(t||"")),b=t=>{const e=String(t||"");return s.includes(e)?e:a},h=()=>{try{const t=window.localStorage?.getItem(e);return f(t)?t:""}catch(t){return""}},w=()=>"undefined"==typeof window||"function"!=typeof window.matchMedia?null:window.matchMedia("(prefers-color-scheme: dark)"),v=(t,e=w())=>{const o=h();if(f(o))return o;const s=b(t);return s===r?e?.matches?n:a:s},M=(t,e,r,o)=>{const s=f(r)?r:a;t.setAttribute("data-plan-color-mode",s),((t,e,a)=>{t.colorModeToggle instanceof HTMLButtonElement&&(t.colorModeToggle.hidden=!1,t.colorModeToggle.setAttribute("aria-pressed",String(e===n)),t.colorModeToggle.setAttribute("aria-label",String(a.darkModeLabel||"Dark mode")),t.colorModeToggleLabel instanceof HTMLElement&&(t.colorModeToggleLabel.textContent=String(a.darkModeLabel||"Dark mode")))})(e,s,o)},L=t=>{const e=t.find((t=>t.checked));return e?e.value:""},R=t=>{const e=String(t||"");return d.includes(e)?e:""},_=(t,e)=>{const a=t.form instanceof HTMLFormElement?new FormData(t.form):new FormData;return{category:String(a.get("category")||e.category||""),category_search:String(a.get("category_search")||e.categorySearch||""),waypoints:a.getAll("waypoints[]").map((t=>String(t||""))).filter(Boolean),start_mode:String(a.get("start_mode")||e.startMode||"default"),custom_start:String(a.get("custom_start")||"")}},E=(t,e,a)=>{const n=String(t?.id||""),r=String(t?.label||""),o=String(t?.address||""),s=String(t?.distance_label||""),i=String(t?.maps_uri||""),l=e.includes(n);return`\n
  • \n
    \n

    ${c(r)}

    \n ${s?`

    ${c(s)}

    `:""}\n

    ${c(o)}

    \n
    \n
    \n ${i?`${c(a.viewInGoogleMaps||"")}`:""}\n ${l?`${c(a.inTrip||"")}`:``}\n
    \n
  • \n `},A=(t,e,a=!1)=>{if(!t?.hasMoreResults)return"";const n=String(a?e.loadingMoreResults||e.moreResultsButton||"":e.moreResultsButton||"");return`\n
    \n \n
    \n `},q=(t,e,a,n={})=>`\n
    \n ${((t,e,a)=>{const n=Array.isArray(t?.searchResults)?t.searchResults:[];if(0===n.length){const e=t?.resultsEmptyState||{};return`\n
    \n

    ${c(e.heading||"")}

    \n

    ${c(e.body||"")}

    \n
    \n `}return`\n \n `})(t,e,a)}\n
    \n ${A(t,a,Boolean(n.isLoadingMore))}\n `,T=(t,e)=>{const a=new Set((t=>{const e=[];return(Array.isArray(t?.searchResults)?t.searchResults:[]).forEach((t=>{const a=String(t?.id||"");a&&!e.includes(a)&&e.push(a)})),e})(t));return(Array.isArray(e?.searchResults)?e.searchResults:[]).filter((t=>{const e=String(t?.id||"");return!e||!a.has(e)&&(a.add(e),!0)}))},H=(t,e)=>{t.categoryInput&&(t.categoryInput.value=e.category||""),t.waypointInputs&&(t.waypointInputs.innerHTML=m(e.route?.selectedWaypointIds).map((t=>``)).join(""))},$=(t,e)=>{if(!t.messages)return;const a=Array.isArray(e)?e:[];t.messages.hidden=0===a.length,t.messages.innerHTML=(t=>(Array.isArray(t)?t:[]).map((t=>{const e=String(t?.type||"note"),a=String(t?.text||""),n="warning"===e?"alert":"";return`
  • ${c(a)}
  • `})).join(""))(a)},k=(t,e,a,n)=>{if(!(t instanceof HTMLElement))return;const r=t.querySelector("[data-plan-load-more-wrap]"),o=A(e,a,n);o?r instanceof HTMLElement?r.outerHTML=o:t.insertAdjacentHTML("beforeend",o):r instanceof HTMLElement&&r.remove()},x=(t,e,a,n,r={})=>{if(!(t instanceof HTMLElement))return;const o=Boolean(r.appendResults),s=Array.isArray(r.appendedResults)?r.appendedResults:[];o&&(s.length>0&&((t,e,a,n)=>{if(!(t instanceof HTMLElement&&Array.isArray(e)&&0!==e.length))return!1;const r=t.querySelector("[data-plan-results-list]");if(!(r instanceof HTMLElement))return!1;const o=t.querySelector("[data-plan-results-empty]");return o instanceof HTMLElement&&o.remove(),r.insertAdjacentHTML("beforeend",e.map((t=>E(t,a,n))).join("")),!0})(t,s,a,n)||0===s.length&&(t=>t instanceof HTMLElement&&t.querySelector("[data-plan-results-list], [data-plan-results-empty]")instanceof HTMLElement)(t))?k(t,e,n,Boolean(r.isLoadingMore)):t.innerHTML=q(e,a,n,r)},C=(t,e,a,n={})=>{const r=e.category||"",o=e.expandedCategory||"",s=m(e.route?.selectedWaypointIds),i=e.browse||{},l=Boolean(i.hasSearch)&&!r,u=l||0===t.categoryButtons.length,d=l&&Boolean(e.customResultsExpanded)||!l&&0===t.categoryButtons.length,c={appendResults:Boolean(n.appendResults),appendedResults:Array.isArray(n.appendedResults)?n.appendedResults:[],isLoadingMore:Boolean(n.isLoadingMore)};t.categoryButtons.forEach((t=>{const e=t.getAttribute("data-category-key")||"",a=e===r&&e===o,n=t.closest(".plan-your-day__category-accordion-item");t.setAttribute("aria-expanded",String(a)),n instanceof HTMLElement&&n.classList.toggle("is-expanded",a)})),t.categoryRegions.forEach((t=>{const e=t.getAttribute("data-category-key")||"",n=e===r&&e===o,l=t.querySelector("[data-plan-category-results-panel]");if(t.hidden=!n,l instanceof HTMLElement){if(!n)return void(l.innerHTML="");x(l,i,s,a,c)}})),t.customResults&&(t.customResults.hidden=!u,t.customResults.classList.toggle("is-expanded",d)),t.customResultsButton&&t.customResultsButton.setAttribute("aria-expanded",String(d)),t.customResultsRegion&&(t.customResultsRegion.hidden=!d),t.customResultsHeading&&(t.customResultsHeading.textContent=l?p(a.searchResultsFor||"",{search:i.categoryLabel||""}):String(i?.resultsEmptyState?.heading||"")),t.customResultsDescription&&(t.customResultsDescription.textContent=String(l?a.customSearchResultsDescription||"":i?.resultsEmptyState?.body||"")),t.customResultsPanel&&(d?l?x(t.customResultsPanel,i,s,a,c):t.customResultsPanel.innerHTML=q(i,s,a,{isLoadingMore:!1}):t.customResultsPanel.innerHTML="")},I=(t,e,a)=>{t.tripHeaderActions&&(t.tripHeaderActions.innerHTML=((t,e)=>{const a=m(t?.selectedWaypointIds),n=String(t?.tripCountLabel||"");return`\n ${c(n)}\n ${a.length>0?``:""}\n `})(e.route,a)),t.tripRegion&&(t.tripRegion.innerHTML=((t,e,a)=>{const n=Array.isArray(t?.tripWaypoints)?t.tripWaypoints:[];if(0===n.length){const a=t?.tripEmptyState||{};return`\n
    \n

    ${c(a.heading||e.tripEmptyHeading||"")}

    \n

    ${c(a.body||e.tripEmptyBody||"")}

    \n
    \n `}return`\n
      \n ${n.map(((t,a)=>{const r=String(t?.id||""),o=String(t?.label||""),s=String(t?.address||""),i=a>0,l=a\n
      \n \n
      \n

      ${c(o)}

      \n

      ${c(s)}

      \n
      \n
      \n
      \n \n ${c(e.moveUp||"")}\n \n \n ${c(e.moveDown||"")}\n \n \n
      \n \n `})).join("")}\n
    \n `})(e.route,a,t.tripRegion.getAttribute("data-plan-trip-help-id")||""))},B=(t,e,a)=>{const n=e.route||{},r=String(n.iframeSrc||""),o=n.emptyPreviewState||{},s=String(n.mapsUrl||""),i=m(n.selectedWaypointIds).length>0;$(t,n.messages),t.mapWrap&&(t.mapWrap.hidden=""===r),t.iframe&&(t.iframe.src=r),t.previewEmpty&&(t.previewEmpty.hidden=""!==r),t.previewEmptyHeading&&(t.previewEmptyHeading.textContent=String(o.heading||"")),t.previewEmptyBody&&(t.previewEmptyBody.textContent=String(o.body||"")),t.summaryCount&&(t.summaryCount.textContent=String(n.tripCountLabel||""),t.summaryCount.hidden=i),t.openLinkLabel&&(t.openLinkLabel.textContent=String(n.mapsLinkLabel||"")),t.openLink&&(t.openLink.hidden=!i,t.openLink.classList.toggle("is-disabled",""===s),s?(t.openLink.href=s,t.openLink.removeAttribute("aria-disabled"),t.openLink.removeAttribute("tabindex"),t.openLink.removeAttribute("role")):(t.openLink.removeAttribute("href"),t.openLink.setAttribute("aria-disabled","true"),t.openLink.setAttribute("tabindex","0"),t.openLink.setAttribute("role","button")))},D=t=>t instanceof HTMLElement&&"function"==typeof t.focus&&(t.focus(),document.activeElement===t),F=t=>{if(!(t instanceof HTMLButtonElement))return null;if(t.matches('[data-plan-action="add-waypoint"]'))return{action:"add-waypoint",placeId:t.getAttribute("data-place-id")||t.value||""};if(t.matches('[data-plan-action="remove-waypoint"]'))return{action:"remove-waypoint",placeId:t.getAttribute("data-place-id")||t.value||""};if(t.matches("[data-plan-clear-trip]"))return{action:"clear-trip",placeId:""};if("move_waypoint"===t.name&&t.value){const[e,a]=String(t.value).split(":",2);return{action:"move-waypoint",placeId:e||"",direction:a||""}}return null},P=t=>0===m(t?.selectedWaypointIds).length,U=(t,e,a)=>{const n=R(e);t.customStartWrap instanceof HTMLElement&&t.customStartWrap.setAttribute("data-plan-custom-start-state",n),t.customStartStatus instanceof HTMLElement&&(t.customStartStatus.textContent=((t,e)=>t===i?String(e.customStartChecking||"Checking starting address."):t===l?String(e.customStartFound||"Starting address found. Results are ready."):t===u?String(e.customStartNotFound||"Starting address was not found."):"")(n,a))},W=(t,e,a)=>{t.startModeInputs.forEach((t=>{t.checked=t.value===e.startMode})),t.customStartInput&&(t.customStartInput.value=e.customStart||"");const n="custom"===(L(t.startModeInputs)||e.startMode||"default");t.customStartWrap&&(t.customStartWrap.hidden=!n),t.customStartInput&&(t.customStartInput.disabled=!n),U(t,n?e.customStartStatus:"",a)},j=(t,e)=>{if(!(t.categorySearchInput instanceof HTMLInputElement))return;const a=String(e.categorySearch||"");t.categorySearchInput.value!==a&&(t.categorySearchInput.value=a)},K=(t,e)=>{t.classList.toggle("is-submitting",e),t.setAttribute("aria-busy",String(e))},O=(t,e,a)=>{t.forEach((t=>{if(!(t instanceof HTMLButtonElement||t instanceof HTMLInputElement))return;if(e)return t.hasAttribute(a)||t.setAttribute(a,t.disabled?"true":"false"),void(t.disabled=!0);const n=t.getAttribute(a);null!==n&&(t.disabled="true"===n,t.removeAttribute(a))}))},N=(t,e)=>{O(t.querySelectorAll("[data-plan-route-mutation]"),e,"data-plan-disabled-before-request")},G=(t,e)=>{O(t.querySelectorAll(['[data-plan-form] button[type="submit"]:not([data-plan-route-mutation])',"[data-plan-load-more-button]","[data-plan-start-toggle]","[data-plan-custom-results-button]",'input[name="start_mode"]',"[data-plan-custom-start]","[data-plan-category-search]"].join(",")),e,"data-plan-browse-disabled-before-request")},J=(t,e,a)=>{const n=e.category||"";if(t.categoryPanels.forEach((e=>{const r=e.getAttribute("data-category-key")||"",o=0===t.categoryButtons.length||r===n||!1===t.customResults?.hidden;e.setAttribute("aria-busy",String(a&&o))})),t.customResultsPanel instanceof HTMLElement){const e=!1===t.customResults?.hidden;t.customResultsPanel.setAttribute("aria-busy",String(a&&e))}t.tripRegion instanceof HTMLElement&&t.tripRegion.setAttribute("aria-busy",String(a)),t.previewCard instanceof HTMLElement&&t.previewCard.setAttribute("aria-busy",String(a))},Y=o=>{if(!(o instanceof HTMLElement)||"true"===o.dataset[t])return;o.dataset[t]="true";const s=(t=>{const e=t.querySelector("[data-plan-config]");if(!e)return{};try{return JSON.parse(e.textContent||"{}")}catch(t){return{}}})(o),l=s.strings||{},u={form:o.querySelector("[data-plan-form]"),liveRegion:o.querySelector("[data-plan-live-region]"),categoryInput:o.querySelector("[data-plan-category-input]"),waypointInputs:o.querySelector("[data-plan-waypoint-inputs]"),categoryButtons:Array.from(o.querySelectorAll("[data-plan-category-button]")),categoryItems:Array.from(o.querySelectorAll("[data-plan-category-item]")),categoryRegions:Array.from(o.querySelectorAll("[data-plan-category-region]")),categoryPanels:Array.from(o.querySelectorAll("[data-plan-category-results-panel]")),categorySearchInput:o.querySelector("[data-plan-category-search]"),customResults:o.querySelector("[data-plan-custom-results]"),customResultsButton:o.querySelector("[data-plan-custom-results-button]"),customResultsHeading:o.querySelector("[data-plan-custom-results-heading]"),customResultsDescription:o.querySelector("[data-plan-custom-results-description]"),customResultsRegion:o.querySelector("[data-plan-custom-results-region]"),customResultsPanel:o.querySelector("[data-plan-custom-results-panel]"),resultsHeading:o.querySelector("[data-plan-results-heading]"),startModeInputs:Array.from(o.querySelectorAll('input[name="start_mode"]')),customStartWrap:o.querySelector("[data-plan-custom-start-wrap]"),customStartInput:o.querySelector("[data-plan-custom-start]"),customStartStatus:o.querySelector("[data-plan-custom-start-status]"),startToggle:o.querySelector("[data-plan-start-toggle]"),startToggleLabel:o.querySelector("[data-plan-start-toggle-label]"),startPanel:o.querySelector("[data-plan-start-panel]"),colorModeToggle:o.querySelector("[data-plan-color-mode-toggle]"),colorModeToggleLabel:o.querySelector("[data-plan-color-mode-toggle-label]"),tripHeaderActions:o.querySelector("[data-plan-trip-header-actions]"),tripHeading:o.querySelector("[data-plan-trip-heading]"),tripRegion:o.querySelector("[data-plan-trip-region]"),messages:o.querySelector("[data-plan-messages]"),previewCard:o.querySelector("[data-plan-preview-card]"),mapWrap:o.querySelector("[data-plan-map-wrap]"),iframe:o.querySelector("[data-plan-iframe]"),previewEmpty:o.querySelector("[data-plan-preview-empty]"),previewEmptyHeading:o.querySelector("[data-plan-preview-empty-heading]"),previewEmptyBody:o.querySelector("[data-plan-preview-empty-body]"),summaryCount:o.querySelector("[data-plan-summary-count]"),openLink:o.querySelector("[data-plan-open-link]"),openLinkLabel:o.querySelector("[data-plan-open-link-label]")},d={category:String(s.initialState?.category||""),categorySearch:String(s.initialState?.categorySearch||""),startMode:String(s.initialState?.startMode||"default"),customStart:String(s.initialState?.customStart||""),customStartStatus:R(s.initialData?.browse?.customStartStatus||""),expandedCategory:String(s.initialState?.category||""),customResultsExpanded:Boolean(s.initialData?.browse?.isCustomSearch),isLoadingMore:!1,browse:s.initialData?.browse||{},route:s.initialData?.route||{}},c=u.form instanceof HTMLFormElement&&"string"==typeof s.rest?.browseUrl&&""!==s.rest.browseUrl&&"string"==typeof s.rest?.routeUrl&&""!==s.rest.routeUrl&&"string"==typeof s.rest?.endpointToken&&""!==s.rest.endpointToken,y=Boolean(s.hydration?.shouldHydrateOnLoad),m=b(s.colorModeDefault||o.getAttribute("data-plan-color-mode-default")),E=w();let A=!0,q=null,k="",x=0,U=null;const O="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches;let Y=0;M(o,u,v(m,E),l),m===r&&E&&E.addEventListener("change",(()=>{h()||M(o,u,v(m,E),l)}));const z=()=>{C(u,d,l,{isLoadingMore:d.isLoadingMore}),I(u,d,l),B(u,d),H(u,d),W(u,d,l),j(u,d),N(o,!1)},Q=(t,e="")=>{$(u,[{type:"warning",text:t||l.requestFailed||""}]),S(u.liveRegion,e||t||l.requestFailed||"")},V=(t={})=>{if(!u.startToggle||!u.startPanel)return;const e=!1!==t.syncHidden;u.startToggle.hidden=!1,u.startToggle.setAttribute("aria-expanded",String(A)),u.startToggle.classList.toggle("is-collapsed",!A),e&&(u.startPanel.hidden=!A),u.startToggleLabel&&(u.startToggleLabel.textContent=String(A?l.hideStartOptions||"Hide options":l.showStartOptions||"Show options"))},X=t=>{if(!(u.startPanel instanceof HTMLElement&&u.startToggle))return A=t,void V();const e=u.startPanel,a=O?0:480;Y&&(window.cancelAnimationFrame(Y),Y=0);const n=e.hidden?0:e.getBoundingClientRect().height;e.hidden=!1,t&&(e.style.height="");const r=t?e.scrollHeight:0,o=t&&r>0?Math.max(n/r,0):1,s=t?1:0;if(A=t,e.style.overflow="hidden",e.style.pointerEvents="none",e.style.height=`${n}px`,e.style.opacity=String(o),V({syncHidden:!1}),a<=0||n===r)return e.style.height="",e.style.overflow="",e.style.pointerEvents="",e.style.opacity="",e.hidden=!t,void V();const i=performance.now(),l=u=>{const d=Math.min((u-i)/a,1),c=(p=d)<.5?4*p*p*p:1-Math.pow(-2*p+2,3)/2;var p;const y=n+(r-n)*c,g=o+(s-o)*c;e.style.height=`${Math.max(y,0)}px`,e.style.opacity=String(Math.max(Math.min(g,1),0)),d<1?Y=window.requestAnimationFrame(l):(e.style.height="",e.style.overflow="",e.style.pointerEvents="",e.style.opacity="",e.hidden=!t,V(),Y=0)};Y=window.requestAnimationFrame(l)},Z=async(t,e,a={})=>{if(!c)return"unsupported";const n=Boolean(a.appendBrowseResults),r=String(a.announcementMessage||""),y=String(a.errorMessage||""),m=String(a.searchContextKey||""),f="browse"===t&&!1!==a.refreshRoute,b="route"===t?a.routeFocusRequest??null:null,h=String(e.start_mode||""),w=String(e.custom_start||""),v="browse"===t&&!n&&"custom"===h&&""!==w.trim(),M="route"===t&&(d.customStartStatus===i||"custom"!==h||""===w.trim()||w!==String(d.customStart||""));if(q instanceof AbortController){if("route"===k)return g(s,"info","request:blocked",{endpointKey:t,blockedBy:k}),"busy";q.abort()}x+=1;const L=x;"route"===t&&(U=b?{requestId:L,focusRequest:b}:null),q=new AbortController,k=t,K(o,!0),J(u,d,!0),N(o,"route"===t),G(o,"route"===t),v?(d.customStartStatus=i,W(u,d,l)):("browse"===t&&!n||M)&&(d.customStartStatus="",W(u,d,l)),g(s,"info","request:start",{endpointKey:t,payload:e});try{const a={...e,endpoint_token:s.rest.endpointToken};"browse"===t&&(a.refresh_route=f,""!==m&&(a.search_context_key=m));const i=await fetch(s.rest["browse"===t?"browseUrl":"routeUrl"],{method:"POST",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(a),signal:q.signal}),c=await i.json().catch((()=>({})));if(g(s,i.ok?"info":"warn","request:response",{endpointKey:t,status:i.status,ok:i.ok,body:c}),!i.ok)throw new Error(c?.message||l.requestFailed||"");if(L!==x)return!0;if("browse"===t){const t=d.category||"",a=d.expandedCategory||"",r=d.categorySearch||"",s=c?.browse||{};if(n&&""!==String(s.searchResultsError||""))return d.isLoadingMore=!1,C(u,d,l,{appendResults:!0,appendedResults:[],isLoadingMore:!1}),Q(y||l.requestFailed||"",y||l.requestFailed||""),"failed";const i=n&&""!==m&&m===String(d.browse?.searchContextKey||"")&&m===String(s.searchContextKey||""),g=i?T(d.browse,s):[];if(d.browse=i?{...(_=d.browse)||{},...(E=s)||{},searchResults:[...Array.isArray(_?.searchResults)?_.searchResults:[],...T(_,E)]}:s,d.route=c?.route||d.route||{},d.category=String(d.browse.categoryKey||e.category||""),d.categorySearch=String(d.browse.categorySearch||e.category_search||""),d.customStartStatus=R(d.browse.customStartStatus||""),d.isLoadingMore=!1,d.category?(d.expandedCategory=d.category===t?a:d.category,d.customResultsExpanded=!1):d.browse.hasSearch?String(e.category_search||"")!==r&&(d.expandedCategory="",d.customResultsExpanded=!0):(d.expandedCategory="",d.customResultsExpanded=!1),i)return C(u,d,l,{appendResults:!0,appendedResults:g,isLoadingMore:!1}),I(u,d,l),B(u,d),H(u,d),W(u,d,l),j(u,d),N(o,!1),S(u.liveRegion,d.browse?.searchResultsError?y||l.requestFailed||"":((t,e,a)=>t>0?p(a.loadedMoreResults||"",{count:t}):String(e?.hasMoreResults?a.resultsUpdated||"":a.noMoreResults||""))(g.length,d.browse,l)),"success"}else d.route=c?.route||d.route||{},d.category=String(d.route.categoryKey||d.category||""),d.categorySearch=String(d.route.categorySearch||e.category_search||"");return d.startMode=String(e.start_mode||d.startMode||"default"),d.customStart=String(e.custom_start||""),z(),"route"===t&&U&&U.requestId===L&&(((t,e)=>{if(!e)return;const a=String(e.placeId||""),n=t.tripHeaderActions?.querySelector("button:not([disabled]):not([hidden])");let r=null;if(a&&"add-waypoint"===e.action)r=t.tripRegion?.querySelector(`[data-waypoint-id="${a}"] button[name="remove_waypoint"]`);else if(a&&"move-waypoint"===e.action){const n=String(e.direction||"");r=t.tripRegion?.querySelector(`[data-waypoint-id="${a}"] button[name="move_waypoint"][value="${a}:${n}"]`)||t.tripRegion?.querySelector(`[data-waypoint-id="${a}"] button[name="remove_waypoint"]`)}else"remove-waypoint"===e.action?r=t.tripRegion?.querySelector('[data-plan-trip-list] button[name="remove_waypoint"]')||n:"clear-trip"===e.action&&(r=n||t.tripHeading);D(r)||D(t.tripHeading)})(u,U.focusRequest),U=null),S(u.liveRegion,r||""),"success"}catch(a){return"AbortError"===a?.name?(g(s,"info","request:aborted",{endpointKey:t}),L===x&&v&&(d.customStartStatus="",W(u,d,l)),"aborted"):L!==x?"stale":(g(s,"error","request:failed",{endpointKey:t,error:a instanceof Error?a.message:String(a||""),payload:e}),n&&(d.isLoadingMore=!1,C(u,d,l,{appendResults:!0,appendedResults:[],isLoadingMore:!1})),Q(n?y||l.requestFailed||"":a instanceof Error?a.message:l.requestFailed||"",n&&(y||l.requestFailed)||""),v&&(d.customStartStatus="",W(u,d,l)),"failed")}finally{L===x&&(n&&d.isLoadingMore&&(d.isLoadingMore=!1),K(o,!1),J(u,d,!1),N(o,!1),"route"===t&&U&&U.requestId===L&&(U=null),G(o,!1),q=null,k="")}var _,E};if(u.startModeInputs.forEach((t=>{t.addEventListener("change",(()=>{d.startMode=L(u.startModeInputs)||d.startMode||"default",W(u,d,l),c?Z("browse",_(u,d),{announcementMessage:l.startingPointUpdated||"",refreshRoute:!0}):S(u.liveRegion,l.startingPointUpdated||"")}))})),u.customStartInput instanceof HTMLInputElement&&u.customStartInput.addEventListener("change",(()=>{d.customStart=u.customStartInput.value||"",W(u,d,l),c?Z("browse",_(u,d),{announcementMessage:l.startingPointUpdated||"",refreshRoute:!0}):S(u.liveRegion,l.startingPointUpdated||"")})),u.categorySearchInput instanceof HTMLInputElement&&(u.categorySearchInput.addEventListener("input",(()=>{d.categorySearch=u.categorySearchInput.value||""})),u.categorySearchInput.addEventListener("keydown",(t=>{if("Enter"!==t.key||!c)return;t.preventDefault();const e=_(u,d);e.category="",e.category_search=u.categorySearchInput.value||"",d.expandedCategory="",d.customResultsExpanded=!0,Z("browse",e,{announcementMessage:l.resultsUpdated||"",refreshRoute:P(d.route)})}))),u.form instanceof HTMLFormElement&&u.form.addEventListener("submit",(t=>{const e=t.submitter;if(!(e instanceof HTMLButtonElement&&c))return;let a="browse",n=l.resultsUpdated||"";const r=_(u,d);if(e.matches("[data-plan-category-button]")){const a=e.getAttribute("data-category-key")||"";if(a===d.category){const n=e.querySelector(".plan-your-day__category-title")?.textContent?.trim()||a;return t.preventDefault(),d.expandedCategory=d.expandedCategory===a?"":a,d.customResultsExpanded=!1,C(u,d,l),void S(u.liveRegion,d.expandedCategory===a?p(l.categoryResultsExpanded||"",{category:n}):p(l.categoryResultsCollapsed||"",{category:n}))}r.category=a,r.category_search="",d.expandedCategory=a,d.customResultsExpanded=!1}else e.matches('[data-plan-action="search-category-query"]')?(r.category="",r.category_search=u.categorySearchInput instanceof HTMLInputElement&&u.categorySearchInput.value||"",d.expandedCategory="",d.customResultsExpanded=!0):e.matches('[data-plan-action="add-waypoint"]')?(r.waypoints=[...r.waypoints,e.getAttribute("data-place-id")||e.value||""],a="route",n=l.tripUpdated||""):e.matches('[data-plan-action="remove-waypoint"]')?(r.remove_waypoint=e.getAttribute("data-place-id")||e.value||"",a="route",n=l.tripUpdated||""):e.matches("[data-plan-clear-trip]")?(r.clear_trip=!0,a="route",n=l.tripUpdated||""):"move_waypoint"===e.name&&e.value&&(r.move_waypoint=e.value,a="route",n=l.tripUpdated||"");t.preventDefault(),Z(a,r,{announcementMessage:n,refreshRoute:"browse"===a?P(d.route):void 0,routeFocusRequest:"route"===a?F(e):null})})),o.addEventListener("click",(t=>{const r=t.target;if(!(r instanceof HTMLElement))return;if(r.closest('[data-plan-open-link][aria-disabled="true"]')instanceof HTMLElement)return t.preventDefault(),void S(u.liveRegion,l.openMapsDisabled||"");if(r.closest("[data-plan-start-toggle]")instanceof HTMLButtonElement)return t.preventDefault(),A?A&&X(!1):A||X(!0),void S(u.liveRegion,A?l.startOptionsExpanded||"":l.startOptionsCollapsed||"");if(r.closest("[data-plan-color-mode-toggle]")instanceof HTMLButtonElement){t.preventDefault();const r=(o.getAttribute("data-plan-color-mode")===n?n:a)===n?a:n;return(t=>{if(f(t))try{window.localStorage?.setItem(e,t)}catch(t){}})(r),void M(o,u,r,l)}if(r.closest("[data-plan-custom-results-button]")instanceof HTMLButtonElement){if(t.preventDefault(),!d.browse?.hasSearch||d.category)return;return d.expandedCategory="",d.customResultsExpanded=!d.customResultsExpanded,C(u,d,l),void S(u.liveRegion,d.customResultsExpanded?String(l.customResultsExpanded||""):String(l.customResultsCollapsed||""))}const s=r.closest("[data-plan-load-more-button]");if(s instanceof HTMLButtonElement){if(t.preventDefault(),!c||d.isLoadingMore||s.disabled)return;const e=String(d.browse?.nextPageToken||"");if(!e)return void S(u.liveRegion,l.noMoreResults||"");d.isLoadingMore=!0,C(u,d,l,{appendResults:!0,appendedResults:[],isLoadingMore:!0}),S(u.liveRegion,l.loadingMoreResults||""),Z("browse",{..._(u,d),page_token:e,append_results:!0},{appendBrowseResults:!0,errorMessage:l.loadMoreError||"",refreshRoute:!1,searchContextKey:String(d.browse?.searchContextKey||"")})}})),o.addEventListener("keydown",(t=>{const e=t.target;if(!(e instanceof HTMLElement))return;e.closest('[data-plan-open-link][aria-disabled="true"]')instanceof HTMLElement&&("Enter"!==t.key&&" "!==t.key||(t.preventDefault(),S(u.liveRegion,l.openMapsDisabled||"")))})),z(),V(),o.classList.add("is-enhanced"),y){if(!c)return void Q(l.requestFailed||"");Z("browse",_(u,d),{refreshRoute:!0})}},z=()=>{document.querySelectorAll("[data-plan-root]").forEach(Y)};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",z,{once:!0}):z()})(); \ No newline at end of file +(()=>{const t="planYourDayEnhanced",e="planYourDayColorMode",a="light",n="dark",r="system",o=[a,n],s=[...o,r],i="checking",l="found",u="not_found",d=[i,l,u],c=t=>String(t??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"),p=(t,e={})=>{let a=String(t||"");return Object.entries(e||{}).forEach((([t,e])=>{a=a.split(`{${t}}`).join(String(e??""))})),a},y=(t,e="")=>Array.isArray(t)?t.map((t=>y(t))):t&&"object"==typeof t?Object.fromEntries(Object.entries(t).map((([t,e])=>{const a=String(t).toLowerCase();return[t,a.includes("token")||a.includes("api_key")||a.includes("authorization")||a.includes("cookie")||a.includes("secret")?"[redacted]":y(e,t)]}))):"string"==typeof t?String(e).toLowerCase().includes("token")?"[redacted]":t.replace(/([?&](?:key|api_key|token)=)[^&]+/gi,"$1[redacted]"):t,g=(t,e,a,n={})=>{if(!t?.debug||"undefined"==typeof console)return;("function"==typeof console[e]?console[e]:console.log).call(console,`[plan-your-day] ${a}`,y(n))},m=t=>Array.isArray(t)?t.map((t=>String(t??""))).filter(Boolean):[],S=(t,e)=>{t&&e&&(t.textContent="",window.requestAnimationFrame((()=>{t.textContent=e})))},f=t=>o.includes(String(t||"")),b=t=>{const e=String(t||"");return s.includes(e)?e:a},h=()=>{try{const t=window.localStorage?.getItem(e);return f(t)?t:""}catch(t){return""}},w=()=>"undefined"==typeof window||"function"!=typeof window.matchMedia?null:window.matchMedia("(prefers-color-scheme: dark)"),v=(t,e=w())=>{const o=h();if(f(o))return o;const s=b(t);return s===r?e?.matches?n:a:s},M=(t,e,r,o)=>{const s=f(r)?r:a;t.setAttribute("data-plan-color-mode",s),((t,e,a)=>{t.colorModeToggle instanceof HTMLButtonElement&&(t.colorModeToggle.hidden=!1,t.colorModeToggle.setAttribute("aria-pressed",String(e===n)),t.colorModeToggle.setAttribute("aria-label",String(a.darkModeLabel||"Dark mode")),t.colorModeToggleLabel instanceof HTMLElement&&(t.colorModeToggleLabel.textContent=String(a.darkModeLabel||"Dark mode")))})(e,s,o)},L=t=>{const e=t.find((t=>t.checked));return e?e.value:""},R=t=>{const e=String(t||"");return d.includes(e)?e:""},E=(t,e)=>{const a=t.form instanceof HTMLFormElement?new FormData(t.form):new FormData;return{category:String(a.get("category")||e.category||""),category_search:String(a.get("category_search")||e.categorySearch||""),waypoints:a.getAll("waypoints[]").map((t=>String(t||""))).filter(Boolean),start_mode:String(a.get("start_mode")||e.startMode||"default"),custom_start:String(a.get("custom_start")||"")}},_=(t,e,a)=>{const n=String(t?.id||""),r=String(t?.label||""),o=String(t?.address||""),s=String(t?.distance_label||""),i=String(t?.maps_uri||""),l=e.includes(n);return`\n
  • \n
    \n

    ${c(r)}

    \n ${s?`

    ${c(s)}

    `:""}\n

    ${c(o)}

    \n
    \n
    \n ${i?`${c(a.viewInGoogleMaps||"")}`:""}\n ${l?`${c(a.inTrip||"")}`:``}\n
    \n
  • \n `},A=(t,e,a=!1)=>{if(!t?.hasMoreResults)return"";const n=String(a?e.loadingMoreResults||e.moreResultsButton||"":e.moreResultsButton||"");return`\n
    \n \n
    \n `},q=(t,e,a,n={})=>`\n
    \n ${((t,e,a)=>{const n=Array.isArray(t?.searchResults)?t.searchResults:[];if(0===n.length){const e=t?.resultsEmptyState||{};return`\n
    \n

    ${c(e.heading||"")}

    \n

    ${c(e.body||"")}

    \n
    \n `}return`\n \n `})(t,e,a)}\n
    \n ${A(t,a,Boolean(n.isLoadingMore))}\n `,T=(t,e)=>{const a=new Set((t=>{const e=[];return(Array.isArray(t?.searchResults)?t.searchResults:[]).forEach((t=>{const a=String(t?.id||"");a&&!e.includes(a)&&e.push(a)})),e})(t));return(Array.isArray(e?.searchResults)?e.searchResults:[]).filter((t=>{const e=String(t?.id||"");return!e||!a.has(e)&&(a.add(e),!0)}))},H=(t,e)=>{t.categoryInput&&(t.categoryInput.value=e.category||""),t.waypointInputs&&(t.waypointInputs.innerHTML=m(e.route?.selectedWaypointIds).map((t=>``)).join(""))},k=(t,e)=>{if(!t.messages)return;const a=Array.isArray(e)?e:[];t.messages.hidden=0===a.length,t.messages.innerHTML=(t=>(Array.isArray(t)?t:[]).map((t=>{const e=String(t?.type||"note"),a=String(t?.text||""),n="warning"===e?"alert":"";return`
  • ${c(a)}
  • `})).join(""))(a)},$=(t,e,a,n)=>{if(!(t instanceof HTMLElement))return;const r=t.querySelector("[data-plan-load-more-wrap]"),o=A(e,a,n);o?r instanceof HTMLElement?r.outerHTML=o:t.insertAdjacentHTML("beforeend",o):r instanceof HTMLElement&&r.remove()},x=(t,e,a,n,r={})=>{if(!(t instanceof HTMLElement))return;const o=Boolean(r.appendResults),s=Array.isArray(r.appendedResults)?r.appendedResults:[];o&&(s.length>0&&((t,e,a,n)=>{if(!(t instanceof HTMLElement&&Array.isArray(e)&&0!==e.length))return!1;const r=t.querySelector("[data-plan-results-list]");if(!(r instanceof HTMLElement))return!1;const o=t.querySelector("[data-plan-results-empty]");return o instanceof HTMLElement&&o.remove(),r.insertAdjacentHTML("beforeend",e.map((t=>_(t,a,n))).join("")),!0})(t,s,a,n)||0===s.length&&(t=>t instanceof HTMLElement&&t.querySelector("[data-plan-results-list], [data-plan-results-empty]")instanceof HTMLElement)(t))?$(t,e,n,Boolean(r.isLoadingMore)):t.innerHTML=q(e,a,n,r)},C=(t,e,a,n={})=>{const r=e.category||"",o=e.expandedCategory||"",s=m(e.route?.selectedWaypointIds),i=e.browse||{},l=Boolean(i.hasSearch)&&!r,u=l||0===t.categoryButtons.length,d=l&&Boolean(e.customResultsExpanded)||!l&&0===t.categoryButtons.length,c={appendResults:Boolean(n.appendResults),appendedResults:Array.isArray(n.appendedResults)?n.appendedResults:[],isLoadingMore:Boolean(n.isLoadingMore)};t.categoryButtons.forEach((t=>{const e=t.getAttribute("data-category-key")||"",a=e===r&&e===o,n=t.closest(".plan-your-day__category-accordion-item");t.setAttribute("aria-expanded",String(a)),n instanceof HTMLElement&&n.classList.toggle("is-expanded",a)})),t.categoryRegions.forEach((t=>{const e=t.getAttribute("data-category-key")||"",n=e===r&&e===o,l=t.querySelector("[data-plan-category-results-panel]");if(t.hidden=!n,l instanceof HTMLElement){if(!n)return void(l.innerHTML="");x(l,i,s,a,c)}})),t.customResults&&(t.customResults.hidden=!u,t.customResults.classList.toggle("is-expanded",d)),t.customResultsButton&&t.customResultsButton.setAttribute("aria-expanded",String(d)),t.customResultsRegion&&(t.customResultsRegion.hidden=!d),t.customResultsHeading&&(t.customResultsHeading.textContent=l?p(a.searchResultsFor||"",{search:i.categoryLabel||""}):String(i?.resultsEmptyState?.heading||"")),t.customResultsDescription&&(t.customResultsDescription.textContent=String(l?a.customSearchResultsDescription||"":i?.resultsEmptyState?.body||"")),t.customResultsPanel&&(d?l?x(t.customResultsPanel,i,s,a,c):t.customResultsPanel.innerHTML=q(i,s,a,{isLoadingMore:!1}):t.customResultsPanel.innerHTML="")},I=(t,e,a)=>{t.tripHeaderActions&&(t.tripHeaderActions.innerHTML=((t,e)=>{const a=m(t?.selectedWaypointIds),n=String(t?.tripCountLabel||"");return`\n ${c(n)}\n ${a.length>0?``:""}\n `})(e.route,a)),t.tripRegion&&(t.tripRegion.innerHTML=((t,e,a)=>{const n=Array.isArray(t?.tripWaypoints)?t.tripWaypoints:[];if(0===n.length){const a=t?.tripEmptyState||{};return`\n
    \n

    ${c(a.heading||e.tripEmptyHeading||"")}

    \n

    ${c(a.body||e.tripEmptyBody||"")}

    \n
    \n `}return`\n
      \n ${n.map(((t,a)=>{const r=String(t?.id||""),o=String(t?.label||""),s=String(t?.address||""),i=a>0,l=a\n
      \n \n
      \n

      ${c(o)}

      \n

      ${c(s)}

      \n
      \n
      \n
      \n \n ${c(e.moveUp||"")}\n \n \n ${c(e.moveDown||"")}\n \n \n
      \n \n `})).join("")}\n
    \n `})(e.route,a,t.tripRegion.getAttribute("data-plan-trip-help-id")||""))},B=(t,e,a)=>{const n=e.route||{},r=String(n.iframeSrc||""),o=n.emptyPreviewState||{},s=String(n.mapsUrl||""),i=m(n.selectedWaypointIds).length>0;k(t,n.messages),t.mapWrap&&(t.mapWrap.hidden=""===r),t.iframe&&(t.iframe.src=r),t.previewEmpty&&(t.previewEmpty.hidden=""!==r),t.previewEmptyHeading&&(t.previewEmptyHeading.textContent=String(o.heading||"")),t.previewEmptyBody&&(t.previewEmptyBody.textContent=String(o.body||"")),t.summaryCount&&(t.summaryCount.textContent=String(n.tripCountLabel||""),t.summaryCount.hidden=i),t.openLinkLabel&&(t.openLinkLabel.textContent=String(n.mapsLinkLabel||"")),t.openLink&&(t.openLink.hidden=!i,t.openLink.classList.toggle("is-disabled",""===s),s?(t.openLink.href=s,t.openLink.removeAttribute("aria-disabled"),t.openLink.removeAttribute("tabindex"),t.openLink.removeAttribute("role")):(t.openLink.removeAttribute("href"),t.openLink.setAttribute("aria-disabled","true"),t.openLink.setAttribute("tabindex","0"),t.openLink.setAttribute("role","button")))},D=t=>t instanceof HTMLElement&&"function"==typeof t.focus&&(t.focus(),document.activeElement===t),F=t=>{if(!(t instanceof HTMLButtonElement))return null;if(t.matches('[data-plan-action="add-waypoint"]'))return{action:"add-waypoint",placeId:t.getAttribute("data-place-id")||t.value||""};if(t.matches('[data-plan-action="remove-waypoint"]'))return{action:"remove-waypoint",placeId:t.getAttribute("data-place-id")||t.value||""};if(t.matches("[data-plan-clear-trip]"))return{action:"clear-trip",placeId:""};if("move_waypoint"===t.name&&t.value){const[e,a]=String(t.value).split(":",2);return{action:"move-waypoint",placeId:e||"",direction:a||""}}return null},P=t=>0===m(t?.selectedWaypointIds).length,U=(t,e,a)=>{const n=R(e);t.customStartWrap instanceof HTMLElement&&t.customStartWrap.setAttribute("data-plan-custom-start-state",n),t.customStartStatus instanceof HTMLElement&&(t.customStartStatus.textContent=((t,e)=>t===i?String(e.customStartChecking||"Checking starting address."):t===l?String(e.customStartFound||"Starting address found. Results are ready."):t===u?String(e.customStartNotFound||"Starting address was not found."):"")(n,a))},j=(t,e,a)=>{t.startModeInputs.forEach((t=>{t.checked=t.value===e.startMode})),t.customStartInput&&(t.customStartInput.value=e.customStart||"");const n="custom"===(L(t.startModeInputs)||e.startMode||"default");t.customStartWrap&&(t.customStartWrap.hidden=!n),t.customStartInput&&(t.customStartInput.disabled=!n),U(t,n?e.customStartStatus:"",a)},W=(t,e)=>{if(!(t.categorySearchInput instanceof HTMLInputElement))return;const a=String(e.categorySearch||"");t.categorySearchInput.value!==a&&(t.categorySearchInput.value=a)},O=(t,e)=>{t.classList.toggle("is-submitting",e),t.setAttribute("aria-busy",String(e))},K=(t,e,a)=>{t.forEach((t=>{if(!(t instanceof HTMLButtonElement||t instanceof HTMLInputElement))return;if(e)return t.hasAttribute(a)||t.setAttribute(a,t.disabled?"true":"false"),void(t.disabled=!0);const n=t.getAttribute(a);null!==n&&(t.disabled="true"===n,t.removeAttribute(a))}))},N=(t,e)=>{K(t.querySelectorAll("[data-plan-route-mutation]"),e,"data-plan-disabled-before-request")},J=(t,e)=>{K(t.querySelectorAll(['[data-plan-form] button[type="submit"]:not([data-plan-route-mutation])',"[data-plan-load-more-button]","[data-plan-start-toggle]","[data-plan-custom-results-button]",'input[name="start_mode"]',"[data-plan-custom-start]","[data-plan-category-search]"].join(",")),e,"data-plan-browse-disabled-before-request")},G=(t,e,a)=>{const n=e.category||"";if(t.categoryPanels.forEach((e=>{const r=e.getAttribute("data-category-key")||"",o=0===t.categoryButtons.length||r===n||!1===t.customResults?.hidden;e.setAttribute("aria-busy",String(a&&o))})),t.customResultsPanel instanceof HTMLElement){const e=!1===t.customResults?.hidden;t.customResultsPanel.setAttribute("aria-busy",String(a&&e))}t.tripRegion instanceof HTMLElement&&t.tripRegion.setAttribute("aria-busy",String(a)),t.previewCard instanceof HTMLElement&&t.previewCard.setAttribute("aria-busy",String(a))},Y=o=>{if(!(o instanceof HTMLElement)||"true"===o.dataset[t])return;o.dataset[t]="true";const s=(t=>{const e=t.querySelector("[data-plan-config]");if(!e)return{};try{return JSON.parse(e.textContent||"{}")}catch(t){return{}}})(o),l=s.strings||{},u={form:o.querySelector("[data-plan-form]"),liveRegion:o.querySelector("[data-plan-live-region]"),categoryInput:o.querySelector("[data-plan-category-input]"),waypointInputs:o.querySelector("[data-plan-waypoint-inputs]"),categoryButtons:Array.from(o.querySelectorAll("[data-plan-category-button]")),categoryItems:Array.from(o.querySelectorAll("[data-plan-category-item]")),categoryRegions:Array.from(o.querySelectorAll("[data-plan-category-region]")),categoryPanels:Array.from(o.querySelectorAll("[data-plan-category-results-panel]")),categorySearchInput:o.querySelector("[data-plan-category-search]"),customResults:o.querySelector("[data-plan-custom-results]"),customResultsButton:o.querySelector("[data-plan-custom-results-button]"),customResultsHeading:o.querySelector("[data-plan-custom-results-heading]"),customResultsDescription:o.querySelector("[data-plan-custom-results-description]"),customResultsRegion:o.querySelector("[data-plan-custom-results-region]"),customResultsPanel:o.querySelector("[data-plan-custom-results-panel]"),resultsHeading:o.querySelector("[data-plan-results-heading]"),startModeInputs:Array.from(o.querySelectorAll('input[name="start_mode"]')),customStartWrap:o.querySelector("[data-plan-custom-start-wrap]"),customStartInput:o.querySelector("[data-plan-custom-start]"),customStartStatus:o.querySelector("[data-plan-custom-start-status]"),startToggle:o.querySelector("[data-plan-start-toggle]"),startToggleLabel:o.querySelector("[data-plan-start-toggle-label]"),startPanel:o.querySelector("[data-plan-start-panel]"),colorModeToggle:o.querySelector("[data-plan-color-mode-toggle]"),colorModeToggleLabel:o.querySelector("[data-plan-color-mode-toggle-label]"),tripHeaderActions:o.querySelector("[data-plan-trip-header-actions]"),tripHeading:o.querySelector("[data-plan-trip-heading]"),tripRegion:o.querySelector("[data-plan-trip-region]"),messages:o.querySelector("[data-plan-messages]"),previewCard:o.querySelector("[data-plan-preview-card]"),mapWrap:o.querySelector("[data-plan-map-wrap]"),iframe:o.querySelector("[data-plan-iframe]"),previewEmpty:o.querySelector("[data-plan-preview-empty]"),previewEmptyHeading:o.querySelector("[data-plan-preview-empty-heading]"),previewEmptyBody:o.querySelector("[data-plan-preview-empty-body]"),summaryCount:o.querySelector("[data-plan-summary-count]"),openLink:o.querySelector("[data-plan-open-link]"),openLinkLabel:o.querySelector("[data-plan-open-link-label]")},d={category:String(s.initialState?.category||""),categorySearch:String(s.initialState?.categorySearch||""),startMode:String(s.initialState?.startMode||"default"),customStart:String(s.initialState?.customStart||""),customStartStatus:R(s.initialData?.browse?.customStartStatus||""),expandedCategory:String(s.initialState?.category||""),customResultsExpanded:Boolean(s.initialData?.browse?.isCustomSearch),isLoadingMore:!1,browse:s.initialData?.browse||{},route:s.initialData?.route||{}},c="string"==typeof s.rest?.bootstrapUrl&&""!==s.rest.bootstrapUrl;let y="string"==typeof s.rest?.endpointToken?s.rest.endpointToken:"",m=!1,_=null;const A=u.form instanceof HTMLFormElement&&"string"==typeof s.rest?.browseUrl&&""!==s.rest.browseUrl&&"string"==typeof s.rest?.routeUrl&&""!==s.rest.routeUrl&&(""!==y||c),q=Boolean(s.hydration?.shouldHydrateOnLoad),$=b(s.colorModeDefault||o.getAttribute("data-plan-color-mode-default")),x=w();let U=!0,K=null,Y="",z=0,Q=null;const V="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches;let X=0;M(o,u,v($,x),l),$===r&&x&&x.addEventListener("change",(()=>{h()||M(o,u,v($,x),l)}));const Z=()=>{C(u,d,l,{isLoadingMore:d.isLoadingMore}),I(u,d,l),B(u,d),H(u,d),j(u,d,l),W(u,d),N(o,!1)},tt=(t,e="")=>{k(u,[{type:"warning",text:t||l.requestFailed||""}]),S(u.liveRegion,e||t||l.requestFailed||"")},et=(t={})=>{if(!u.startToggle||!u.startPanel)return;const e=!1!==t.syncHidden;u.startToggle.hidden=!1,u.startToggle.setAttribute("aria-expanded",String(U)),u.startToggle.classList.toggle("is-collapsed",!U),e&&(u.startPanel.hidden=!U),u.startToggleLabel&&(u.startToggleLabel.textContent=String(U?l.hideStartOptions||"Hide options":l.showStartOptions||"Show options"))},at=t=>{if(!(u.startPanel instanceof HTMLElement&&u.startToggle))return U=t,void et();const e=u.startPanel,a=V?0:480;X&&(window.cancelAnimationFrame(X),X=0);const n=e.hidden?0:e.getBoundingClientRect().height;e.hidden=!1,t&&(e.style.height="");const r=t?e.scrollHeight:0,o=t&&r>0?Math.max(n/r,0):1,s=t?1:0;if(U=t,e.style.overflow="hidden",e.style.pointerEvents="none",e.style.height=`${n}px`,e.style.opacity=String(o),et({syncHidden:!1}),a<=0||n===r)return e.style.height="",e.style.overflow="",e.style.pointerEvents="",e.style.opacity="",e.hidden=!t,void et();const i=performance.now(),l=u=>{const d=Math.min((u-i)/a,1),c=(p=d)<.5?4*p*p*p:1-Math.pow(-2*p+2,3)/2;var p;const y=n+(r-n)*c,g=o+(s-o)*c;e.style.height=`${Math.max(y,0)}px`,e.style.opacity=String(Math.max(Math.min(g,1),0)),d<1?X=window.requestAnimationFrame(l):(e.style.height="",e.style.overflow="",e.style.pointerEvents="",e.style.opacity="",e.hidden=!t,et(),X=0)};X=window.requestAnimationFrame(l)},nt=async(t,e,a={})=>{if(!A)return"unsupported";const n=Boolean(a.appendBrowseResults),r=String(a.announcementMessage||""),f=String(a.errorMessage||""),b=String(a.searchContextKey||""),h="browse"===t&&!1!==a.refreshRoute,w="route"===t?a.routeFocusRequest??null:null,v=String(e.start_mode||""),M=String(e.custom_start||""),L="browse"===t&&!n&&"custom"===v&&""!==M.trim(),E="route"===t&&(d.customStartStatus===i||"custom"!==v||""===M.trim()||M!==String(d.customStart||""));if(K instanceof AbortController){if("route"===Y)return g(s,"info","request:blocked",{endpointKey:t,blockedBy:Y}),"busy";K.abort()}z+=1;const q=z;"route"===t&&(Q=w?{requestId:q,focusRequest:w}:null),K=new AbortController,Y=t,O(o,!0),G(u,d,!0),N(o,"route"===t),J(o,"route"===t),L?(d.customStartStatus=i,j(u,d,l)):("browse"===t&&!n||E)&&(d.customStartStatus="",j(u,d,l)),g(s,"info","request:start",{endpointKey:t,payload:e});try{const a=await(async()=>c?m&&""!==y?y:_||(_=fetch(s.rest.bootstrapUrl,{method:"POST",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify({})}).then((async t=>{const e=await t.json().catch((()=>({})));if(g(s,t.ok?"info":"warn","request:bootstrap",{status:t.status,ok:t.ok,body:e}),!t.ok)throw new Error(e?.message||l.requestFailed||"");const a=String(e?.endpointToken||"");if(""===a)throw new Error(l.requestFailed||"");return y=a,m=!0,y})).finally((()=>{_=null})),_):y)();if(""===a)throw new Error(l.requestFailed||"");const i={...e,endpoint_token:a};"browse"===t&&(i.refresh_route=h,""!==b&&(i.search_context_key=b));const w=await fetch(s.rest["browse"===t?"browseUrl":"routeUrl"],{method:"POST",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(i),signal:K.signal}),v=await w.json().catch((()=>({})));if(g(s,w.ok?"info":"warn","request:response",{endpointKey:t,status:w.status,ok:w.ok,body:v}),!w.ok)throw new Error(v?.message||l.requestFailed||"");if(q!==z)return!0;if("browse"===t){const t=d.category||"",a=d.expandedCategory||"",r=d.categorySearch||"",s=v?.browse||{};if(n&&""!==String(s.searchResultsError||""))return d.isLoadingMore=!1,C(u,d,l,{appendResults:!0,appendedResults:[],isLoadingMore:!1}),tt(f||l.requestFailed||"",f||l.requestFailed||""),"failed";const i=n&&""!==b&&b===String(d.browse?.searchContextKey||"")&&b===String(s.searchContextKey||""),c=i?T(d.browse,s):[];if(d.browse=i?{...(k=d.browse)||{},...($=s)||{},searchResults:[...Array.isArray(k?.searchResults)?k.searchResults:[],...T(k,$)]}:s,d.route=v?.route||d.route||{},d.category=String(d.browse.categoryKey||e.category||""),d.categorySearch=String(d.browse.categorySearch||e.category_search||""),d.customStartStatus=R(d.browse.customStartStatus||""),d.isLoadingMore=!1,d.category?(d.expandedCategory=d.category===t?a:d.category,d.customResultsExpanded=!1):d.browse.hasSearch?String(e.category_search||"")!==r&&(d.expandedCategory="",d.customResultsExpanded=!0):(d.expandedCategory="",d.customResultsExpanded=!1),i)return C(u,d,l,{appendResults:!0,appendedResults:c,isLoadingMore:!1}),I(u,d,l),B(u,d),H(u,d),j(u,d,l),W(u,d),N(o,!1),S(u.liveRegion,d.browse?.searchResultsError?f||l.requestFailed||"":((t,e,a)=>t>0?p(a.loadedMoreResults||"",{count:t}):String(e?.hasMoreResults?a.resultsUpdated||"":a.noMoreResults||""))(c.length,d.browse,l)),"success"}else d.route=v?.route||d.route||{},d.category=String(d.route.categoryKey||d.category||""),d.categorySearch=String(d.route.categorySearch||e.category_search||"");return d.startMode=String(e.start_mode||d.startMode||"default"),d.customStart=String(e.custom_start||""),Z(),"route"===t&&Q&&Q.requestId===q&&(((t,e)=>{if(!e)return;const a=String(e.placeId||""),n=t.tripHeaderActions?.querySelector("button:not([disabled]):not([hidden])");let r=null;if(a&&"add-waypoint"===e.action)r=t.tripRegion?.querySelector(`[data-waypoint-id="${a}"] button[name="remove_waypoint"]`);else if(a&&"move-waypoint"===e.action){const n=String(e.direction||"");r=t.tripRegion?.querySelector(`[data-waypoint-id="${a}"] button[name="move_waypoint"][value="${a}:${n}"]`)||t.tripRegion?.querySelector(`[data-waypoint-id="${a}"] button[name="remove_waypoint"]`)}else"remove-waypoint"===e.action?r=t.tripRegion?.querySelector('[data-plan-trip-list] button[name="remove_waypoint"]')||n:"clear-trip"===e.action&&(r=n||t.tripHeading);D(r)||D(t.tripHeading)})(u,Q.focusRequest),Q=null),S(u.liveRegion,r||""),"success"}catch(a){return"AbortError"===a?.name?(g(s,"info","request:aborted",{endpointKey:t}),q===z&&L&&(d.customStartStatus="",j(u,d,l)),"aborted"):q!==z?"stale":(g(s,"error","request:failed",{endpointKey:t,error:a instanceof Error?a.message:String(a||""),payload:e}),n&&(d.isLoadingMore=!1,C(u,d,l,{appendResults:!0,appendedResults:[],isLoadingMore:!1})),tt(n?f||l.requestFailed||"":a instanceof Error?a.message:l.requestFailed||"",n&&(f||l.requestFailed)||""),L&&(d.customStartStatus="",j(u,d,l)),"failed")}finally{q===z&&(n&&d.isLoadingMore&&(d.isLoadingMore=!1),O(o,!1),G(u,d,!1),N(o,!1),"route"===t&&Q&&Q.requestId===q&&(Q=null),J(o,!1),K=null,Y="")}var k,$};if(u.startModeInputs.forEach((t=>{t.addEventListener("change",(()=>{d.startMode=L(u.startModeInputs)||d.startMode||"default",j(u,d,l),A?nt("browse",E(u,d),{announcementMessage:l.startingPointUpdated||"",refreshRoute:!0}):S(u.liveRegion,l.startingPointUpdated||"")}))})),u.customStartInput instanceof HTMLInputElement&&u.customStartInput.addEventListener("change",(()=>{d.customStart=u.customStartInput.value||"",j(u,d,l),A?nt("browse",E(u,d),{announcementMessage:l.startingPointUpdated||"",refreshRoute:!0}):S(u.liveRegion,l.startingPointUpdated||"")})),u.categorySearchInput instanceof HTMLInputElement&&(u.categorySearchInput.addEventListener("input",(()=>{d.categorySearch=u.categorySearchInput.value||""})),u.categorySearchInput.addEventListener("keydown",(t=>{if("Enter"!==t.key||!A)return;t.preventDefault();const e=E(u,d);e.category="",e.category_search=u.categorySearchInput.value||"",d.expandedCategory="",d.customResultsExpanded=!0,nt("browse",e,{announcementMessage:l.resultsUpdated||"",refreshRoute:P(d.route)})}))),u.form instanceof HTMLFormElement&&u.form.addEventListener("submit",(t=>{const e=t.submitter;if(!(e instanceof HTMLButtonElement&&A))return;let a="browse",n=l.resultsUpdated||"";const r=E(u,d);if(e.matches("[data-plan-category-button]")){const a=e.getAttribute("data-category-key")||"";if(a===d.category){const n=e.querySelector(".plan-your-day__category-title")?.textContent?.trim()||a;return t.preventDefault(),d.expandedCategory=d.expandedCategory===a?"":a,d.customResultsExpanded=!1,C(u,d,l),void S(u.liveRegion,d.expandedCategory===a?p(l.categoryResultsExpanded||"",{category:n}):p(l.categoryResultsCollapsed||"",{category:n}))}r.category=a,r.category_search="",d.expandedCategory=a,d.customResultsExpanded=!1}else e.matches('[data-plan-action="search-category-query"]')?(r.category="",r.category_search=u.categorySearchInput instanceof HTMLInputElement&&u.categorySearchInput.value||"",d.expandedCategory="",d.customResultsExpanded=!0):e.matches('[data-plan-action="add-waypoint"]')?(r.waypoints=[...r.waypoints,e.getAttribute("data-place-id")||e.value||""],a="route",n=l.tripUpdated||""):e.matches('[data-plan-action="remove-waypoint"]')?(r.remove_waypoint=e.getAttribute("data-place-id")||e.value||"",a="route",n=l.tripUpdated||""):e.matches("[data-plan-clear-trip]")?(r.clear_trip=!0,a="route",n=l.tripUpdated||""):"move_waypoint"===e.name&&e.value&&(r.move_waypoint=e.value,a="route",n=l.tripUpdated||"");t.preventDefault(),nt(a,r,{announcementMessage:n,refreshRoute:"browse"===a?P(d.route):void 0,routeFocusRequest:"route"===a?F(e):null})})),o.addEventListener("click",(t=>{const r=t.target;if(!(r instanceof HTMLElement))return;if(r.closest('[data-plan-open-link][aria-disabled="true"]')instanceof HTMLElement)return t.preventDefault(),void S(u.liveRegion,l.openMapsDisabled||"");if(r.closest("[data-plan-start-toggle]")instanceof HTMLButtonElement)return t.preventDefault(),U?U&&at(!1):U||at(!0),void S(u.liveRegion,U?l.startOptionsExpanded||"":l.startOptionsCollapsed||"");if(r.closest("[data-plan-color-mode-toggle]")instanceof HTMLButtonElement){t.preventDefault();const r=(o.getAttribute("data-plan-color-mode")===n?n:a)===n?a:n;return(t=>{if(f(t))try{window.localStorage?.setItem(e,t)}catch(t){}})(r),void M(o,u,r,l)}if(r.closest("[data-plan-custom-results-button]")instanceof HTMLButtonElement){if(t.preventDefault(),!d.browse?.hasSearch||d.category)return;return d.expandedCategory="",d.customResultsExpanded=!d.customResultsExpanded,C(u,d,l),void S(u.liveRegion,d.customResultsExpanded?String(l.customResultsExpanded||""):String(l.customResultsCollapsed||""))}const s=r.closest("[data-plan-load-more-button]");if(s instanceof HTMLButtonElement){if(t.preventDefault(),!A||d.isLoadingMore||s.disabled)return;const e=String(d.browse?.nextPageToken||"");if(!e)return void S(u.liveRegion,l.noMoreResults||"");d.isLoadingMore=!0,C(u,d,l,{appendResults:!0,appendedResults:[],isLoadingMore:!0}),S(u.liveRegion,l.loadingMoreResults||""),nt("browse",{...E(u,d),page_token:e,append_results:!0},{appendBrowseResults:!0,errorMessage:l.loadMoreError||"",refreshRoute:!1,searchContextKey:String(d.browse?.searchContextKey||"")})}})),o.addEventListener("keydown",(t=>{const e=t.target;if(!(e instanceof HTMLElement))return;e.closest('[data-plan-open-link][aria-disabled="true"]')instanceof HTMLElement&&("Enter"!==t.key&&" "!==t.key||(t.preventDefault(),S(u.liveRegion,l.openMapsDisabled||"")))})),Z(),et(),o.classList.add("is-enhanced"),q){if(!A)return void tt(l.requestFailed||"");nt("browse",E(u,d),{refreshRoute:!0})}},z=()=>{document.querySelectorAll("[data-plan-root]").forEach(Y)};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",z,{once:!0}):z()})(); \ No newline at end of file diff --git a/plugin/plan-your-day/src/Frontend/PlannerRenderer.php b/plugin/plan-your-day/src/Frontend/PlannerRenderer.php index 81b7890..29d8179 100644 --- a/plugin/plan-your-day/src/Frontend/PlannerRenderer.php +++ b/plugin/plan-your-day/src/Frontend/PlannerRenderer.php @@ -8,7 +8,6 @@ use Acodebeard\PlanYourDay\Planner\PlannerStateBuilder; use Acodebeard\PlanYourDay\Planner\RequestStateParser; use Acodebeard\PlanYourDay\Rest\PlannerRoutes; -use Acodebeard\PlanYourDay\Security\VisitorTokenManager; use Acodebeard\PlanYourDay\Settings\Settings; defined( 'ABSPATH' ) || exit; @@ -19,22 +18,19 @@ final class PlannerRenderer { private RequestStateParser $request_state_parser; private PlannerStateBuilder $planner_state_builder; private PlannerPayloadBuilder $planner_payload_builder; - private VisitorTokenManager $visitor_token_manager; public function __construct( Settings $settings, CategoryCatalog $category_catalog, RequestStateParser $request_state_parser, PlannerStateBuilder $planner_state_builder, - PlannerPayloadBuilder $planner_payload_builder, - VisitorTokenManager $visitor_token_manager + PlannerPayloadBuilder $planner_payload_builder ) { $this->settings = $settings; $this->category_catalog = $category_catalog; $this->request_state_parser = $request_state_parser; $this->planner_state_builder = $planner_state_builder; $this->planner_payload_builder = $planner_payload_builder; - $this->visitor_token_manager = $visitor_token_manager; } public function render( array $request = [], string $action_url = '' ): string { @@ -67,7 +63,6 @@ public function render( array $request = [], string $action_url = '' ): string { $action_url = '' !== $action_url ? $action_url : $this->get_current_url(); $form_action = $action_url . '#' . $instance_id; $maps_link_enabled = '' !== $planner_state['maps_url']; - $endpoint_token = $this->visitor_token_manager->get_endpoint_token(); $color_mode_default = $this->settings->get_color_mode_default(); $initial_color_mode = Settings::COLOR_MODE_SYSTEM === $color_mode_default ? '' : $color_mode_default; @@ -126,7 +121,7 @@ class="plan-your-day__layout" - + planner_payload_builder->build_browse_payload( $planner_state ); $route_payload = $this->planner_payload_builder->build_route_payload( $planner_state ); @@ -697,14 +692,15 @@ private function build_config( string $instance_id, string $action_url, array $p 'actionUrl' => $action_url, 'sectionId' => $instance_id, 'colorModeDefault' => $this->settings->get_color_mode_default(), - 'startPoints' => $start_points, - 'categoryCatalog' => $category_catalog, - 'rest' => [ - 'browseUrl' => rest_url( PlannerRoutes::REST_NAMESPACE . '/browse' ), - 'routeUrl' => rest_url( PlannerRoutes::REST_NAMESPACE . '/route' ), - 'endpointToken' => $endpoint_token, + 'startPoints' => $start_points, + 'categoryCatalog' => $category_catalog, + 'rest' => [ + 'bootstrapUrl' => rest_url( PlannerRoutes::REST_NAMESPACE . '/bootstrap' ), + 'browseUrl' => rest_url( PlannerRoutes::REST_NAMESPACE . '/browse' ), + 'routeUrl' => rest_url( PlannerRoutes::REST_NAMESPACE . '/route' ), + 'endpointToken' => '', ], - 'hydration' => [ + 'hydration' => [ 'shouldHydrateOnLoad' => $should_hydrate_on_load, ], 'strings' => [ diff --git a/plugin/plan-your-day/src/Plugin.php b/plugin/plan-your-day/src/Plugin.php index 470ee20..a48101a 100644 --- a/plugin/plan-your-day/src/Plugin.php +++ b/plugin/plan-your-day/src/Plugin.php @@ -89,8 +89,7 @@ private function __construct() { $this->category_catalog, $this->request_state_parser, $this->planner_state_builder(), - $this->planner_payload_builder, - $this->visitor_token_manager + $this->planner_payload_builder ); $this->planner_shortcode = new PlannerShortcode( $this->planner_renderer, $this->frontend_assets ); $this->planner_block = new PlannerBlock( $this->planner_renderer, $this->frontend_assets ); diff --git a/plugin/plan-your-day/src/Rest/PlannerRoutes.php b/plugin/plan-your-day/src/Rest/PlannerRoutes.php index ceadce7..9eb56d2 100644 --- a/plugin/plan-your-day/src/Rest/PlannerRoutes.php +++ b/plugin/plan-your-day/src/Rest/PlannerRoutes.php @@ -56,6 +56,16 @@ public function __construct( } public function register(): void { + register_rest_route( + self::REST_NAMESPACE, + '/bootstrap', + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'bootstrap' ], + 'permission_callback' => '__return_true', + ] + ); + register_rest_route( self::REST_NAMESPACE, '/browse', @@ -79,6 +89,32 @@ public function register(): void { ); } + public function bootstrap( WP_REST_Request $request ): WP_REST_Response|WP_Error { + $guard = $this->guard_bootstrap_request(); + + if ( $guard instanceof WP_Error ) { + return $guard; + } + + $endpoint_token = $this->visitor_token_manager->get_endpoint_token(); + + if ( '' === $endpoint_token ) { + return new WP_Error( + 'plan_your_day_token_unavailable', + $this->request_verification_failed_message(), + [ + 'status' => 403, + ] + ); + } + + return new WP_REST_Response( + [ + 'endpointToken' => $endpoint_token, + ] + ); + } + public function browse( WP_REST_Request $request ): WP_REST_Response|WP_Error { DebugLogger::log( 'rest.browse.request', @@ -180,6 +216,20 @@ public function route( WP_REST_Request $request ): WP_REST_Response|WP_Error { ); } + private function guard_bootstrap_request(): ?WP_Error { + if ( ! $this->request_origin_validator->is_same_site_request( $_SERVER ) ) { + return new WP_Error( + 'plan_your_day_invalid_origin', + $this->request_verification_failed_message(), + [ + 'status' => 403, + ] + ); + } + + return $this->rate_limiter->enforce( 'bootstrap', $_SERVER, self::RATE_LIMIT_BASE_COST ); + } + private function guard_request( WP_REST_Request $request, string $scope, array $request_state ): ?WP_Error { if ( ! $this->request_origin_validator->is_same_site_request( $_SERVER ) ) { $error = new WP_Error( diff --git a/plugin/plan-your-day/tests/PlannerBlockTest.php b/plugin/plan-your-day/tests/PlannerBlockTest.php index aa41a48..c7a7f0a 100644 --- a/plugin/plan-your-day/tests/PlannerBlockTest.php +++ b/plugin/plan-your-day/tests/PlannerBlockTest.php @@ -132,7 +132,6 @@ function wp_unique_id( string $prefix = '' ): string { use Acodebeard\PlanYourDay\Planner\StartContextResolver; use Acodebeard\PlanYourDay\Planner\WaypointList; use Acodebeard\PlanYourDay\Security\RequestOriginValidator; - use Acodebeard\PlanYourDay\Security\VisitorTokenManager; use Acodebeard\PlanYourDay\Settings\Settings; use PHPUnit\Framework\TestCase; @@ -224,6 +223,10 @@ public function test_render_enqueues_frontend_assets_and_uses_shared_renderer_ou self::assertStringContainsString( 'class="plan-your-day"', $output ); self::assertStringContainsString( 'data-plan-color-mode-default="dark"', $output ); self::assertStringContainsString( '"colorModeDefault":"dark"', $output ); + self::assertStringContainsString( '"bootstrapUrl":"https:\/\/example.test\/wp-json\/plan-your-day\/v1\/bootstrap"', $output ); + self::assertStringContainsString( '"endpointToken":""', $output ); + self::assertStringNotContainsString( 'plan_your_day_visitor', $output ); + self::assertStringNotContainsString( hash_hmac( 'sha256', str_repeat( 'ab', 24 ), 'tests-auth|plan-your-day' ), $output ); self::assertStringContainsString( 'action="https://example.test/planner#plan-your-day-1"', $output ); self::assertStringNotContainsString( 'Editable starting point helper.', $output ); self::assertStringNotContainsString( 'Editable custom start label', $output ); @@ -287,8 +290,7 @@ private function build_renderer(): PlannerRenderer { $category_catalog, $request_state_parser, $planner_state_builder, - new PlannerPayloadBuilder( $settings ), - new VisitorTokenManager() + new PlannerPayloadBuilder( $settings ) ); } } diff --git a/plugin/plan-your-day/tests/PlannerRoutesTest.php b/plugin/plan-your-day/tests/PlannerRoutesTest.php index a860f0c..56c6d79 100644 --- a/plugin/plan-your-day/tests/PlannerRoutesTest.php +++ b/plugin/plan-your-day/tests/PlannerRoutesTest.php @@ -204,6 +204,46 @@ public function test_route_rate_limiter_uses_trusted_proxy_forwarded_client_ip() self::assertSame( 'plan_your_day_rate_limited', $second->get_error_code() ); } + public function test_bootstrap_returns_endpoint_token_for_same_site_request(): void { + $visitor_token = str_repeat( 'ab', 24 ); + $routes = $this->build_routes( 10 ); + $request = new WP_REST_Request( 'POST', '/plan-your-day/v1/bootstrap' ); + + $_SERVER = $this->same_site_server( '198.51.100.10' ); + $_COOKIE['plan_your_day_visitor'] = $visitor_token; + + $response = $routes->bootstrap( $request ); + + self::assertInstanceOf( WP_REST_Response::class, $response ); + self::assertSame( + [ + 'endpointToken' => hash_hmac( 'sha256', $visitor_token, 'tests-auth|plan-your-day' ), + ], + $response->get_data() + ); + } + + public function test_bootstrap_rejects_cross_site_request_before_token_creation(): void { + $routes = $this->build_routes( 10 ); + $request = new WP_REST_Request( 'POST', '/plan-your-day/v1/bootstrap' ); + + $_SERVER = $this->same_site_server( + '198.51.100.10', + [ + 'HTTP_ORIGIN' => 'https://evil.example', + 'HTTP_REFERER' => 'https://evil.example/planner', + 'HTTP_SEC_FETCH_SITE' => 'cross-site', + ] + ); + + $response = $routes->bootstrap( $request ); + + self::assertInstanceOf( WP_Error::class, $response ); + self::assertSame( 'plan_your_day_invalid_origin', $response->get_error_code() ); + self::assertSame( 403, $response->get_error_data()['status'] ?? null ); + self::assertArrayNotHasKey( 'plan_your_day_visitor', $_COOKIE ); + } + public function test_browse_append_results_uses_cached_search_context_ids_and_skips_route_refresh(): void { $google_api_client = new PlannerRoutesGoogleApiClient( GoogleApiResult::success( diff --git a/plugin/plan-your-day/tests/browser-app/router.php b/plugin/plan-your-day/tests/browser-app/router.php index a074254..c3668d6 100644 --- a/plugin/plan-your-day/tests/browser-app/router.php +++ b/plugin/plan-your-day/tests/browser-app/router.php @@ -62,6 +62,10 @@ plan_your_day_browser_render_page( 'plain' ); return; + case '/wp-json/plan-your-day/v1/bootstrap': + plan_your_day_browser_dispatch_rest( 'bootstrap' ); + return; + case '/wp-json/plan-your-day/v1/browse': plan_your_day_browser_dispatch_rest( 'browse' ); return; @@ -109,8 +113,7 @@ function plan_your_day_browser_app(): array { $category_catalog, $request_state_parser, $planner_state_builder, - $planner_payload_builder, - $visitor_token_manager + $planner_payload_builder ); $planner_routes = new PlannerRoutes( @@ -192,9 +195,13 @@ function plan_your_day_browser_dispatch_rest( string $route_name ): void { $request->set_param( (string) $key, $value ); } - $result = 'browse' === $route_name - ? $app['routes']->browse( $request ) - : $app['routes']->route( $request ); + if ( 'bootstrap' === $route_name ) { + $result = $app['routes']->bootstrap( $request ); + } elseif ( 'browse' === $route_name ) { + $result = $app['routes']->browse( $request ); + } else { + $result = $app['routes']->route( $request ); + } if ( $result instanceof WP_Error ) { plan_your_day_browser_send_json(