Skip to content

Commit 1185f9d

Browse files
author
Sebastian Thulin
committed
feat: implement async nonce
1 parent 1e3a7de commit 1185f9d

4 files changed

Lines changed: 185 additions & 3 deletions

File tree

source/js/asyncNonce/asyncNonce.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
interface AsyncNonceInterface {
2+
setup(form: HTMLFormElement): Promise<void>;
3+
}
4+
5+
class AsyncNonce {
6+
/**
7+
* Constructor for AsyncNonce.
8+
* @param modularityFrontendFormData The form data object containing API routes.
9+
*/
10+
constructor(
11+
private modularityFrontendFormData: ModularityFrontendFormData
12+
) {}
13+
14+
/**
15+
* Fetch the nonce from the server.
16+
* @returns The nonce string or null if not found.
17+
*/
18+
public async get(): Promise<string | null> {
19+
const url = this.modularityFrontendFormData?.apiRoutes?.nonceGet;
20+
21+
if (!url) {
22+
console.error("Nonce URL is not defined.");
23+
return null;
24+
}
25+
26+
try {
27+
const response = await fetch(url);
28+
if (!response.ok) {
29+
throw new Error(`HTTP error: ${response.status}`);
30+
}
31+
32+
const json = await response.json();
33+
return json?.nonce ?? null;
34+
} catch (error: any) {
35+
console.error("Nonce fetching failed:", error?.message || error);
36+
return null;
37+
}
38+
}
39+
40+
/**
41+
* Inject the nonce into the form as a hidden input field.
42+
* @param form The form element to inject the nonce into.
43+
* @param nonce The nonce value to inject.
44+
*/
45+
public inject(form: HTMLFormElement, nonce: string, nonceElementId: string): void {
46+
if (this.isNoncePresent(form, nonceElementId)) {
47+
this.removeNonce(form, nonceElementId);
48+
}
49+
50+
form.appendChild(
51+
this.createNonceElement(nonce, nonceElementId)
52+
);
53+
}
54+
55+
/**
56+
* Check if the nonce is already present in the form.
57+
* @param form The form element to check.
58+
* @returns True if the nonce is present, false otherwise.
59+
*/
60+
public isNoncePresent(form: HTMLFormElement, nonceElementId: string): boolean {
61+
return form.querySelector("#" + nonceElementId) !== null;
62+
}
63+
64+
/**
65+
* Remove the nonce from the form.
66+
* @param form The form element to remove the nonce from.
67+
*/
68+
public removeNonce(form: HTMLFormElement, nonceElementId: string): void {
69+
const existingNonceInput = form.querySelector("#" + nonceElementId);
70+
if (existingNonceInput) {
71+
form.removeChild(existingNonceInput);
72+
}
73+
}
74+
75+
/**
76+
* Create a nonce element to be injected into the form.
77+
* @param nonce The nonce value to inject.
78+
* @param nonceElementId The ID for the nonce input element.
79+
* @returns The created input element.
80+
*/
81+
public createNonceElement(nonce: string, nonceElementId: string): HTMLInputElement {
82+
const nonceInput = document.createElement("input");
83+
84+
nonceInput.id = nonceElementId;
85+
nonceInput.type = "hidden";
86+
nonceInput.name = "nonce";
87+
nonceInput.value = nonce;
88+
89+
nonceInput.setAttribute("aria-hidden", "true");
90+
nonceInput.setAttribute("autocomplete", "off");
91+
nonceInput.setAttribute("autocorrect", "off");
92+
93+
return nonceInput;
94+
}
95+
96+
/**
97+
* Setup the nonce by fetching it and injecting it into the form.
98+
* @param form The form element to inject the nonce into.
99+
*/
100+
public async setup(
101+
form: HTMLFormElement
102+
): Promise<void> {
103+
console.log("Setting up nonce...");
104+
const nonce = await this.get();
105+
if (nonce) {
106+
console.log("Nonce fetched successfully:", nonce);
107+
this.inject(form, nonce, "async-nonce-element");
108+
} else {
109+
console.error("Nonce is not defined.");
110+
}
111+
}
112+
}
113+
export default AsyncNonce;

