WiFiManager: stream info and wifi pages through DFTE.

Harden the root placeholder path, add direct render-isolation coverage, move simple status pages and /info onto request-scoped streamed templates, and switch /wifi to a streamed shell while keeping scan/form sections string-built for now.
This commit is contained in:
2026-04-17 06:41:44 +10:00
parent 9f111ab8bd
commit 69aea5ce35
9 changed files with 613 additions and 190 deletions
@@ -22,6 +22,10 @@
// DFTE (template engine)
#include <TemplateEngine.h>
#ifndef WM_TEMPLATE_REGISTRY_CAPACITY
#define WM_TEMPLATE_REGISTRY_CAPACITY 24
#endif
// Forward declarations
class WiFiManager;
+32
View File
@@ -0,0 +1,32 @@
/**
* Info.h
* Streamed info page template for WiFiManager.
*/
#ifndef _WM_INFO_TEMPLATE_H_
#define _WM_INFO_TEMPLATE_H_
#include <Arduino.h>
const char WM_INFO_TEMPLATE[] PROGMEM =
"<!DOCTYPE html>"
"<html lang='en'>"
"<head>"
"<meta name='format-detection' content='telephone=no'>"
"<meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
"<title>%DOC_TITLE%</title>"
"%SCRIPTS%"
"%STYLES%"
"</head>"
"<body class='info'>"
"<div class='wrap'>"
"%INFO_STATUS%"
"%INFO_DEVICE_SECTION%"
"%INFO_WIFI_SECTION%"
"%INFO_ABOUT_SECTION%"
"</div>"
"</body>"
"</html>";
#endif // _WM_INFO_TEMPLATE_H_
@@ -0,0 +1,29 @@
/**
* PageShell.h
* Generic whole-document shell template for streamed WiFiManager pages.
*/
#ifndef _WM_PAGE_SHELL_TEMPLATE_H_
#define _WM_PAGE_SHELL_TEMPLATE_H_
#include <Arduino.h>
const char WM_PAGE_SHELL_TEMPLATE[] PROGMEM =
"<!DOCTYPE html>"
"<html lang='en'>"
"<head>"
"<meta name='format-detection' content='telephone=no'>"
"<meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
"<title>%DOC_TITLE%</title>"
"%SCRIPTS%"
"%STYLES%"
"</head>"
"<body class='%BODY_CLASS%'>"
"<div class='wrap'>"
"%PAGE_CONTENT%"
"</div>"
"</body>"
"</html>";
#endif // _WM_PAGE_SHELL_TEMPLATE_H_
+32
View File
@@ -0,0 +1,32 @@
/**
* WiFi.h
* Streamed WiFi configuration page template for WiFiManager.
*/
#ifndef _WM_WIFI_TEMPLATE_H_
#define _WM_WIFI_TEMPLATE_H_
#include <Arduino.h>
const char WM_WIFI_TEMPLATE[] PROGMEM =
"<!DOCTYPE html>"
"<html lang='en'>"
"<head>"
"<meta name='format-detection' content='telephone=no'>"
"<meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
"<title>%DOC_TITLE%</title>"
"%SCRIPTS%"
"%STYLES%"
"</head>"
"<body class='wifi'>"
"<div class='wrap'>"
"%WIFI_SCAN_RESULTS%"
"%WIFI_FORM_SECTION%"
"%WIFI_BACK_SECTION%"
"%WIFI_STATUS%"
"</div>"
"</body>"
"</html>";
#endif // _WM_WIFI_TEMPLATE_H_
+394 -188
View File
@@ -13,6 +13,9 @@
#include "templates/HTML.h"
#include "templates/CSS.h"
#include "templates/JS.h"
#include "templates/PageShell.h"
#include "templates/Info.h"
#include "templates/WiFi.h"
#include "templates/RootSelector.h"
#include <TemplateEngine.h>
#include <cstring>
@@ -35,24 +38,72 @@ inline void reservePage(String& page, size_t extraBytes = WM_PAGE_RESERVE_BYTES)
(void)page.reserve(targetLen);
}
const char* dynamicStringGetter(void* userData) {
const auto* value = static_cast<const String*>(userData);
return value ? value->c_str() : "";
}
size_t dynamicStringLengthGetter(const char* data, void* /*userData*/) {
return data ? strlen(data) : 0;
}
void configureDynamicStringDescriptor(DynamicTemplateDescriptor& descriptor, String& value) {
descriptor.getter = &dynamicStringGetter;
descriptor.getLength = &dynamicStringLengthGetter;
descriptor.userData = &value;
}
template <typename BundleT>
AsyncWebServerResponse* beginTemplateResponse(AsyncWebServerRequest* request,
const std::shared_ptr<BundleT>& bundle,
const char* templateData) {
bundle->context.setRegistry(&bundle->registry);
TemplateRenderer::initializeContext(bundle->context, templateData);
return request->beginChunkedResponse(String(FPSTR(HTTP_HEAD_CT)),
[bundle](uint8_t *buffer, size_t maxLen, size_t /*index*/) -> size_t {
return TemplateRenderer::renderNextChunk(bundle->context, buffer, maxLen);
}
);
}
void registerSharedShellPlaceholders(WiFiManagerServer* server, PlaceholderRegistry& registry) {
if (server) {
server->registerDefaultStyles(registry);
server->registerDefaultScripts(registry);
server->applyTemplateSetupCallback(registry);
return;
}
registry.registerProgmemData("%STYLES%", CSS_STYLE);
registry.registerProgmemData("%SCRIPTS%", JS_SCRIPT);
}
struct RootState {
String menu;
String status;
};
const char* rootMenuGetter(void* userData) {
const auto* state = static_cast<const RootState*>(userData);
return state ? state->menu.c_str() : "";
}
struct MessagePageState {
String docTitle;
String bodyClass;
String content;
};
const char* rootStatusGetter(void* userData) {
const auto* state = static_cast<const RootState*>(userData);
return state ? state->status.c_str() : "";
}
struct InfoPageState {
String docTitle;
String status;
String deviceSection;
String wifiSection;
String aboutSection;
};
size_t rootTemplateLengthGetter(const char* data, void* /*userData*/) {
return data ? strlen(data) : 0;
}
struct WiFiPageState {
String docTitle;
String scanResults;
String formSection;
String backSection;
String status;
};
void escapePercentsForTemplate(String& value) {
if (value.indexOf('%') < 0) return;
@@ -81,6 +132,147 @@ void buildRootState(WiFiManagerHandlers* handlers, RootState& state) {
escapePercentsForTemplate(state.status);
}
void appendPortalHeader(const String& title,
const String& subtitle,
String& content) {
content += F("<h1>");
content += title;
content += F("</h1><h3>");
content += subtitle;
content += F("</h3>");
}
void buildMessagePageState(MessagePageState& state,
const __FlashStringHelper* title,
const __FlashStringHelper* bodyClass,
const String& contentHtml) {
state.docTitle = String(title);
state.bodyClass = String(bodyClass);
state.content = contentHtml;
escapePercentsForTemplate(state.content);
}
void appendInfoEntries(WiFiManagerHandlers* handlers,
String& section,
const char* const* ids,
size_t count) {
for (size_t i = 0; i < count; i++) {
section += handlers->getInfoData(ids[i]);
}
}
void buildInfoPageState(WiFiManagerHandlers* handlers,
InfoPageState& state,
bool showInfoUpdate,
bool showInfoErase,
bool showBack) {
reservePage(state.status, 512);
reservePage(state.deviceSection, 4096);
reservePage(state.wifiSection, 4096);
reservePage(state.aboutSection, 1536);
state.docTitle = F("Info");
handlers->reportStatus(state.status);
#ifdef ESP8266
static const char* const deviceIds[] = {
"esphead", "uptime", "chipid", "fchipid", "idesize", "flashsize",
"corever", "bootver", "cpufreq", "freeheap", "memsketch", "memsmeter",
"lastreset"
};
static const char* const wifiIds[] = {
"wifihead", "conx", "stassid", "staip", "stagw", "stasub", "dnss",
"host", "stamac", "autoconx", "wifiaphead", "apssid", "apip", "apbssid",
"apmac"
};
#elif defined(ESP32)
static const char* const deviceIds[] = {
"esphead", "uptime", "chipid", "chiprev", "idesize", "flashsize",
"cpufreq", "freeheap", "memsketch", "memsmeter", "lastreset", "temp"
};
static const char* const wifiIds[] = {
"wifihead", "conx", "stassid", "staip", "stagw", "stasub", "dnss",
"host", "stamac", "apssid", "wifiaphead", "apip", "apmac", "aphost",
"apbssid"
};
#endif
appendInfoEntries(handlers, state.deviceSection, deviceIds, sizeof(deviceIds) / sizeof(deviceIds[0]));
state.deviceSection += F("</dl>");
appendInfoEntries(handlers, state.wifiSection, wifiIds, sizeof(wifiIds) / sizeof(wifiIds[0]));
state.wifiSection += F("</dl>");
state.aboutSection += F("<h3>About</h3><hr><dl>");
state.aboutSection += handlers->getInfoData("aboutver");
state.aboutSection += handlers->getInfoData("aboutarduinover");
state.aboutSection += handlers->getInfoData("aboutidfver");
state.aboutSection += handlers->getInfoData("aboutdate");
state.aboutSection += F("</dl>");
if (showInfoUpdate) {
state.aboutSection += HTML_PORTAL_MENU[8];
state.aboutSection += HTML_PORTAL_MENU[9];
}
if (showInfoErase) state.aboutSection += FPSTR(HTML_ERASEBTN);
if (showBack) state.aboutSection += FPSTR(HTML_BACKBTN);
state.aboutSection += FPSTR(HTML_HELP);
escapePercentsForTemplate(state.status);
escapePercentsForTemplate(state.deviceSection);
escapePercentsForTemplate(state.wifiSection);
escapePercentsForTemplate(state.aboutSection);
}
void buildWiFiPageState(WiFiManagerHandlers* handlers,
WiFiPageState& state,
bool includeScanResults,
bool showBack,
const String& ssidPlaceholder,
const String& passwordPlaceholder,
bool paramsInWifi) {
reservePage(state.scanResults, includeScanResults ? 4096 : 64);
reservePage(state.formSection, 4096);
reservePage(state.backSection, 256);
reservePage(state.status, 512);
state.docTitle = F("Config ESP");
if (includeScanResults) {
state.scanResults += F("<div id=\"scan-results\">");
state.scanResults += handlers->getScanItemOut();
state.scanResults += F("</div>");
}
state.formSection += F("<form method='POST' action='wifisave'>");
state.formSection += F("<label for='s'>SSID</label><input id='s' name='s' maxlength='32' autocorrect='off' autocapitalize='none' placeholder='");
state.formSection += ssidPlaceholder;
state.formSection += F("'><br/><label for='p'>Password</label><input id='p' name='p' maxlength='64' type='password' placeholder='");
state.formSection += passwordPlaceholder;
state.formSection += F("'><input type='checkbox' id='showpass' onclick='f()'> <label for='showpass'>Show Password</label><br/>");
state.formSection += handlers->getStaticOut();
state.formSection += FPSTR(HTML_FORM_WIFI_END);
if (paramsInWifi) {
state.formSection += FPSTR(HTML_FORM_PARAM_HEAD);
state.formSection += handlers->getParamOut();
}
state.formSection += FPSTR(HTML_FORM_END);
state.formSection += F("<br/><div class=\"c\"><button id=\"refresh-btn\" onclick=\"refreshScan()\">Refresh</button></div>");
if (showBack) {
state.backSection += FPSTR(HTML_BACKBTN);
}
handlers->reportStatus(state.status);
escapePercentsForTemplate(state.scanResults);
escapePercentsForTemplate(state.formSection);
escapePercentsForTemplate(state.backSection);
escapePercentsForTemplate(state.status);
}
#if WM_ROOT_REQUEST_STATE
struct RootRenderBundle {
RootState state;
@@ -90,10 +282,63 @@ struct RootRenderBundle {
DynamicTemplateDescriptor statusDescriptor;
RootRenderBundle()
: registry(12), menuDescriptor{}, statusDescriptor{} {}
: registry(WM_TEMPLATE_REGISTRY_CAPACITY), menuDescriptor{}, statusDescriptor{} {}
};
#endif
struct MessageRenderBundle {
MessagePageState state;
PlaceholderRegistry registry;
TemplateContext context;
DynamicTemplateDescriptor docTitleDescriptor;
DynamicTemplateDescriptor bodyClassDescriptor;
DynamicTemplateDescriptor contentDescriptor;
MessageRenderBundle()
: registry(WM_TEMPLATE_REGISTRY_CAPACITY),
docTitleDescriptor{},
bodyClassDescriptor{},
contentDescriptor{} {}
};
struct InfoRenderBundle {
InfoPageState state;
PlaceholderRegistry registry;
TemplateContext context;
DynamicTemplateDescriptor docTitleDescriptor;
DynamicTemplateDescriptor statusDescriptor;
DynamicTemplateDescriptor deviceDescriptor;
DynamicTemplateDescriptor wifiDescriptor;
DynamicTemplateDescriptor aboutDescriptor;
InfoRenderBundle()
: registry(WM_TEMPLATE_REGISTRY_CAPACITY),
docTitleDescriptor{},
statusDescriptor{},
deviceDescriptor{},
wifiDescriptor{},
aboutDescriptor{} {}
};
struct WiFiRenderBundle {
WiFiPageState state;
PlaceholderRegistry registry;
TemplateContext context;
DynamicTemplateDescriptor docTitleDescriptor;
DynamicTemplateDescriptor scanResultsDescriptor;
DynamicTemplateDescriptor formDescriptor;
DynamicTemplateDescriptor backDescriptor;
DynamicTemplateDescriptor statusDescriptor;
WiFiRenderBundle()
: registry(WM_TEMPLATE_REGISTRY_CAPACITY),
docTitleDescriptor{},
scanResultsDescriptor{},
formDescriptor{},
backDescriptor{},
statusDescriptor{} {}
};
} // namespace
WiFiManagerHandlers::WiFiManagerHandlers(WiFiManager* wm) : _wm(wm) {}
@@ -788,29 +1033,24 @@ void WiFiManagerHandlers::handleRoot(AsyncWebServerRequest *request) {
buildRootState(this, bundle->state);
if (_wm->_serverManager) {
_wm->_serverManager->registerDefaultPlaceholders(bundle->registry);
_wm->_serverManager->registerDefaultStyles(bundle->registry);
_wm->_serverManager->registerDefaultScripts(bundle->registry);
_wm->_serverManager->registerDefaultPageTitle(bundle->registry);
_wm->_serverManager->registerDefaultSubtitle(bundle->registry);
_wm->_serverManager->applyTemplateSetupCallback(bundle->registry);
}
bundle->menuDescriptor.getter = &rootMenuGetter;
bundle->menuDescriptor.getLength = &rootTemplateLengthGetter;
bundle->menuDescriptor.userData = &bundle->state;
if (bundle->registry.getPlaceholder("%MENU%") == nullptr) {
configureDynamicStringDescriptor(bundle->menuDescriptor, bundle->state.menu);
bundle->registry.registerDynamicTemplate("%MENU%", &bundle->menuDescriptor);
}
bundle->statusDescriptor.getter = &rootStatusGetter;
bundle->statusDescriptor.getLength = &rootTemplateLengthGetter;
bundle->statusDescriptor.userData = &bundle->state;
if (bundle->registry.getPlaceholder("%STATUS%") == nullptr) {
configureDynamicStringDescriptor(bundle->statusDescriptor, bundle->state.status);
bundle->registry.registerDynamicTemplate("%STATUS%", &bundle->statusDescriptor);
}
bundle->registry.registerDynamicTemplate("%MENU%", &bundle->menuDescriptor);
bundle->registry.registerDynamicTemplate("%STATUS%", &bundle->statusDescriptor);
bundle->context.setRegistry(&bundle->registry);
TemplateRenderer::initializeContext(bundle->context, WM_ROOT_TEMPLATE);
response = request->beginChunkedResponse(String(FPSTR(HTTP_HEAD_CT)),
[bundle](uint8_t *buffer, size_t maxLen, size_t /*index*/) -> size_t {
return TemplateRenderer::renderNextChunk(bundle->context, buffer, maxLen);
}
);
response = beginTemplateResponse(request, bundle, WM_ROOT_TEMPLATE);
#else
TemplateContext ctx;
if (_wm->_serverManager && _wm->_serverManager->getPlaceholderRegistry()) {
@@ -843,18 +1083,13 @@ void WiFiManagerHandlers::handleWifi(AsyncWebServerRequest *request, boolean sca
return;
}
handleRequest(request);
String page = getHTTPHead(F("Config ESP"), FPSTR(C_wifi));
reservePage(page);
auto bundle = std::make_shared<WiFiRenderBundle>();
if (scan) {
bool forceRefresh = false;
if (request->hasParam("refresh")) {
forceRefresh = true;
}
page += F("<div id=\"scan-results\">");
page += getScanItemOut();
page += F("</div>");
if(forceRefresh || !_wm->_lastscan || (millis()-_wm->_lastscan > _wm->_scancachetime)){
if(!_wm->_scanInProgress){
_wm->WiFi_scanNetworks(true);
@@ -863,47 +1098,48 @@ void WiFiManagerHandlers::handleWifi(AsyncWebServerRequest *request, boolean sca
}
}
}
// Build WiFi form directly without tokens
page += F("<form method='POST' action='wifisave'>");
String ssidPlaceholder = _wm->WiFi_SSID();
String passwordPlaceholder = "";
if(_wm->_showPassword){
passwordPlaceholder = _wm->WiFi_psk();
}
else if(_wm->WiFi_psk() != ""){
passwordPlaceholder = F("********");
}
page += F("<label for='s'>SSID</label><input id='s' name='s' maxlength='32' autocorrect='off' autocapitalize='none' placeholder='");
page += ssidPlaceholder;
page += F("'><br/><label for='p'>Password</label><input id='p' name='p' maxlength='64' type='password' placeholder='");
page += passwordPlaceholder;
page += F("'><input type='checkbox' id='showpass' onclick='f()'> <label for='showpass'>Show Password</label><br/>");
page += getStaticOut();
page += FPSTR(HTML_FORM_WIFI_END);
if(_wm->_paramsInWifi && _wm->_paramsCount > 0){
page += FPSTR(HTML_FORM_PARAM_HEAD);
page += getParamOut();
}
page += FPSTR(HTML_FORM_END);
page += F("<br/><div class=\"c\"><button id=\"refresh-btn\" onclick=\"refreshScan()\">Refresh</button></div>");
if(_wm->_showBack) page += FPSTR(HTML_BACKBTN);
reportStatus(page);
page += getHTTPEnd();
buildWiFiPageState(this,
bundle->state,
scan,
_wm->_showBack,
ssidPlaceholder,
passwordPlaceholder,
_wm->_paramsInWifi && _wm->_paramsCount > 0);
registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry);
configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle);
configureDynamicStringDescriptor(bundle->scanResultsDescriptor, bundle->state.scanResults);
configureDynamicStringDescriptor(bundle->formDescriptor, bundle->state.formSection);
configureDynamicStringDescriptor(bundle->backDescriptor, bundle->state.backSection);
configureDynamicStringDescriptor(bundle->statusDescriptor, bundle->state.status);
bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor);
bundle->registry.registerDynamicTemplate("%WIFI_SCAN_RESULTS%", &bundle->scanResultsDescriptor);
bundle->registry.registerDynamicTemplate("%WIFI_FORM_SECTION%", &bundle->formDescriptor);
bundle->registry.registerDynamicTemplate("%WIFI_BACK_SECTION%", &bundle->backDescriptor);
bundle->registry.registerDynamicTemplate("%WIFI_STATUS%", &bundle->statusDescriptor);
#ifdef WM_DEBUG_LEVEL
_wm->DEBUG_WM(WM_DEBUG_DEV, F("Page length: "), String(page.length()));
size_t debugPageLength = bundle->state.scanResults.length() + bundle->state.formSection.length()
+ bundle->state.backSection.length() + bundle->state.status.length();
_wm->DEBUG_WM(WM_DEBUG_DEV, F("Page length: "), String(debugPageLength));
_wm->DEBUG_WM(WM_DEBUG_DEV, F("_numNetworks: "), String(_wm->_numNetworks));
_wm->DEBUG_WM(WM_DEBUG_DEV, F("_scanInProgress: "), _wm->_scanInProgress ? "true" : "false");
_wm->DEBUG_WM(WM_DEBUG_DEV, F("_lastscan: "), String(_wm->_lastscan));
_wm->DEBUG_WM(WM_DEBUG_DEV, F("About to send response"));
#endif
request->send(200, FPSTR(HTTP_HEAD_CT), page);
request->send(beginTemplateResponse(request, bundle, WM_WIFI_TEMPLATE));
#ifdef WM_DEBUG_LEVEL
_wm->DEBUG_WM(WM_DEBUG_DEV, F("Response sent"));
@@ -1119,100 +1355,23 @@ void WiFiManagerHandlers::handleInfo(AsyncWebServerRequest *request) {
_wm->DEBUG_WM(WM_DEBUG_VERBOSE, F("<- HTTP Info"));
#endif
handleRequest(request);
String page = getHTTPHead(F("Info"), FPSTR(C_info));
reservePage(page);
reportStatus(page);
auto bundle = std::make_shared<InfoRenderBundle>();
buildInfoPageState(this, bundle->state, _wm->_showInfoUpdate, _wm->_showInfoErase, _wm->_showBack);
uint16_t infos = 0;
registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry);
configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle);
configureDynamicStringDescriptor(bundle->statusDescriptor, bundle->state.status);
configureDynamicStringDescriptor(bundle->deviceDescriptor, bundle->state.deviceSection);
configureDynamicStringDescriptor(bundle->wifiDescriptor, bundle->state.wifiSection);
configureDynamicStringDescriptor(bundle->aboutDescriptor, bundle->state.aboutSection);
#ifdef ESP8266
infos = 28;
String infoids[] = {
F("esphead"),
F("uptime"),
F("chipid"),
F("fchipid"),
F("idesize"),
F("flashsize"),
F("corever"),
F("bootver"),
F("cpufreq"),
F("freeheap"),
F("memsketch"),
F("memsmeter"),
F("lastreset"),
F("wifihead"),
F("conx"),
F("stassid"),
F("staip"),
F("stagw"),
F("stasub"),
F("dnss"),
F("host"),
F("stamac"),
F("autoconx"),
F("wifiaphead"),
F("apssid"),
F("apip"),
F("apbssid"),
F("apmac")
};
bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor);
bundle->registry.registerDynamicTemplate("%INFO_STATUS%", &bundle->statusDescriptor);
bundle->registry.registerDynamicTemplate("%INFO_DEVICE_SECTION%", &bundle->deviceDescriptor);
bundle->registry.registerDynamicTemplate("%INFO_WIFI_SECTION%", &bundle->wifiDescriptor);
bundle->registry.registerDynamicTemplate("%INFO_ABOUT_SECTION%", &bundle->aboutDescriptor);
#elif defined(ESP32)
infos = 27;
String infoids[] = {
F("esphead"),
F("uptime"),
F("chipid"),
F("chiprev"),
F("idesize"),
F("flashsize"),
F("cpufreq"),
F("freeheap"),
F("memsketch"),
F("memsmeter"),
F("lastreset"),
F("temp"),
F("wifihead"),
F("conx"),
F("stassid"),
F("staip"),
F("stagw"),
F("stasub"),
F("dnss"),
F("host"),
F("stamac"),
F("apssid"),
F("wifiaphead"),
F("apip"),
F("apmac"),
F("aphost"),
F("apbssid")
};
#endif
for(size_t i=0; i<infos;i++){
if(infoids[i] != NULL) page += getInfoData(infoids[i]);
}
page += F("</dl>");
page += F("<h3>About</h3><hr><dl>");
page += getInfoData("aboutver");
page += getInfoData("aboutarduinover");
page += getInfoData("aboutidfver");
page += getInfoData("aboutdate");
page += F("</dl>");
if(_wm->_showInfoUpdate){
page += HTML_PORTAL_MENU[8];
page += HTML_PORTAL_MENU[9];
}
if(_wm->_showInfoErase) page += FPSTR(HTML_ERASEBTN);
if(_wm->_showBack) page += FPSTR(HTML_BACKBTN);
page += FPSTR(HTML_HELP);
page += getHTTPEnd();
request->send(200, FPSTR(HTTP_HEAD_CT), page);
request->send(beginTemplateResponse(request, bundle, WM_INFO_TEMPLATE));
#ifdef WM_DEBUG_LEVEL
_wm->DEBUG_WM(WM_DEBUG_DEV, F("Sent info page"));
@@ -1224,11 +1383,19 @@ void WiFiManagerHandlers::handleExit(AsyncWebServerRequest *request) {
_wm->DEBUG_WM(WM_DEBUG_VERBOSE, F("<- HTTP Exit"));
#endif
handleRequest(request);
String page = getHTTPHead(F("Exit"), FPSTR(C_exit));
reservePage(page, 1024);
page += F("Exiting");
page += getHTTPEnd();
AsyncWebServerResponse *response = request->beginResponse(200, FPSTR(HTTP_HEAD_CT), page);
auto bundle = std::make_shared<MessageRenderBundle>();
buildMessagePageState(bundle->state, F("Exit"), FPSTR(C_exit), String(F("Exiting")));
registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry);
configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle);
configureDynamicStringDescriptor(bundle->bodyClassDescriptor, bundle->state.bodyClass);
configureDynamicStringDescriptor(bundle->contentDescriptor, bundle->state.content);
bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor);
bundle->registry.registerDynamicTemplate("%BODY_CLASS%", &bundle->bodyClassDescriptor);
bundle->registry.registerDynamicTemplate("%PAGE_CONTENT%", &bundle->contentDescriptor);
AsyncWebServerResponse *response = beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE);
response->addHeader(F("Cache-Control"), F("no-cache, no-store, must-revalidate"));
request->send(response);
@@ -1241,12 +1408,22 @@ void WiFiManagerHandlers::handleReset(AsyncWebServerRequest *request) {
_wm->DEBUG_WM(WM_DEBUG_VERBOSE, F("<- HTTP Reset"));
#endif
handleRequest(request);
String page = getHTTPHead(F("Reset"), FPSTR(C_restart));
reservePage(page, 1024);
page += F("Module will reset in a few seconds.");
page += getHTTPEnd();
auto bundle = std::make_shared<MessageRenderBundle>();
buildMessagePageState(bundle->state,
F("Reset"),
FPSTR(C_restart),
String(F("Module will reset in a few seconds.")));
request->send(200, FPSTR(HTTP_HEAD_CT), page);
registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry);
configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle);
configureDynamicStringDescriptor(bundle->bodyClassDescriptor, bundle->state.bodyClass);
configureDynamicStringDescriptor(bundle->contentDescriptor, bundle->state.content);
bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor);
bundle->registry.registerDynamicTemplate("%BODY_CLASS%", &bundle->bodyClassDescriptor);
bundle->registry.registerDynamicTemplate("%PAGE_CONTENT%", &bundle->contentDescriptor);
request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE));
#ifdef WM_DEBUG_LEVEL
_wm->DEBUG_WM(F("RESETTING ESP"));
@@ -1260,21 +1437,31 @@ void WiFiManagerHandlers::handleErase(AsyncWebServerRequest *request, boolean op
_wm->DEBUG_WM(WM_DEBUG_NOTIFY, F("<- HTTP Erase"));
#endif
handleRequest(request);
String page = getHTTPHead(F("Erase"), FPSTR(C_erase));
reservePage(page, 1024);
bool ret = _wm->erase(opt);
auto bundle = std::make_shared<MessageRenderBundle>();
String content;
reservePage(content, 256);
if(ret) page += F("Module will reset in a few seconds.");
if(ret) content += F("Module will reset in a few seconds.");
else {
page += F("An error occured");
content += F("An error occured");
#ifdef WM_DEBUG_LEVEL
_wm->DEBUG_WM(WM_DEBUG_ERROR, F("[ERROR] WiFi EraseConfig failed"));
#endif
}
page += getHTTPEnd();
request->send(200, FPSTR(HTTP_HEAD_CT), page);
buildMessagePageState(bundle->state, F("Erase"), FPSTR(C_erase), content);
registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry);
configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle);
configureDynamicStringDescriptor(bundle->bodyClassDescriptor, bundle->state.bodyClass);
configureDynamicStringDescriptor(bundle->contentDescriptor, bundle->state.content);
bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor);
bundle->registry.registerDynamicTemplate("%BODY_CLASS%", &bundle->bodyClassDescriptor);
bundle->registry.registerDynamicTemplate("%PAGE_CONTENT%", &bundle->contentDescriptor);
request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE));
if(ret){
_wm->_rebootScheduled = true;
@@ -1292,11 +1479,22 @@ void WiFiManagerHandlers::handleClose(AsyncWebServerRequest *request){
_wm->DEBUG_WM(WM_DEBUG_VERBOSE, F("<- HTTP close"));
#endif
handleRequest(request);
String page = getHTTPHead(F("Close"), FPSTR(C_close));
reservePage(page, 1024);
page += F("You can close the page, portal will continue to run");
page += getHTTPEnd();
request->send(200, FPSTR(HTTP_HEAD_CT), page);
auto bundle = std::make_shared<MessageRenderBundle>();
buildMessagePageState(bundle->state,
F("Close"),
FPSTR(C_close),
String(F("You can close the page, portal will continue to run")));
registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry);
configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle);
configureDynamicStringDescriptor(bundle->bodyClassDescriptor, bundle->state.bodyClass);
configureDynamicStringDescriptor(bundle->contentDescriptor, bundle->state.content);
bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor);
bundle->registry.registerDynamicTemplate("%BODY_CLASS%", &bundle->bodyClassDescriptor);
bundle->registry.registerDynamicTemplate("%PAGE_CONTENT%", &bundle->contentDescriptor);
request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE));
}
void WiFiManagerHandlers::handleNotFound(AsyncWebServerRequest *request) {
@@ -1482,28 +1680,36 @@ void WiFiManagerHandlers::handleUpdateDone(AsyncWebServerRequest *request) {
_wm->DEBUG_WM(WM_DEBUG_VERBOSE, F("<- Handle update done"));
#endif
String page = getHTTPHead(F("options"), FPSTR(C_update));
reservePage(page, 2048);
// Build root main HTML directly without tokens
page += F("<h1>");
page += _wm->_title;
page += F("</h1><h3>");
page += (_wm->configPortalActive ? _wm->_apName : WiFi.localIP().toString());
page += F("</h3>");
auto bundle = std::make_shared<MessageRenderBundle>();
String content;
reservePage(content, 2048);
appendPortalHeader(_wm->_title,
(_wm->configPortalActive ? _wm->_apName : WiFi.localIP().toString()),
content);
if (Update.hasError()) {
page += FPSTR(HTML_UPDATE_FAIL);
content += FPSTR(HTML_UPDATE_FAIL);
#ifdef ESP32
page += "OTA Error: " + (String)Update.errorString();
content += "OTA Error: " + (String)Update.errorString();
#else
page += "OTA Error: " + (String)Update.getError();
content += "OTA Error: " + (String)Update.getError();
#endif
} else {
page += FPSTR(HTML_UPDATE_SUCCESS);
content += FPSTR(HTML_UPDATE_SUCCESS);
}
page += getHTTPEnd();
request->send(200, FPSTR(HTTP_HEAD_CT), page);
buildMessagePageState(bundle->state, F("options"), FPSTR(C_update), content);
registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry);
configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle);
configureDynamicStringDescriptor(bundle->bodyClassDescriptor, bundle->state.bodyClass);
configureDynamicStringDescriptor(bundle->contentDescriptor, bundle->state.content);
bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor);
bundle->registry.registerDynamicTemplate("%BODY_CLASS%", &bundle->bodyClassDescriptor);
bundle->registry.registerDynamicTemplate("%PAGE_CONTENT%", &bundle->contentDescriptor);
request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE));
if (!Update.hasError()) {
delay(1000);
+2 -2
View File
@@ -106,7 +106,7 @@ void WiFiManagerServer::applyTemplateSetupCallback(PlaceholderRegistry& reg) {
}
void WiFiManagerServer::rebuildPlaceholderRegistry(std::function<void(PlaceholderRegistry&)> customizer) {
_tplRegistry = std::unique_ptr<PlaceholderRegistry>(new PlaceholderRegistry(12));
_tplRegistry = std::unique_ptr<PlaceholderRegistry>(new PlaceholderRegistry(WM_TEMPLATE_REGISTRY_CAPACITY));
registerDefaultPlaceholders(*_tplRegistry);
if (customizer) {
customizer(*_tplRegistry);
@@ -118,7 +118,7 @@ void WiFiManagerServer::rebuildPlaceholderRegistry(std::function<void(Placeholde
void WiFiManagerServer::setupTemplateEngine() {
// Initialize shared placeholder registry once
if (!_tplRegistry) {
_tplRegistry = std::unique_ptr<PlaceholderRegistry>(new PlaceholderRegistry(12));
_tplRegistry = std::unique_ptr<PlaceholderRegistry>(new PlaceholderRegistry(WM_TEMPLATE_REGISTRY_CAPACITY));
} else {
_tplRegistry->clear();
}
+1
View File
@@ -32,6 +32,7 @@ TestCase tests[] = {
TEST_ENTRY(test_get_config_portal_ssid),
TEST_ENTRY(test_root_render_menu_state_transitions),
TEST_ENTRY(test_root_render_snapshot_consistency),
TEST_ENTRY(test_root_render_interleaved_context_isolation),
// Non-blocking tests
TEST_ENTRY(test_nonblocking_process),
+1
View File
@@ -42,6 +42,7 @@ void test_config_portal_already_active();
void test_get_config_portal_ssid();
void test_root_render_menu_state_transitions();
void test_root_render_snapshot_consistency();
void test_root_render_interleaved_context_isolation();
// Non-blocking tests
void test_nonblocking_process();
@@ -2,6 +2,59 @@
#include <Arduino.h>
#include <WiFiManager.h>
#include <WiFiManagerHandlers.h>
#include <TemplateEngine.h>
#include "templates/RootSelector.h"
namespace {
const char kEmptyTemplateChunk[] PROGMEM = "";
const char kTestTitle[] PROGMEM = "WiFiManager";
const char kTestSubtitle[] PROGMEM = "Root Test";
const char* dynamicStringGetter(void* userData) {
const auto* value = static_cast<const String*>(userData);
return value ? value->c_str() : "";
}
size_t dynamicStringLengthGetter(const char* data, void* /*userData*/) {
return data ? strlen(data) : 0;
}
void configureDescriptor(DynamicTemplateDescriptor& descriptor, String& value) {
descriptor.getter = &dynamicStringGetter;
descriptor.getLength = &dynamicStringLengthGetter;
descriptor.userData = &value;
}
void renderContextsInterleaved(TemplateContext& first, String& firstOutput,
TemplateContext& second, String& secondOutput) {
uint8_t firstBuffer[31];
uint8_t secondBuffer[31];
bool firstDone = false;
bool secondDone = false;
while (!firstDone || !secondDone) {
if (!firstDone) {
size_t written = TemplateRenderer::renderNextChunk(first, firstBuffer, sizeof(firstBuffer));
if (written == 0) {
firstDone = true;
} else {
firstOutput.concat(reinterpret_cast<const char*>(firstBuffer), written);
}
}
if (!secondDone) {
size_t written = TemplateRenderer::renderNextChunk(second, secondBuffer, sizeof(secondBuffer));
if (written == 0) {
secondDone = true;
} else {
secondOutput.concat(reinterpret_cast<const char*>(secondBuffer), written);
}
}
}
}
} // namespace
void test_root_render_menu_state_transitions() {
Serial.println("[TEST] Testing root menu state transitions...");
@@ -62,3 +115,68 @@ void test_root_render_snapshot_consistency() {
Serial.println("[TEST] Root render snapshot consistency test completed successfully");
}
void test_root_render_interleaved_context_isolation() {
Serial.println("[TEST] Testing root render interleaved context isolation...");
PlaceholderRegistry registryA(8);
PlaceholderRegistry registryB(8);
registryA.registerProgmemData("%SCRIPTS%", kEmptyTemplateChunk);
registryA.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
registryA.registerProgmemData("%PAGE_TITLE%", kTestTitle);
registryA.registerProgmemData("%SUBTITLE%", kTestSubtitle);
registryB.registerProgmemData("%SCRIPTS%", kEmptyTemplateChunk);
registryB.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
registryB.registerProgmemData("%PAGE_TITLE%", kTestTitle);
registryB.registerProgmemData("%SUBTITLE%", kTestSubtitle);
String menuA = F("<div>Menu A</div>");
String statusA = F("<div>Status A</div>");
String menuB = F("<div>Menu B</div>");
String statusB = F("<div>Status B</div>");
DynamicTemplateDescriptor menuDescriptorA{};
DynamicTemplateDescriptor statusDescriptorA{};
DynamicTemplateDescriptor menuDescriptorB{};
DynamicTemplateDescriptor statusDescriptorB{};
configureDescriptor(menuDescriptorA, menuA);
configureDescriptor(statusDescriptorA, statusA);
configureDescriptor(menuDescriptorB, menuB);
configureDescriptor(statusDescriptorB, statusB);
TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicTemplate("%MENU%", &menuDescriptorA),
"Registry A menu placeholder should register");
TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicTemplate("%STATUS%", &statusDescriptorA),
"Registry A status placeholder should register");
TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicTemplate("%MENU%", &menuDescriptorB),
"Registry B menu placeholder should register");
TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicTemplate("%STATUS%", &statusDescriptorB),
"Registry B status placeholder should register");
TemplateContext contextA;
TemplateContext contextB;
contextA.setRegistry(&registryA);
contextB.setRegistry(&registryB);
TemplateRenderer::initializeContext(contextA, WM_ROOT_TEMPLATE);
TemplateRenderer::initializeContext(contextB, WM_ROOT_TEMPLATE);
String outputA;
String outputB;
renderContextsInterleaved(contextA, outputA, contextB, outputB);
TEST_ASSERT_NOT_EQUAL(-1, outputA.indexOf("Menu A"));
TEST_ASSERT_NOT_EQUAL(-1, outputA.indexOf("Status A"));
TEST_ASSERT_EQUAL(-1, outputA.indexOf("Menu B"));
TEST_ASSERT_EQUAL(-1, outputA.indexOf("Status B"));
TEST_ASSERT_NOT_EQUAL(-1, outputB.indexOf("Menu B"));
TEST_ASSERT_NOT_EQUAL(-1, outputB.indexOf("Status B"));
TEST_ASSERT_EQUAL(-1, outputB.indexOf("Menu A"));
TEST_ASSERT_EQUAL(-1, outputB.indexOf("Status A"));
Serial.println("[TEST] Root render interleaved context isolation test completed successfully");
}