diff --git a/lib/WiFiManager/include/WiFiManagerServer.h b/lib/WiFiManager/include/WiFiManagerServer.h index 0698a33..6cba122 100644 --- a/lib/WiFiManager/include/WiFiManagerServer.h +++ b/lib/WiFiManager/include/WiFiManagerServer.h @@ -22,6 +22,10 @@ // DFTE (template engine) #include +#ifndef WM_TEMPLATE_REGISTRY_CAPACITY +#define WM_TEMPLATE_REGISTRY_CAPACITY 24 +#endif + // Forward declarations class WiFiManager; diff --git a/lib/WiFiManager/include/templates/Info.h b/lib/WiFiManager/include/templates/Info.h new file mode 100644 index 0000000..e89c487 --- /dev/null +++ b/lib/WiFiManager/include/templates/Info.h @@ -0,0 +1,32 @@ +/** + * Info.h + * Streamed info page template for WiFiManager. + */ + +#ifndef _WM_INFO_TEMPLATE_H_ +#define _WM_INFO_TEMPLATE_H_ + +#include + +const char WM_INFO_TEMPLATE[] PROGMEM = +"" +"" +"" +"" +"" +"" +"%DOC_TITLE%" +"%SCRIPTS%" +"%STYLES%" +"" +"" +"
" +"%INFO_STATUS%" +"%INFO_DEVICE_SECTION%" +"%INFO_WIFI_SECTION%" +"%INFO_ABOUT_SECTION%" +"
" +"" +""; + +#endif // _WM_INFO_TEMPLATE_H_ diff --git a/lib/WiFiManager/include/templates/PageShell.h b/lib/WiFiManager/include/templates/PageShell.h new file mode 100644 index 0000000..f714359 --- /dev/null +++ b/lib/WiFiManager/include/templates/PageShell.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 + +const char WM_PAGE_SHELL_TEMPLATE[] PROGMEM = +"" +"" +"" +"" +"" +"" +"%DOC_TITLE%" +"%SCRIPTS%" +"%STYLES%" +"" +"" +"
" +"%PAGE_CONTENT%" +"
" +"" +""; + +#endif // _WM_PAGE_SHELL_TEMPLATE_H_ diff --git a/lib/WiFiManager/include/templates/WiFi.h b/lib/WiFiManager/include/templates/WiFi.h new file mode 100644 index 0000000..d4f2b74 --- /dev/null +++ b/lib/WiFiManager/include/templates/WiFi.h @@ -0,0 +1,32 @@ +/** + * WiFi.h + * Streamed WiFi configuration page template for WiFiManager. + */ + +#ifndef _WM_WIFI_TEMPLATE_H_ +#define _WM_WIFI_TEMPLATE_H_ + +#include + +const char WM_WIFI_TEMPLATE[] PROGMEM = +"" +"" +"" +"" +"" +"" +"%DOC_TITLE%" +"%SCRIPTS%" +"%STYLES%" +"" +"" +"
" +"%WIFI_SCAN_RESULTS%" +"%WIFI_FORM_SECTION%" +"%WIFI_BACK_SECTION%" +"%WIFI_STATUS%" +"
" +"" +""; + +#endif // _WM_WIFI_TEMPLATE_H_ diff --git a/lib/WiFiManager/src/WiFiManagerHandlers.cpp b/lib/WiFiManager/src/WiFiManagerHandlers.cpp index d93235b..9eec362 100644 --- a/lib/WiFiManager/src/WiFiManagerHandlers.cpp +++ b/lib/WiFiManager/src/WiFiManagerHandlers.cpp @@ -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 #include @@ -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(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 +AsyncWebServerResponse* beginTemplateResponse(AsyncWebServerRequest* request, + const std::shared_ptr& 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(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(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("

"); + content += title; + content += F("

"); + content += subtitle; + content += F("

"); +} + +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(""); + + appendInfoEntries(handlers, state.wifiSection, wifiIds, sizeof(wifiIds) / sizeof(wifiIds[0])); + state.wifiSection += F(""); + + state.aboutSection += F("

About


