PluginProbe
RabbitLoader / trunk
RabbitLoader vtrunk
4.0.2 4.0.1 4.0 3.2.0 3.1.1 3.1.0 3.0.5 3.0.3 3.0.4 2.17.1 2.17.2 2.17.3 2.17.4 2.17.5 2.17.6 2.17.7 2.18.0 2.18.1 2.18.2 2.18.3 2.18.4 2.18.5 2.18.6 2.18.7 2.18.8 All 129 releases
rabbit-loader / admin / js / connect-proof.js

connect-proof.js in RabbitLoader trunk, at admin/js/connect-proof.js

319 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function () {
2 'use strict';
3
4 var savingToken = false;
5 var proofInFlight = false;
6
7 // If the dashboard delivers a token but saving stalls, don't sit on
8 // "Saving…" forever. Fail open into a retry after this many ms.
9 var SAVE_TIMEOUT_MS = 20000;
10
11 function config() {
12 return window.RL5Config || {};
13 }
14
15 function debug() {
16 var cfg = config();
17 if (!cfg.debug || !window.console) {
18 return;
19 }
20 var args = Array.prototype.slice.call(arguments);
21 args.unshift('[RabbitLoader connect]');
22 console.log.apply(console, args);
23 }
24
25 function status(message, type) {
26 if (window.RL5ConnectUI && typeof window.RL5ConnectUI.status === 'function') {
27 window.RL5ConnectUI.status(message, type);
28 }
29 }
30
31 function resetConnectButton() {
32 var button = document.getElementById('rl5-connect-button');
33 if (button) {
34 button.disabled = false;
35 button.textContent = button.dataset.originalLabel || 'Connect RabbitLoader';
36 }
37 }
38
39 function parseMessage(raw) {
40 if (raw && typeof raw === 'object') {
41 return raw;
42 }
43
44 if (typeof raw === 'string') {
45 try {
46 var parsed = JSON.parse(raw);
47 return parsed && typeof parsed === 'object' ? parsed : {};
48 } catch (e) {
49 return {};
50 }
51 }
52
53 return {};
54 }
55
56 function layers(data) {
57 var out = [data];
58 ['payload', 'data', 'detail', 'message'].forEach(function (key) {
59 if (data && data[key] && typeof data[key] === 'object') {
60 out.push(data[key]);
61 }
62 });
63 return out;
64 }
65
66 function field(data, names) {
67 var candidates = layers(data);
68
69 for (var i = 0; i < candidates.length; i++) {
70 for (var j = 0; j < names.length; j++) {
71 var value = candidates[i][names[j]];
72 if (value !== undefined && value !== null && String(value).trim() !== '') {
73 return String(value);
74 }
75 }
76 }
77
78 return '';
79 }
80
81 function allowedOrigin(origin) {
82 var allowed = config().allowedOrigins || [];
83 return allowed.indexOf(origin) !== -1;
84 }
85
86 async function readJsonResponse(response) {
87 var text = await response.text();
88 var body = {};
89
90 try {
91 body = text ? JSON.parse(text) : {};
92 } catch (e) {
93 body = {
94 result: false,
95 message: text || ('HTTP ' + response.status)
96 };
97 }
98
99 return {
100 response: response,
101 body: body
102 };
103 }
104
105 // Rejects if the fetch doesn't settle within `ms`. Lets a stalled save
106 // surface as a clear retry instead of an endless "Saving…" spinner.
107 function withTimeout(promise, ms, message) {
108 return new Promise(function (resolve, reject) {
109 var timer = window.setTimeout(function () {
110 reject(new Error(message || 'Request timed out.'));
111 }, ms);
112
113 promise.then(
114 function (value) {
115 window.clearTimeout(timer);
116 resolve(value);
117 },
118 function (error) {
119 window.clearTimeout(timer);
120 reject(error);
121 }
122 );
123 });
124 }
125
126 async function postForm(values) {
127 var cfg = config();
128 var params = new URLSearchParams();
129
130 Object.keys(values).forEach(function (key) {
131 if (values[key] !== undefined && values[key] !== null) {
132 params.set(key, String(values[key]));
133 }
134 });
135
136 var response = await fetch(cfg.ajaxUrl, {
137 method: 'POST',
138 credentials: 'same-origin',
139 headers: {
140 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
141 },
142 body: params.toString()
143 });
144
145 return readJsonResponse(response);
146 }
147
148 async function handleProof(event, data) {
149 if (proofInFlight) {
150 return;
151 }
152
153 var cfg = config();
154 var challengeId = field(data, ['challenge_id', 'challengeId']);
155 var challengeNonce = field(data, ['challenge_nonce', 'challengeNonce']);
156 var siteUrl = field(data, ['site_url', 'siteUrl']) || cfg.homeUrl || '';
157
158 debug('proof message', {
159 origin: event.origin,
160 challenge_id_present: !!challengeId,
161 challenge_nonce_present: !!challengeNonce,
162 site_url: siteUrl
163 });
164
165 if (!challengeId || !challengeNonce || !siteUrl) {
166 status('RabbitLoader sent an incomplete reconnect challenge.', 'error');
167
168 if (event.source && typeof event.source.postMessage === 'function') {
169 event.source.postMessage({
170 type: 'rabbitloader:connect-proof-response',
171 source: 'rabbitloader-wordpress-plugin',
172 result: false,
173 success: false,
174 challenge_id: challengeId,
175 site_url: siteUrl,
176 message: 'Incomplete reconnect challenge.'
177 }, event.origin);
178 }
179 return;
180 }
181
182 proofInFlight = true;
183 status('Verifying this WordPress site…', 'info');
184
185 try {
186 var result = await postForm({
187 action: 'rabbitloader_connect_proof',
188 rl_nonce: cfg.nonce || '',
189 challenge_id: challengeId,
190 challenge_nonce: challengeNonce,
191 site_url: siteUrl
192 });
193
194 debug('proof ajax', result.response.status, result.body);
195
196 if (!result.response.ok || !result.body || result.body.result !== true || !result.body.redeem_token) {
197 throw new Error(
198 result.body && result.body.message
199 ? result.body.message
200 : 'WordPress could not redeem the RabbitLoader reconnect challenge.'
201 );
202 }
203
204 if (event.source && typeof event.source.postMessage === 'function') {
205 event.source.postMessage({
206 type: 'rabbitloader:connect-proof-response',
207 source: 'rabbitloader-wordpress-plugin',
208 result: true,
209 success: true,
210 challenge_id: challengeId,
211 site_url: result.body.site_url || siteUrl,
212 redeem_token: result.body.redeem_token,
213 expires_at: result.body.expires_at || 0
214 }, event.origin);
215 }
216
217 status('Site verified. Finishing RabbitLoader connection…', 'success');
218 } catch (error) {
219 debug('proof failed', error);
220
221 if (event.source && typeof event.source.postMessage === 'function') {
222 event.source.postMessage({
223 type: 'rabbitloader:connect-proof-response',
224 source: 'rabbitloader-wordpress-plugin',
225 result: false,
226 success: false,
227 challenge_id: challengeId,
228 site_url: siteUrl,
229 message: error && error.message ? error.message : 'Reconnect proof failed.'
230 }, event.origin);
231 }
232
233 status(error && error.message ? error.message : 'Reconnect proof failed.', 'error');
234 } finally {
235 proofInFlight = false;
236 }
237 }
238
239 async function handleFinalToken(data) {
240 if (savingToken) {
241 return;
242 }
243
244 var token = field(data, ['token', 'rl-token', 'rl_token']);
245
246 if (!token) {
247 return;
248 }
249
250 savingToken = true;
251 status('Saving RabbitLoader connection…', 'info');
252
253 try {
254 var cfg = config();
255 var result = await withTimeout(
256 postForm({
257 action: 'rabbitloader_save_keys',
258 rl_nonce: cfg.nonce || '',
259 'rl-token': token
260 }),
261 SAVE_TIMEOUT_MS,
262 'Login completed but saving timed out. Please click Connect again.'
263 );
264
265 debug('save token ajax', result.response.status, result.body);
266
267 if (!result.response.ok || !result.body || result.body.result !== true) {
268 throw new Error(
269 result.body && result.body.message
270 ? result.body.message
271 : 'RabbitLoader login completed, but WordPress could not save the connection.'
272 );
273 }
274
275 window.RL5ConnectionCompleted = true;
276 status('Connected. Loading your RabbitLoader dashboard…', 'success');
277
278 window.setTimeout(function () {
279 window.location.assign(result.body.redirect_url || cfg.dashboardUrl || window.location.href);
280 }, 150);
281 } catch (error) {
282 savingToken = false;
283 debug('save token failed', error);
284 status(error && error.message ? error.message : 'Could not save RabbitLoader connection.', 'error');
285 resetConnectButton();
286 }
287 }
288
289 window.addEventListener('message', function (event) {
290 if (!allowedOrigin(event.origin)) {
291 return;
292 }
293
294 var data = parseMessage(event.data);
295 var type = field(data, ['type']);
296
297 debug('message', event.origin, type, data);
298
299 if (type === 'rabbitloader:connect-proof-request') {
300 void handleProof(event, data);
301 return;
302 }
303
304 if (type === 'rabbitloader:connect') {
305 void handleFinalToken(data);
306 return;
307 }
308
309 // Fallback: some dashboard builds deliver the credentials under a
310 // different message type. If any message from an allowed origin
311 // carries a token, treat it as the final connect step.
312 var maybeToken = field(data, ['token', 'rl-token', 'rl_token']);
313 if (maybeToken) {
314 debug('final token via fallback type', type);
315 void handleFinalToken(data);
316 }
317 });
318 })();
319