PluginProbe
Code Snippets / 4.0.0-beta.2
Code Snippets v4.0.0-beta.2
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
code-snippets / js / utils / restAPI.ts

restAPI.ts in Code Snippets 4.0.0-beta.2, at js/utils/restAPI.ts

142 lines 5.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { trimTrailingChar } from './text'
2 import { buildUrl } from './urls'
3 import type { UrlQueryArgs } from './urls'
4 import type { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'
5
6 const normalizeUrl = (url: string | undefined) =>
7 trimTrailingChar(url ?? '', '/')
8
9 export const REST_BASES = {
10 base: normalizeUrl(window.CODE_SNIPPETS?.restAPI.base),
11 snippets: normalizeUrl(window.CODE_SNIPPETS?.restAPI.snippets),
12 recentlyActive: normalizeUrl(window.CODE_SNIPPETS?.restAPI.recentlyActive),
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 },
22 cloud: {
23 snippets: normalizeUrl(window.CODE_SNIPPETS?.restAPI.cloud.snippets),
24 }
25 }
26
27 /** Verbs that hosts and firewalls commonly reject outright. */
28 const OVERRIDDEN_METHODS = ['delete', 'put', 'patch']
29
30 /**
31 * Send write requests as POST, naming the intended verb in a header.
32 *
33 * Plenty of hosts allow only GET and POST, so a DELETE never reaches
34 * WordPress: the request is rejected upstream, and the browser reports a 403 —
35 * or a severed connection — that no amount of correct authentication can fix.
36 * The REST server reads `X-HTTP-Method-Override` on a POST and dispatches the
37 * route exactly as it would have, so this changes nothing WordPress sees while
38 * letting the request through.
39 */
40 export const applyMethodOverride = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
41 const method = config.method?.toLowerCase()
42
43 if (!method || !OVERRIDDEN_METHODS.includes(method)) {
44 return config
45 }
46
47 config.headers.set('X-HTTP-Method-Override', method.toUpperCase())
48 config.method = 'post'
49
50 return config
51 }
52
53 /**
54 * The REST nonce to authenticate the next request with.
55 *
56 * Held in a variable rather than baked into the axios config, because the value
57 * the page was rendered with does not stay valid. A nonce expires with the
58 * session, and the snippet editor is a screen people leave open for a long
59 * time. Once it lapsed, every save failed with a 403 and the only cure was
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.
64 */
65 let restNonce = window.CODE_SNIPPETS?.restAPI.nonce ?? window.CODE_SNIPPETS_FEEDBACK?.nonce
66 let runOnceNonce = window.CODE_SNIPPETS_MANAGE?.runOnceNonce
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
71 /**
72 * Keep the REST nonce current for as long as the page is open.
73 *
74 * WordPress already sends a freshly minted nonce with every Heartbeat response,
75 * from `wp_refresh_heartbeat_nonces()`. Core applies it to `wpApiSettings`,
76 * which our screens do not enqueue, so the value went unused. Listening for the
77 * tick ourselves means an editor left open stays able to save.
78 */
79 export const listenForNonceRefresh = () => {
80 // Heartbeat also fires the tick through the hooks API, which avoids
81 // depending on jQuery being present and typed.
82 window.wp.hooks?.addAction(
83 'heartbeat.tick',
84 'code-snippets/refresh-rest-nonce',
85 (data: { rest_nonce?: string, code_snippets_run_once_nonce?: string }) => {
86 if (data.rest_nonce) {
87 restNonce = data.rest_nonce
88 }
89
90 if (data.code_snippets_run_once_nonce) {
91 runOnceNonce = data.code_snippets_run_once_nonce
92 }
93 }
94 )
95 }
96
97 /**
98 * Attach the current nonce to an outgoing request.
99 *
100 * Read per request, so that a nonce refreshed since page load is actually used.
101 */
102 export const applyRestNonce = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
103 if (restNonce) {
104 config.headers.set('X-WP-Nonce', restNonce)
105 }
106
107 return config
108 }
109
110 export const REST_API_AXIOS_CONFIG: AxiosRequestConfig = {
111 headers: {
112 'Access-Control': window.CODE_SNIPPETS?.restAPI.cloud.token
113 }
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)
142