| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | import { trimTrailingChar } from './text' |
| 2 | +import { buildUrl } from './urls' | |
| 3 | +import type { UrlQueryArgs } from './urls' | |
| 2 | 4 | import type { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios' |
| 3 | 5 | |
| 4 | 6 | const normalizeUrl = (url: string | undefined) => |
| 5 | 7 | trimTrailingChar(url ?? '', '/') |
| @@ -4,13 +6,20 @@ | ||
| 4 | 6 | const normalizeUrl = (url: string | undefined) => |
| 5 | 7 | trimTrailingChar(url ?? '', '/') |
| 6 | 8 | |
| 7 | 9 | export const REST_BASES = { |
| 10 | + base: normalizeUrl(window.CODE_SNIPPETS?.restAPI.base), | |
| 8 | 11 | snippets: normalizeUrl(window.CODE_SNIPPETS?.restAPI.snippets), |
| 9 | 12 | recentlyActive: normalizeUrl(window.CODE_SNIPPETS?.restAPI.recentlyActive), |
| 10 | - preferences: normalizeUrl(window.CODE_SNIPPETS?.restAPI.preferences), | |
| 11 | - importPlugins: normalizeUrl(window.CODE_SNIPPETS?.restAPI.importPlugins), | |
| 12 | - importFiles: normalizeUrl(window.CODE_SNIPPETS?.restAPI.importFiles), | |
| 13 | + import: { | |
| 14 | + plugins: normalizeUrl(window.CODE_SNIPPETS?.restAPI.importPlugins), | |
| 15 | + files: normalizeUrl(window.CODE_SNIPPETS?.restAPI.importFiles), | |
| 16 | + }, | |
| 17 | + preferences: { | |
| 18 | + snippetView: normalizeUrl(window.CODE_SNIPPETS?.restAPI.snippetView), | |
| 19 | + insights: normalizeUrl(window.CODE_SNIPPETS?.restAPI.insightsView), | |
| 20 | + demosSeen: normalizeUrl(window.CODE_SNIPPETS?.restAPI.demosSeen), | |
| 21 | + }, | |
| 13 | 22 | cloud: { |
| 14 | 23 | snippets: normalizeUrl(window.CODE_SNIPPETS?.restAPI.cloud.snippets), |
| 15 | 24 | } |
| 16 | 25 | } |
| @@ -48,11 +57,18 @@ | ||
| 48 | 57 | * the page was rendered with does not stay valid. A nonce expires with the |
| 49 | 58 | * session, and the snippet editor is a screen people leave open for a long |
| 50 | 59 | * time. Once it lapsed, every save failed with a 403 and the only cure was |
| 51 | 60 | * reloading the page, which loses whatever was being written. |
| 61 | + * | |
| 62 | + * The feedback reporter mounts on screens that do not enqueue the main | |
| 63 | + * `CODE_SNIPPETS` object, so it carries a nonce of its own to fall back on. | |
| 52 | 64 | */ |
| 53 | -let restNonce = window.CODE_SNIPPETS?.restAPI.nonce | |
| 65 | +let restNonce = window.CODE_SNIPPETS?.restAPI.nonce ?? window.CODE_SNIPPETS_FEEDBACK?.nonce | |
| 66 | +let runOnceNonce = window.CODE_SNIPPETS_MANAGE?.runOnceNonce | |
| 54 | 67 | |
| 68 | +/** The Run Once nonce as last refreshed by the Heartbeat, or the one rendered with the page. */ | |
| 69 | +export const getRunOnceNonce = (): string => runOnceNonce ?? '' | |
| 70 | + | |
| 55 | 71 | /** |
| 56 | 72 | * Keep the REST nonce current for as long as the page is open. |
| 57 | 73 | * |
| 58 | 74 | * WordPress already sends a freshly minted nonce with every Heartbeat response, |
| @@ -65,12 +81,16 @@ | ||
| 65 | 81 | // depending on jQuery being present and typed. |
| 66 | 82 | window.wp.hooks?.addAction( |
| 67 | 83 | 'heartbeat.tick', |
| 68 | 84 | 'code-snippets/refresh-rest-nonce', |
| 69 | - (data: { rest_nonce?: string }) => { | |
| 85 | + (data: { rest_nonce?: string, code_snippets_run_once_nonce?: string }) => { | |
| 70 | 86 | if (data.rest_nonce) { |
| 71 | 87 | restNonce = data.rest_nonce |
| 72 | 88 | } |
| 89 | + | |
| 90 | + if (data.code_snippets_run_once_nonce) { | |
| 91 | + runOnceNonce = data.code_snippets_run_once_nonce | |
| 92 | + } | |
| 73 | 93 | } |
| 74 | 94 | ) |
| 75 | 95 | } |
| 76 | 96 | |
| @@ -91,4 +111,31 @@ | ||
| 91 | 111 | headers: { |
| 92 | 112 | 'Access-Control': window.CODE_SNIPPETS?.restAPI.cloud.token |
| 93 | 113 | } |
| 94 | 114 | } |
| 115 | + | |
| 116 | +export interface QueryArg { | |
| 117 | + url: string | |
| 118 | + name: string | |
| 119 | + value: string | |
| 120 | +} | |
| 121 | + | |
| 122 | +/** | |
| 123 | + * Add a query parameter to a REST URL. | |
| 124 | + * | |
| 125 | + * Concatenation is not enough: with plain permalinks a REST URL already carries the route | |
| 126 | + * in a query string, so a second `?` would bury the parameter inside the route instead of | |
| 127 | + * adding one. | |
| 128 | + */ | |
| 129 | +export const addQueryArg = ({ url, name, value }: QueryArg): string => { | |
| 130 | + const parsed = new URL(url, window.location.origin) | |
| 131 | + parsed.searchParams.set(name, value) | |
| 132 | + return parsed.toString() | |
| 133 | +} | |
| 134 | + | |
| 135 | +/** | |
| 136 | + * A WordPress core REST route (`wp/v2/…`) with its query arguments, built so it | |
| 137 | + * works whether or not the site has pretty permalinks: without them the REST | |
| 138 | + * base already carries a query string, so arguments must be appended with `&`. | |
| 139 | + */ | |
| 140 | +export const buildWpRestUrl = (route: string, args: UrlQueryArgs = {}): string => | |
| 141 | + buildUrl(`${REST_BASES.base}/wp/v2/${route}`, args) | |