source/js/init.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Submit from "./steps/submit/submit";
88
import FieldBuilder from "./fields/fieldBuilder";
99
import Fields from "./fields/fields";
1010
import ConditionBuilder from "./conditions/conditionBuilder";
11-
11+
import AsyncNonce from "./asyncNonce/asyncNonce";
1212

1313
declare const modularityFrontendFormData: ModularityFrontendFormData;
1414
declare const modularityFrontendFormLang: ModularityFrontendFormLang;
@@ -59,7 +59,8 @@ class Form {
5959
steps,
6060
new Submit(
6161
this.form,
62-
modularityFrontendFormData
62+
modularityFrontendFormData,
63+
new AsyncNonce(modularityFrontendFormData)
6364
),
6465
),
6566
new StepUIManager(

source/js/steps/submit/submit.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import AsyncNonce from "../../asyncNonce/asyncNonce";
12
interface SubmitInterface {
23
submit(event: Event): void | Promise<void>;
34
}
5+
46
class Submit implements SubmitInterface {
57
constructor(
68
private form: HTMLFormElement,
7-
private modularityFrontendFormData: ModularityFrontendFormData
9+
private modularityFrontendFormData: ModularityFrontendFormData,
10+
private asyncNonce: AsyncNonce
811
) {}
912

1013

@@ -17,6 +20,9 @@ class Submit implements SubmitInterface {
1720
console.error("Submit URL is not defined.");
1821
return;
1922
}
23+
24+
//Init nonce by fetching nonce from endpoint and injecting it into the form
25+
await this.asyncNonce.setup(this.form);
2026

2127
try {
2228
const response = await fetch(url, {

source/php/Api/Nonce/Get.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace ModularityFrontendForm\Api\Nonce;
4+
5+
use ModularityFrontendForm\Api\RestApiEndpoint;
6+
use ModularityFrontendForm\Config\ConfigInterface;
7+
use WP_Error;
8+
use WP_REST_Request;
9+
use WP_REST_Response;
10+
use WP_REST_Server;
11+
use WpService\WpService;
12+
13+
class Get extends RestApiEndpoint
14+
{
15+
public const NAMESPACE = 'modularity-frontend-form/v1';
16+
public const ROUTE = 'nonce/get';
17+
public const KEY = 'nonceGet';
18+
19+
public function __construct(
20+
private WpService $wpService,
21+
private ConfigInterface $config
22+
) {}
23+
24+
/**
25+
* Registers a REST route
26+
*
27+
* @return bool Whether the route was registered successfully
28+
*/
29+
public function handleRegisterRestRoute(): bool
30+
{
31+
return register_rest_route(self::NAMESPACE, self::ROUTE, array(
32+
'methods' => WP_REST_Server::READABLE,
33+
'callback' => array($this, 'handleRequest'),
34+
'permission_callback' => array($this, 'permissionCallback')
35+
));
36+
}
37+
38+
39+
/**
40+
* Handles a REST request and sideloads an image
41+
*
42+
* @param WP_REST_Request $request The REST request object
43+
*
44+
* @return WP_REST_Response|WP_Error The sideloaded image URL or an error object if the sideload fails
45+
*/
46+
public function handleRequest(WP_REST_Request $request): WP_REST_Response
47+
{
48+
return new WP_REST_Response([
49+
'nonce' => $this->wpService->wpCreateNonce($this->config->getNonceKey()),
50+
]);
51+
}
52+
53+
/**
54+
* Callback function for checking if the current user has permission to get a nonce
55+
*
56+
* @return bool Whether the user has permission to get a nonce
57+
*/
58+
public function permissionCallback(): bool
59+
{
60+
return true;
61+
}
62+
}

0 commit comments

Comments
 (0)