"); + state.aboutSection += handlers->getInfoData("aboutver"); + state.aboutSection += handlers->getInfoData("aboutarduinover"); + state.aboutSection += handlers->getInfoData("aboutidfver"); + state.aboutSection += handlers->getInfoData("aboutdate"); + state.aboutSection += F("
"); + + 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("
"); + state.scanResults += handlers->getScanItemOut(); + state.scanResults += F("
"); + } + + state.formSection += F("
"); + + state.formSection += F("

"); + + 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("
"); + + 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(); if (scan) { bool forceRefresh = false; if (request->hasParam("refresh")) { forceRefresh = true; } - - page += F("
"); - page += getScanItemOut(); - page += F("
"); - + 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(""); - + String ssidPlaceholder = _wm->WiFi_SSID(); String passwordPlaceholder = ""; - if(_wm->_showPassword){ passwordPlaceholder = _wm->WiFi_psk(); } else if(_wm->WiFi_psk() != ""){ passwordPlaceholder = F("********"); } - - page += F("

"); - 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("
"); - 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(); + 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"); - - page += F("

About


"); - page += getInfoData("aboutver"); - page += getInfoData("aboutarduinover"); - page += getInfoData("aboutidfver"); - page += getInfoData("aboutdate"); - page += F("
"); - - 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(); + 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(); + 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(); + 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(); + 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("

"); - page += _wm->_title; - page += F("

"); - page += (_wm->configPortalActive ? _wm->_apName : WiFi.localIP().toString()); - page += F("

"); + auto bundle = std::make_shared(); + 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); diff --git a/lib/WiFiManager/src/WiFiManagerServer.cpp b/lib/WiFiManager/src/WiFiManagerServer.cpp index 9936bdc..fd17c87 100644 --- a/lib/WiFiManager/src/WiFiManagerServer.cpp +++ b/lib/WiFiManager/src/WiFiManagerServer.cpp @@ -106,7 +106,7 @@ void WiFiManagerServer::applyTemplateSetupCallback(PlaceholderRegistry& reg) { } void WiFiManagerServer::rebuildPlaceholderRegistry(std::function customizer) { - _tplRegistry = std::unique_ptr(new PlaceholderRegistry(12)); + _tplRegistry = std::unique_ptr(new PlaceholderRegistry(WM_TEMPLATE_REGISTRY_CAPACITY)); registerDefaultPlaceholders(*_tplRegistry); if (customizer) { customizer(*_tplRegistry); @@ -118,7 +118,7 @@ void WiFiManagerServer::rebuildPlaceholderRegistry(std::function(new PlaceholderRegistry(12)); + _tplRegistry = std::unique_ptr(new PlaceholderRegistry(WM_TEMPLATE_REGISTRY_CAPACITY)); } else { _tplRegistry->clear(); } diff --git a/test/test_wifimanager/test_main.cpp b/test/test_wifimanager/test_main.cpp index 7a30808..4e0c17c 100644 --- a/test/test_wifimanager/test_main.cpp +++ b/test/test_wifimanager/test_main.cpp @@ -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), diff --git a/test/test_wifimanager/test_main.h b/test/test_wifimanager/test_main.h index 7067013..4e03c83 100644 --- a/test/test_wifimanager/test_main.h +++ b/test/test_wifimanager/test_main.h @@ -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(); diff --git a/test/test_wifimanager/tests/test_root_render_lifecycle.cpp b/test/test_wifimanager/tests/test_root_render_lifecycle.cpp index 4654610..8c0df02 100644 --- a/test/test_wifimanager/tests/test_root_render_lifecycle.cpp +++ b/test/test_wifimanager/tests/test_root_render_lifecycle.cpp @@ -2,6 +2,59 @@ #include #include #include +#include +#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(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(firstBuffer), written); + } + } + + if (!secondDone) { + size_t written = TemplateRenderer::renderNextChunk(second, secondBuffer, sizeof(secondBuffer)); + if (written == 0) { + secondDone = true; + } else { + secondOutput.concat(reinterpret_cast(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("
Menu A
"); + String statusA = F("
Status A
"); + String menuB = F("
Menu B
"); + String statusB = F("
Status B
"); + + 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(®istryA); + contextB.setRegistry(®istryB); + + 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"); +}