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
← All changes | js/utils/restAPI.ts +123 -5 3.10.04.0.0-beta.2 View file →
@@ -1,23 +1,141 @@
1 1 import { trimTrailingChar } from './text'
2 -import type { AxiosRequestConfig } from 'axios'
2 +import { buildUrl } from './urls'
3 +import type { UrlQueryArgs } from './urls'
4 +import type { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'
3 5
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 }
17 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 +
18 110 export const REST_API_AXIOS_CONFIG: AxiosRequestConfig = {
19 111 headers: {
20 - 'X-WP-Nonce': window.CODE_SNIPPETS?.restAPI.nonce,
21 112 'Access-Control': window.CODE_SNIPPETS?.restAPI.cloud.token
22 113 }
23 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)