diff --git a/README.md b/README.md index 9d72631..46c914e 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,49 @@ # WiFiManager -Espressif ESPx WiFi connection manager with a fallback web configuration portal. +This repository is a **breaking fork** of upstream [`tzapu/WiFiManager`](https://github.com/tzapu/WiFiManager). +It is not a drop-in replacement for upstream behavior, APIs, templates, or portal customization patterns. -## Fork notice +If you are evaluating this fork, assume that core web-portal architecture has changed and review the code before adopting it in an existing upstream-based project. -**This is a fork with breaking changes.** The web portal is built with **DFTE** ([Device Framework Template Engine](https://github.com/alexhopeoconnor/DFTE)): composable templates and chunked HTML responses instead of upstream’s monolithic string assembly. +**This repository:** [alexhopeoconnor/WiFiManager](https://github.com/alexhopeoconnor/WiFiManager) +**Upstream:** [tzapu/WiFiManager](https://github.com/tzapu/WiFiManager) -**This repository:** [alexhopeoconnor/WiFiManager](https://github.com/alexhopeoconnor/WiFiManager) +## Breaking Changes -**Upstream (original):** [tzapu/WiFiManager](https://github.com/tzapu/WiFiManager) +This fork intentionally modernizes and restructures the configuration portal. +Notable differences from upstream include: + +- The portal now serves a **single HTML shell** from `GET /`. +- Client navigation is handled as a **SPA with hash routing**. +- Interactive behavior is exposed through **JSON APIs under `/api/...`** plus firmware upload at **`POST /u`**. +- Legacy multi-page portal routes and legacy root-template override paths have been removed. +- The portal rendering pipeline is built around **DFTE** instead of upstream's monolithic HTML string assembly. +- JSON endpoints are expected to be **data-first**, not derived from generated HTML fragments. + +## Current Improvements And Modernization + +This fork currently includes the following architectural improvements: + +- A **single-shell portal architecture** with embedded bootstrap JSON and embedded application JS. +- A **SPA-based configuration UI** for WiFi setup, parameters, info views, device actions, and OTA flow. +- A **clean API surface** for WiFi scanning, WiFi save, parameters, info, status, restart, erase, portal exit, and captive-portal close behavior. +- **Captive portal redirect handling** retained while removing duplicate legacy UI architecture. +- **Data-first JSON generation** for portal APIs, including info/device/about data, instead of HTML-to-JSON parsing. +- **Capability-driven UI flags** in bootstrap/API payloads so features like info, update, erase, and action visibility can be controlled by backend state. +- **SPA-native feedback UX** using in-DOM dialog/toast behavior rather than page-based action flows. +- A **shell-scoped placeholder model** centered on `%PAGE_TITLE%`, `%STYLES%`, `%SCRIPTS%`, `%BOOTSTRAP_JSON%`, and `%PORTAL_APP_JS%`. +- A clearer separation between: + - shell rendering + - API responses + - captive portal behavior + - OTA handling +- Updated tests focused on the **shell contract**, **bootstrap payloads**, and **API JSON shapes** rather than removed legacy portal pages. ## Dependencies -**DFTE** is required and is declared in `library.json` (PlatformIO / `lib_deps` pick it up automatically). For **this repo’s** `platformio.ini` tests, a **sibling checkout** is expected: +This fork depends on **DFTE** ([Device Framework Template Engine](https://github.com/alexhopeoconnor/DFTE)) and **ESP32Async/ESPAsyncWebServer**. + +For this repo's `platformio.ini` test setup, DFTE is expected as a sibling checkout: ```ini lib_deps = @@ -20,11 +51,7 @@ lib_deps = symlink://../DFTE ``` -Clone `DFTE` next to `WiFiManager` (same parent directory), or point `lib_deps` at the Git URL used in `library.json`. - -Other pinned dependency: **ESP32Async/ESPAsyncWebServer** (see `library.json`). - -## Installation (PlatformIO) +## Installation ```ini [env:your_environment] @@ -42,46 +69,9 @@ lib_deps = file:///path/to/WiFiManager ``` -## HTML customization (DFTE) +## AI Assistance Notice -- **Page shell and routes** stream through DFTE (`TemplateRenderer` + chunked responses). Per-request bundles use their own `PlaceholderRegistry` for dynamic sections; the server also keeps a **shared** registry for defaults (`%STYLES%`, `%SCRIPTS%`, `%PAGE_TITLE%`, `%SUBTITLE%`, `%MENU%`, `%STATUS%`, …). -- **`WM_TEMPLATE_REGISTRY_CAPACITY`** (default in `WiFiManagerServer.h`) sizes the shared registry; raise it if you register many custom placeholders. -- **Override the root document template:** define **`WM_CUSTOM_ROOT_TEMPLATE_HEADER`** to a **quoted** include path (see `lib/WiFiManager/include/templates/RootSelector.h`). By default it includes `Root.h` (`WM_ROOT_TEMPLATE`). -- **Advanced / tests:** `WiFiManagerServer::instance()` exposes the same hooks after the server exists; prefer the `WiFiManager` methods below for typical apps. - -### `WiFiManager` API - -```cpp -wm.registerTemplateSetupCallback([](PlaceholderRegistry& reg) { - // reg.registerProgmemData(...), registerRamData, registerDynamicTemplate, etc. -}); -wm.rebuildPlaceholderRegistry(); // optional: after changing callback while portal runs -``` - -`registerTemplateSetupCallback` may be called **before** `startConfigPortal` / `startWebPortal` (stored until the HTTP server is created). If the portal is **already** running, the callback is applied and the shared registry is **rebuilt** so changes take effect. - -`rebuildPlaceholderRegistry(customizer)` forwards to the server when present: rebuilds defaults, then runs your optional one-shot customizer, or the registered setup callback if `customizer` is null. - -### Content built as `String` / PROGMEM fragments - -Some small HTML snippets (status messages, OTA blurb, help table, etc.) are still assembled in RAM or pulled from `HTML.h` and then injected into templates. That is **intentional**: not every line of markup needs its own placeholder or registry churn. Customize those via DFTE where it pays off (shell, branding, layout); keep trivial blobs as strings when appropriate. - -## Debug: WiFiManager logging and DFTE - -### How DFTE logging is designed - -DFTE keeps a **single global pointer** `deviceFrameworkTemplateEngineLogger` (see DFTE’s `DeviceFrameworkTemplateEngineDebug.h`). By default it is **`nullptr`**: every `DFTE_LOG_*` macro expands to a cheap null check and does nothing—no virtual calls, no strings, no flash cost. A host (sketch or library) implements `DeviceFrameworkTemplateEngineLogger` (virtual `error` / `warn` / `info` / `debug`) and calls `deviceFrameworkTemplateEngineEnableLogging(&myLogger)` once. Until then, DFTE is silent on the happy path; internal code mostly uses `DFTE_LOG_ERROR` and `DFTE_LOG_WARN` when something is wrong. - -### WiFiManager today - -- **`WM_LOG_LEVEL`**: compile-time default is **`0`** (silent). Set **`1`–`5`** for `Error` … `Trace` (see `WiFiManagerLogLevel` in `WiFiManagerLogLevel.h`). Example: `-DWM_LOG_LEVEL=5` in `platformio.ini` for full verbosity. Strip all log **call sites** from the binary with **`WM_NO_LOG`** (or legacy **`WM_NODEBUG`**). At runtime, **`setLogEnabled`**, **`setLogOutput(enabled, maxLevel)`**, **`_logPrefix`**, and **`_logPort`** gate output; use **`WM_DEBUG_PORT`** to redirect the default `Print` target. **`WiFiManagerLogSink`** receives structured **`WiFiManagerLogMessage`** (level, subsystem, full line). -- **`WM_DFTE_LOGGING`** (build flag, **off by default**): when defined, `WiFiManagerServer::setupTemplateEngine()` registers **`WiFiManagerDfteLogger`**, which forwards DFTE into **`WiFiManager::log(..., "WM/DFTE", ...)`**. It uses **`deviceFrameworkTemplateEngineEnableLogging(..., this)`** so **`disableLoggingForOwner(this)`** on shutdown does not clobber another owner. If DFTE logging was already enabled, WiFiManager does not replace it. - -Use **`pio test -e esp8266_dfte_log --without-uploading --without-testing`** in this repo to compile the test firmware with **`WM_DFTE_LOGGING`** enabled. - -### Design note - -DFTE uses a small optional logger pointer; WiFiManager uses **`#ifndef WM_NO_LOG`** around log call sites so release builds can omit strings entirely. **`WiFiManagerLogSink`** lets hosts (e.g. **DeviceFramework**) map levels without parsing ad hoc prefixes. +Parts of this codebase have been developed and refactored with the aid of AI coding agents under human direction and review. ## License diff --git a/lib/WiFiManager/include/WiFiManager.h b/lib/WiFiManager/include/WiFiManager.h index 7f623e6..f73474d 100644 --- a/lib/WiFiManager/include/WiFiManager.h +++ b/lib/WiFiManager/include/WiFiManager.h @@ -6,7 +6,7 @@ * * @author Creator tzapu * @author tablatronix - * @author Alex Hope-O'Connor + * @author alexhopeoconnor * @license MIT */ @@ -139,8 +139,6 @@ #include #include #include -#include - // Include utility functions #include "WiFiManagerUtils.h" @@ -437,6 +435,9 @@ class WiFiManager // show OTA upload button on info page void setShowInfoUpdate(boolean enabled); + // show Info nav / info view (portal SPA); default true + void setShowInfo(boolean enabled); + // set ap channel void setWiFiAPChannel(int32_t channel); @@ -449,13 +450,6 @@ class WiFiManager // set the webapp title, default WiFiManager void setTitle(String title); - // DFTE (portal HTML): register extra placeholders or override defaults (%STYLES%, %PAGE_TITLE%, …). - // Call before startConfigPortal / startWebPortal to have it applied when the server starts; if the - // portal is already running, the shared registry is rebuilt immediately. - void registerTemplateSetupCallback(std::function cb); - // Rebuild the server’s shared PlaceholderRegistry (defaults + registerTemplateSetupCallback, or customizer if passed). - void rebuildPlaceholderRegistry(std::function customizer = nullptr); - // add params to its own menu page and remove from wifi, NOT TO BE COMBINED WITH setMenu! void setParamsPage(bool enable); @@ -589,7 +583,6 @@ class WiFiManager protected: // vars std::unique_ptr _serverManager; - std::function _templateSetupCallback; // ip configs @todo struct ? IPAddress _ap_static_ip; @@ -684,6 +677,7 @@ class WiFiManager boolean _webClientCheck = true; // keep cp alive if web have client boolean _scanDispOptions = false; // show percentage in scans not icons boolean _paramsInWifi = true; // show custom parameters on wifi page + boolean _showInfo = true; // show Info in portal SPA boolean _showInfoErase = true; // info page erase button boolean _showInfoUpdate = true; // info page update button boolean _showBack = false; // show back button diff --git a/lib/WiFiManager/include/WiFiManagerDfteLogger.h b/lib/WiFiManager/include/WiFiManagerDfteLogger.h index e3ac5a1..904f1d8 100644 --- a/lib/WiFiManager/include/WiFiManagerDfteLogger.h +++ b/lib/WiFiManager/include/WiFiManagerDfteLogger.h @@ -1,6 +1,9 @@ /** * WiFiManagerDfteLogger.h * + * @author alexhopeoconnor + * @license MIT + * * Optional bridge from DFTE's logging hooks to WiFiManager::log. * Enabled only when the sketch / library build defines WM_DFTE_LOGGING. */ diff --git a/lib/WiFiManager/include/WiFiManagerHandlers.h b/lib/WiFiManager/include/WiFiManagerHandlers.h index 4d8dfc9..1677a3c 100644 --- a/lib/WiFiManager/include/WiFiManagerHandlers.h +++ b/lib/WiFiManager/include/WiFiManagerHandlers.h @@ -1,11 +1,9 @@ /** * WiFiManagerHandlers.h - * - * HTTP request handlers and rendering logic for WiFiManager - * Handles all web portal page generation and request processing - * - * @author tablatronix - * @author Alex Hope-O'Connor + * + * HTTP request handlers for WiFiManager: single HTML shell + JSON APIs. + * + * @author alexhopeoconnor * @license MIT */ @@ -16,20 +14,7 @@ #include #include -#include "WiFiManager.h" // Need full definition for WiFiManagerRequestArgs - -// ----------------------------------------------------------------------------------------------- -// CSS CLASSES (for body class attribute) - -const char C_root[] PROGMEM = "home"; -const char C_wifi[] PROGMEM = "wifi"; -const char C_info[] PROGMEM = "info"; -const char C_param[] PROGMEM = "param"; -const char C_close[] PROGMEM = "close"; -const char C_restart[] PROGMEM = "restart"; -const char C_exit[] PROGMEM = "exit"; -const char C_erase[] PROGMEM = "erase"; -const char C_update[] PROGMEM = "update"; +#include "WiFiManager.h" // ----------------------------------------------------------------------------------------------- // FORM FIELD NAMES (for IP configuration forms) @@ -50,27 +35,13 @@ const char HTTP_HEAD_CORS_ALLOW_ALL[] PROGMEM = "*"; class WiFiManagerHandlers { public: WiFiManagerHandlers(WiFiManager* wm); - - // HTTP Request Handlers + void handleRoot(AsyncWebServerRequest *request); - void handleWifi(AsyncWebServerRequest *request, boolean scan); - void handleWifiSave(AsyncWebServerRequest *request); - void handleParam(AsyncWebServerRequest *request); - void handleParamSave(AsyncWebServerRequest *request); - void handleInfo(AsyncWebServerRequest *request); - void handleReset(AsyncWebServerRequest *request); - void handleExit(AsyncWebServerRequest *request); - void handleClose(AsyncWebServerRequest *request); - void handleErase(AsyncWebServerRequest *request, boolean opt); void handleNotFound(AsyncWebServerRequest *request); void handleRequest(AsyncWebServerRequest *request); - void handleWiFiScanRequest(AsyncWebServerRequest *request); - void handleWiFiScanStatus(AsyncWebServerRequest *request); - void handleUpdate(AsyncWebServerRequest *request); void handleUpdating(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); void handleUpdateDone(AsyncWebServerRequest *request); - // JSON / SPA API (portal is a single-page app; only GET / serves HTML) void handleApiBootstrap(AsyncWebServerRequest *request); void handleApiWifiScanStatus(AsyncWebServerRequest *request); void handleApiWifiScan(AsyncWebServerRequest *request); @@ -81,39 +52,58 @@ class WiFiManagerHandlers { void handleApiInfo(AsyncWebServerRequest *request); void handleApiStatus(AsyncWebServerRequest *request); void handleApiDeviceRestart(AsyncWebServerRequest *request); - void handleApiDeviceErase(AsyncWebServerRequest *request); + void handleApiDeviceErase(AsyncWebServerRequest *request, boolean optionalErase = false); void handleApiPortalClose(AsyncWebServerRequest *request); void handleApiPortalExit(AsyncWebServerRequest *request); - - // Captive Portal + boolean captivePortal(AsyncWebServerRequest *request); void stopCaptivePortal(); - - // Rendering Methods - String getMenuOut(); - String getMenuOut(String* outOpt); - String getScanItemOut(); - String getParamOut(); - String getStaticOut(); - String getIpForm(String id, String title, String value); - String getInfoData(String id); - void reportStatus(String &page); - - // Internal helper (needed by handlers) + + /** True when the HTTP Host header does not match the portal’s canonical host:port (captive redirect needed). */ + static bool shouldRedirectCaptiveForHost(const String& requestHost, const String& serverLocWithPort); + void doParamSave(WiFiManager::WiFiManagerRequestArgs requestArgs); - + + /** Portal bootstrap JSON (same payload as GET /api/bootstrap). Public for tests and host integration. */ + String buildPortalBootstrapJson(); + /** Same JSON body as GET /api/wifi/meta (tests + embedding). */ + String buildApiWifiMetaJson(); + /** Same JSON body as GET /api/info (tests + embedding). */ + String buildApiInfoJson(); + /** Same JSON body as GET /api/params (tests + embedding). */ + String buildApiParamsGetJson(); + /** Same JSON body as GET /api/status (tests + embedding). */ + String buildApiStatusJson(); + + /** Fixed JSON bodies for POST action endpoints (single source for handlers + tests). */ + static String jsonApiDeviceRestartScheduled(); + static String jsonApiParamsSaveOk(); + static String jsonApiPortalCloseOk(); + static String jsonApiPortalExitOk(); + static String jsonApiPortalExitForbidden(); + static String jsonApiOtaUpdateSuccess(); + static String jsonApiEraseResponse(boolean success); + private: WiFiManager* _wm; void collectVisibleScanResults(std::vector& networks); void appendVisibleScanResultsJson(String& json, const std::vector& networks); - String buildPortalBootstrapJson(); void applyWifiAndParamsFromRequest(AsyncWebServerRequest *request); void buildPlainStatusSummary(String& out); + void appendPortalJsonStaticFields(String& json, bool& first); + void appendPortalJsonCustomParams(String& json, bool& first); + + void appendJsonKvItem(String& json, bool& first, const char* key, const String& label, const String& value); + void appendOneInfoItemForId(String& json, bool& first, const char* id); + void appendInfoSectionFromIds(String& json, const char* const* ids, size_t count, bool& first); + + void appendPortalUiFeatureFlagsJson(String& json); + void appendApiInfoActionsJson(String& json); + static void sendApiJson(AsyncWebServerRequest *request, int code, const String& json); }; #endif // defined(ESP8266) || defined(ESP32) #endif // WiFiManagerHandlers_h - diff --git a/lib/WiFiManager/include/WiFiManagerLogLevel.h b/lib/WiFiManager/include/WiFiManagerLogLevel.h index f755f13..5dd15b7 100644 --- a/lib/WiFiManager/include/WiFiManagerLogLevel.h +++ b/lib/WiFiManager/include/WiFiManagerLogLevel.h @@ -1,6 +1,9 @@ /** * WiFiManagerLogLevel.h * + * @author alexhopeoconnor + * @license MIT + * * Compile-time and runtime log levels for WiFiManager. Higher numeric values * mean more verbose output. Matches the old 0..5 tier model for build flags. */ diff --git a/lib/WiFiManager/include/WiFiManagerLogSink.h b/lib/WiFiManager/include/WiFiManagerLogSink.h index 7374e39..42c82dd 100644 --- a/lib/WiFiManager/include/WiFiManagerLogSink.h +++ b/lib/WiFiManager/include/WiFiManagerLogSink.h @@ -1,6 +1,9 @@ /** * WiFiManagerLogSink.h * + * @author alexhopeoconnor + * @license MIT + * * Optional host-provided sink for WiFiManager log output. When set, logs are * delivered here instead of the configured Print stream (usually Serial). */ diff --git a/lib/WiFiManager/include/WiFiManagerLogTemplates.h b/lib/WiFiManager/include/WiFiManagerLogTemplates.h index 14e5e11..964988f 100644 --- a/lib/WiFiManager/include/WiFiManagerLogTemplates.h +++ b/lib/WiFiManager/include/WiFiManagerLogTemplates.h @@ -1,6 +1,9 @@ /** * WiFiManagerLogTemplates.h * + * @author alexhopeoconnor + * @license MIT + * * Template implementations for WiFiManager::log(...). Included at the end of WiFiManager.h. */ diff --git a/lib/WiFiManager/include/WiFiManagerParameter.h b/lib/WiFiManager/include/WiFiManagerParameter.h index ba6fcbd..a49862a 100644 --- a/lib/WiFiManager/include/WiFiManagerParameter.h +++ b/lib/WiFiManager/include/WiFiManagerParameter.h @@ -1,3 +1,10 @@ +/** + * WiFiManagerParameter.h + * + * @author alexhopeoconnor + * @license MIT + */ + #ifndef WiFiManagerParameter_h #define WiFiManagerParameter_h diff --git a/lib/WiFiManager/include/WiFiManagerServer.h b/lib/WiFiManager/include/WiFiManagerServer.h index 066896c..698c00d 100644 --- a/lib/WiFiManager/include/WiFiManagerServer.h +++ b/lib/WiFiManager/include/WiFiManagerServer.h @@ -1,12 +1,29 @@ /** * WiFiManagerServer.h - * - * Server lifecycle management for WiFiManager - * Handles AsyncWebServer creation, route registration, and shutdown - * - * @author tablatronix - * @author Alex Hope-O'Connor + * + * @author alexhopeoconnor * @license MIT + * + * HTTP server for the config portal: single HTML shell (GET /), JSON under /api/..., OTA POST /u, + * captive-portal redirects, and 404 handling. No legacy page routes. + * + * Supported surface (authoritative; registerRoutes mirrors this): + * GET / + * GET /api/bootstrap + * GET /api/wifi/scan-status + * POST /api/wifi/scan + * GET /api/wifi/meta + * POST /api/wifi/save + * GET /api/params + * POST /api/params/save + * GET /api/info + * GET /api/status + * POST /api/device/restart + * POST /api/device/erase + * POST /api/portal/close + * POST /api/portal/exit + * POST /u (multipart firmware upload; JSON completion response) + * notFound -> captive redirect or 404 */ #ifndef WiFiManagerServer_h @@ -19,17 +36,13 @@ #include #include -// DFTE (template engine) #include #ifndef WM_TEMPLATE_REGISTRY_CAPACITY -#define WM_TEMPLATE_REGISTRY_CAPACITY 24 +#define WM_TEMPLATE_REGISTRY_CAPACITY 16 #endif -// Forward declarations class WiFiManager; - -// Forward declaration - full definition needed in .cpp file class WiFiManagerHandlers; #ifdef WM_DFTE_LOGGING @@ -37,23 +50,9 @@ class WiFiManagerDfteLogger; #endif // ----------------------------------------------------------------------------------------------- -// HTTP ROUTES +// HTTP routes (portal API only; no legacy /wifi, /info, … page URLs) const char R_root[] PROGMEM = "/"; -const char R_wifi[] PROGMEM = "/wifi"; -const char R_wifinoscan[] PROGMEM = "/0wifi"; -const char R_wifisave[] PROGMEM = "/wifisave"; -const char R_info[] PROGMEM = "/info"; -const char R_param[] PROGMEM = "/param"; -const char R_paramsave[] PROGMEM = "/paramsave"; -const char R_restart[] PROGMEM = "/restart"; -const char R_exit[] PROGMEM = "/exit"; -const char R_close[] PROGMEM = "/close"; -const char R_erase[] PROGMEM = "/erase"; -const char R_scanstatus[] PROGMEM = "/wifistatus"; -const char R_scan[] PROGMEM = "/wifi/scan"; -const char R_update[] PROGMEM = "/update"; -const char R_updatedone[] PROGMEM = "/u"; const char R_api_bootstrap[] PROGMEM = "/api/bootstrap"; const char R_api_wifi_scan_status[] PROGMEM = "/api/wifi/scan-status"; @@ -69,86 +68,50 @@ const char R_api_device_erase[] PROGMEM = "/api/device/erase"; const char R_api_portal_close[] PROGMEM = "/api/portal/close"; const char R_api_portal_exit[] PROGMEM = "/api/portal/exit"; +const char R_updatedone[] PROGMEM = "/u"; + class WiFiManagerServer { public: - // Singleton access static WiFiManagerServer* instance(); - + WiFiManagerServer(WiFiManager* wm); ~WiFiManagerServer(); - - // Template engine setup and access + void setupTemplateEngine(); PlaceholderRegistry* getPlaceholderRegistry() { return _tplRegistry.get(); } - - // Static zero-arg getters for DFTE RAM placeholders + static const char* tplGetPageTitle(); - static const char* tplGetSubtitle(); - static const char* tplGetMenu(); - static const char* tplGetStatus(); - - // Template customization API - // Consumers can register a callback to customize placeholders - void registerTemplateSetupCallback(std::function cb) { _tplSetupCallback = cb; } - - // Convenience helpers to register our defaults (granular + all) + void registerDefaultStyles(PlaceholderRegistry& reg); void registerDefaultScripts(PlaceholderRegistry& reg); void registerDefaultPageTitle(PlaceholderRegistry& reg); - void registerDefaultSubtitle(PlaceholderRegistry& reg); - void registerDefaultMenu(PlaceholderRegistry& reg); - void registerDefaultStatus(PlaceholderRegistry& reg); + /** Shell placeholders only: %STYLES%, %SCRIPTS%, %PAGE_TITLE%. */ void registerDefaultPlaceholders(PlaceholderRegistry& reg); - void applyTemplateSetupCallback(PlaceholderRegistry& reg); - - // Rebuild registry with defaults and optional customizer - void rebuildPlaceholderRegistry(std::function customizer = nullptr); - - // Create server instance + void createServer(uint16_t port); - - // Register all routes (takes handler callbacks) void registerRoutes(); - - // Setup DNS server for captive portal void setupDNSD(); - - // Process DNS requests (call periodically) void processDNS(); - - // Shutdown and cleanup server and DNS void shutdownServer(); - - // Get server instance AsyncWebServer* getServer() { return server.get(); } - - // Get DNS server instance (for testing) DNSServer* getDNSServer() { return dnsServer.get(); } - + private: WiFiManager* _wm; std::unique_ptr _handlers; std::unique_ptr server; std::unique_ptr dnsServer; - - // Shared DFTE placeholder registry + std::unique_ptr _tplRegistry; - std::function _tplSetupCallback; #ifdef WM_DFTE_LOGGING - // DFTE -> WiFiManager::log bridge (only installed when WM_DFTE_LOGGING and no other logger registered) std::unique_ptr _dfteLogger; bool _wmOwnsDfteLogSink = false; #endif - - // Singleton instance + static WiFiManagerServer* s_instance; }; -// Include WiFiManagerHandlers.h after class definition to provide full definition -// needed for std::unique_ptr destructor -#include "WiFiManagerHandlers.h" - #endif // defined(ESP8266) || defined(ESP32) #endif // WiFiManagerServer_h diff --git a/lib/WiFiManager/include/WiFiManagerUtils.h b/lib/WiFiManager/include/WiFiManagerUtils.h index cae5383..687f8d8 100644 --- a/lib/WiFiManager/include/WiFiManagerUtils.h +++ b/lib/WiFiManager/include/WiFiManagerUtils.h @@ -4,7 +4,7 @@ * String conversions, HTML encoding, and helper calculations * These functions have no dependency on WiFiManager instance state * - * @author Alex Hope-O'Connor + * @author alexhopeoconnor * @license MIT */ diff --git a/lib/WiFiManager/include/templates/CSS.h b/lib/WiFiManager/include/templates/CSS.h index 9fdd365..3345e5e 100644 --- a/lib/WiFiManager/include/templates/CSS.h +++ b/lib/WiFiManager/include/templates/CSS.h @@ -1,7 +1,11 @@ /** * CSS.h - * CSS stylesheet for WiFiManager web interface - * Embedded styles to avoid external file dependencies + * + * @author alexhopeoconnor + * @license MIT + * + * CSS stylesheet for WiFiManager web interface. + * Embedded styles to avoid external file dependencies. */ #ifndef _CSS_TEMPLATES_H_ @@ -39,6 +43,21 @@ const char CSS_STYLE[] PROGMEM = ""; #endif // _CSS_TEMPLATES_H_ diff --git a/lib/WiFiManager/include/templates/Fragments.h b/lib/WiFiManager/include/templates/Fragments.h deleted file mode 100644 index 3c9208b..0000000 --- a/lib/WiFiManager/include/templates/Fragments.h +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Fragments.h - * Reusable DFTE fragment templates for WiFiManager page composition. - */ - -#ifndef _WM_FRAGMENTS_TEMPLATE_H_ -#define _WM_FRAGMENTS_TEMPLATE_H_ - -#include - -const char WM_ACTION_FORM_TEMPLATE[] PROGMEM = -"%ACTION_PREFIX%
%BUTTON_LABEL%%ACTION_SUFFIX%"; - -const char WM_SUBMIT_BUTTON_TEMPLATE[] PROGMEM = -"

"; - -const char WM_CENTERED_BUTTON_TEMPLATE[] PROGMEM = -"
"; - -const char WM_SECTION_BREAK_TEMPLATE[] PROGMEM = -"

%SECTION_CONTENT%"; - -const char WM_SCAN_MESSAGE_TEMPLATE[] PROGMEM = -"%SCAN_MESSAGE%

"; - -const char WM_SCAN_RESULT_ROW_TEMPLATE[] PROGMEM = -"
%SSID_TEXT%" -"" -"
%QUALITY_VALUE%
"; - -const char WM_FIELD_LABEL_BEFORE_TEMPLATE[] PROGMEM = -"
"; - -const char WM_FIELD_LABEL_AFTER_TEMPLATE[] PROGMEM = -"
" -""; - -const char WM_FIELD_INPUT_ONLY_TEMPLATE[] PROGMEM = -"
"; - -const char WM_INFO_ROW_TEMPLATE[] PROGMEM = -"
%INFO_LABEL%
%INFO_VALUE%
"; - -const char WM_INFO_SECTION_TEMPLATE[] PROGMEM = -"%SECTION_PREFIX%

%SECTION_TITLE%


%SECTION_ROWS%
%SECTION_SUFFIX%"; - -const char WM_STATUS_MESSAGE_TEMPLATE[] PROGMEM = -"
%STATUS_TITLE%%STATUS_BODY%
"; - -const char WM_PAGE_HEADING_TEMPLATE[] PROGMEM = -"

%HEADER_TITLE%

%HEADER_SUBTITLE%

"; - -#endif // _WM_FRAGMENTS_TEMPLATE_H_ diff --git a/lib/WiFiManager/include/templates/HTML.h b/lib/WiFiManager/include/templates/HTML.h deleted file mode 100644 index 3d244fc..0000000 --- a/lib/WiFiManager/include/templates/HTML.h +++ /dev/null @@ -1,67 +0,0 @@ -/** - * HTML.h - * HTML template fragments for WiFiManager web interface - * Static PROGMEM strings to reduce string concatenations - */ - -#ifndef _HTML_TEMPLATES_H_ -#define _HTML_TEMPLATES_H_ - -#include - -// Status messages -const char HTML_STATUS_OFFPW[] PROGMEM = "
Authentication failure"; - -const char HTML_STATUS_OFFNOAP[] PROGMEM = "
AP not found"; - -const char HTML_STATUS_OFFFAIL[] PROGMEM = "
Could not connect"; - -const char HTML_STATUS_NONE[] PROGMEM = "
No AP set
"; - -const char HTML_SAVED[] PROGMEM = "
Saving Credentials
Trying to connect ESP to network.
If it fails reconnect to AP to try again
"; - -const char HTML_PARAMSAVED[] PROGMEM = "
Saved
"; - -// Update page -const char HTML_UPDATE[] PROGMEM = "Upload new firmware
* May not function inside captive portal, open in browser http://192.168.4.1"; - -const char HTML_UPDATE_FAIL[] PROGMEM = "
Update failed!
Reboot device and try again
"; - -const char HTML_UPDATE_SUCCESS[] PROGMEM = "
Update successful.
Device rebooting now...
"; - -// Help page -#ifndef WM_NOHELP -const char HTML_HELP[] PROGMEM = - "

Available pages


" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "
PageFunction
/Menu page.
/wifiShow WiFi scan results and enter WiFi configuration.
/0wifiShow WiFi configuration without loading scan results until refresh is requested.
/wifisaveSave WiFi configuration information and configure device. Needs variables supplied.
/paramParameter page
/infoInformation page
/updateOTA update page. Firmware upload posts to /u.
/closeClose the captiveportal popup, config portal will remain active
/exitExit Config portal, config portal will close
/restartReboot the device
/eraseErase WiFi configuration and reboot device. Device will not reconnect to a network until new WiFi configuration data is entered.
" - "

Fork https://github.com/alexhopeoconnor/WiFiManager."; -#else -const char HTML_HELP[] PROGMEM = ""; -#endif - -#endif // _HTML_TEMPLATES_H_ - diff --git a/lib/WiFiManager/include/templates/Info.h b/lib/WiFiManager/include/templates/Info.h deleted file mode 100644 index 66921c5..0000000 --- a/lib/WiFiManager/include/templates/Info.h +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Info.h - * Streamed info page content template for WiFiManager. - */ - -#ifndef _WM_INFO_TEMPLATE_H_ -#define _WM_INFO_TEMPLATE_H_ - -#include - -const char WM_INFO_CONTENT_TEMPLATE[] PROGMEM = -"%INFO_STATUS%" -"

%INFO_DEVICE_SECTION%
" -"
%INFO_WIFI_SECTION%
" -"
%INFO_ABOUT_SECTION%
" -"%INFO_FOOTER%"; - -#endif // _WM_INFO_TEMPLATE_H_ diff --git a/lib/WiFiManager/include/templates/JS.h b/lib/WiFiManager/include/templates/JS.h index 26e9785..e560a0f 100644 --- a/lib/WiFiManager/include/templates/JS.h +++ b/lib/WiFiManager/include/templates/JS.h @@ -1,7 +1,11 @@ /** * JS.h - * JavaScript code for WiFiManager web interface - * Client-side functionality for WiFi configuration + * + * @author alexhopeoconnor + * @license MIT + * + * JavaScript code for WiFiManager web interface. + * Client-side functionality for WiFi configuration. */ #ifndef _JS_TEMPLATES_H_ diff --git a/lib/WiFiManager/include/templates/Message.h b/lib/WiFiManager/include/templates/Message.h deleted file mode 100644 index 9284ffa..0000000 --- a/lib/WiFiManager/include/templates/Message.h +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Message.h - * Streamed message page content template for WiFiManager. - */ - -#ifndef _WM_MESSAGE_TEMPLATE_H_ -#define _WM_MESSAGE_TEMPLATE_H_ - -#include - -const char WM_MESSAGE_CONTENT_TEMPLATE[] PROGMEM = -"%MESSAGE_BODY%" -"%MESSAGE_ACTIONS%"; - -#endif // _WM_MESSAGE_TEMPLATE_H_ diff --git a/lib/WiFiManager/include/templates/PageShell.h b/lib/WiFiManager/include/templates/PageShell.h deleted file mode 100644 index b3f7070..0000000 --- a/lib/WiFiManager/include/templates/PageShell.h +++ /dev/null @@ -1,31 +0,0 @@ -/** - * 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%" -"%PAGE_SCRIPTS%" -"%STYLES%" -"%PAGE_STYLES%" -"" -"" -"
" -"%PAGE_CONTENT%" -"
" -"" -""; - -#endif // _WM_PAGE_SHELL_TEMPLATE_H_ diff --git a/lib/WiFiManager/include/templates/Param.h b/lib/WiFiManager/include/templates/Param.h deleted file mode 100644 index 1dfef89..0000000 --- a/lib/WiFiManager/include/templates/Param.h +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Param.h - * Streamed setup page content template for WiFiManager. - */ - -#ifndef _WM_PARAM_TEMPLATE_H_ -#define _WM_PARAM_TEMPLATE_H_ - -#include - -const char WM_PARAM_CONTENT_TEMPLATE[] PROGMEM = -"
" -"%PARAM_FIELDS%" -"%PARAM_FORM_ACTIONS%
" -"%PARAM_PAGE_ACTIONS%" -"%PARAM_STATUS%"; - -#endif // _WM_PARAM_TEMPLATE_H_ diff --git a/lib/WiFiManager/include/templates/PortalAppJS.h b/lib/WiFiManager/include/templates/PortalAppJS.h index 3416968..15e315c 100644 --- a/lib/WiFiManager/include/templates/PortalAppJS.h +++ b/lib/WiFiManager/include/templates/PortalAppJS.h @@ -1,5 +1,9 @@ /** * PortalAppJS.h + * + * @author alexhopeoconnor + * @license MIT + * * Embedded SPA for WiFiManager config portal (hash routing + JSON APIs). */ @@ -17,30 +21,96 @@ try{var el=document.getElementById('wm-bootstrap');if(el&&el.textContent)boot=JS function $(id){return document.getElementById(id);} function esc(t){if(!t)return'';return String(t).replace(/[&<>"']/g,function(c){return({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]);});} +var _wmToastTimer=null; +function showToast(msg,isErr){ + var el=$('wm-toast'); + if(!el)return; + el.textContent=msg||''; + el.className='wm-toast '+(isErr?'err':'ok'); + el.style.display='block'; + if(_wmToastTimer)clearTimeout(_wmToastTimer); + _wmToastTimer=setTimeout(function(){el.style.display='none';el.textContent='';},4500); +} +function showConfirm(msg,onOk){ + var dlg=$('wm-dialog'),dmsg=$('wm-dialog-msg'),bk=$('wm-dialog-backdrop'),ok=$('wm-dialog-ok'),cancel=$('wm-dialog-cancel'); + if(!dlg||!dmsg||!ok||!cancel){if(onOk)onOk();return;} + dmsg.textContent=msg||''; + dlg.style.display='flex'; + dlg.setAttribute('aria-hidden','false'); + function hide(){ + dlg.style.display='none'; + dlg.setAttribute('aria-hidden','true'); + ok.onclick=null; + cancel.onclick=null; + if(bk)bk.onclick=null; + } + function onCancel(){hide();} + cancel.onclick=onCancel; + if(bk)bk.onclick=onCancel; + ok.onclick=function(){hide();if(onOk)onOk();}; +} + function setView(html){ var a=$('app'); if(!a)return; a.innerHTML="
"+html+"
"; } +function renderMetaField(f){ + if(!f)return''; + if(f.html)return f.html; + var id=f.id||f.name||''; + var nm=f.name||f.id||''; + var domId=(id==='s')?'wm-s':(id==='p')?'wm-p':('wm-f-'+id); + var t=f.type||'text'; + var h=''; + h+='Info";} + if(f.showUpdate){h+=" · Update";} + if(boot.showBack){h+=" · Back";} h+="


"; return h; } +function deviceActionsHtml(){ + var f=boot.features||{}; + var h='', ok=false; + if(f.showRestart){h+="

";ok=true;} + if(f.showExitPortal){h+="

";ok=true;} + if(f.showCloseCaptive){h+="

";ok=true;} + if(!ok)return''; + return "

Device actions

"+h+"
"; +} + function viewHome(){ var sub=esc(boot.subtitle||''); var t=esc(boot.title||'WiFiManager'); var html=navBar(); html+="

"+t+"

"+sub+"

"; + if(boot.initialStatus)html+="

"+esc(boot.initialStatus)+"

"; html+="

Use the links above to configure WiFi, view device info, or update firmware.

"; + html+=deviceActionsHtml(); setView(html); } @@ -99,11 +169,16 @@ function viewWifi(){ var html=navBar()+"

WiFi

"; html+="
"; html+="
"; - html+=""; - html+=""; - html+=" "; - if(m.staticFieldsHtml){html+=m.staticFieldsHtml;} - if(m.paramsHtml){html+=m.paramsHtml;} + html+=renderFieldList(m.wifiFields||[]); + var wf=m.wifiFields||[]; + var hasPass=false; + var i; + for(i=0;i "; + } + html+=renderFieldList(m.staticFields||[]); + html+=renderFieldList(m.params||[]); html+=""; html+="
"; html+="
"; @@ -143,22 +218,52 @@ function viewInfo(){ h+=""; return h; } - html+=section('Status',d.status||[]); + var st=d.status; + if(st && typeof st==='object' && !Array.isArray(st)){ + html+="

Status


"; + html+="
Connected
"+esc(String(st.connected))+"
"; + if(st.ssid!==undefined)html+="
SSID
"+esc(st.ssid)+"
"; + if(st.stationIp)html+="
Station IP
"+esc(st.stationIp)+"
"; + if(st.apIp)html+="
AP IP
"+esc(st.apIp)+"
"; + if(st.summary)html+="
Summary
"+esc(st.summary)+"
"; + html+="
"; + } else { + html+=section('Status',d.status||[]); + } html+=section('Device',d.device||[]); html+=section('WiFi',d.wifi||[]); html+=section('About',d.about||[]); var act=d.actions||{}; + html+="

Actions


"; + if(act.showRestart)html+="

"; + if(act.showExitPortal)html+="

"; + if(act.showCloseCaptive)html+="

"; if(act.showUpdate)html+="

Firmware update

"; if(act.showErase)html+="

"; setView(html); }); } +function portalConfirmPost(url, promptMsg){ + showConfirm(promptMsg||'Continue?',function(){ + api(url,{method:'POST'}).then(function(res){ + try{var j=JSON.parse(res.body);}catch(e){j={};} + var m=(j&&j.message)?j.message:(res.ok?'OK':'Error'); + showToast(m,!res.ok); + }); + }); +} + +window.portalRestart=function(){portalConfirmPost('/api/device/restart','Restart the device?');}; +window.portalExit=function(){portalConfirmPost('/api/portal/exit','Exit the configuration portal?');}; +window.portalCloseCaptive=function(){portalConfirmPost('/api/portal/close','Stop redirecting clients to this portal (captive detection off)?');}; + window.portalErase=function(){ - if(!confirm('Erase WiFi configuration?'))return; - api('/api/device/erase',{method:'POST'}).then(function(res){ - try{var j=JSON.parse(res.body);}catch(e){j={};} - alert((j&&j.message)?j.message:'Done'); + showConfirm('Erase WiFi configuration?',function(){ + api('/api/device/erase',{method:'POST'}).then(function(res){ + try{var j=JSON.parse(res.body);}catch(e){j={};} + showToast((j&&j.message)?j.message:(res.ok?'Done':'Error'),!res.ok); + }); }); }; @@ -167,7 +272,9 @@ function viewSetup(){ api('/api/params').then(function(res){ try{var d=JSON.parse(res.body);}catch(e){d={};} var html=navBar()+"

Parameters

"; - html+="
"+(d.paramsHtml||'')+"
"; + html+="
"; + html+=renderFieldList(d.params||[]); + html+="
"; html+="
"; setView(html); }); @@ -186,17 +293,59 @@ window.portalParamSave=function(ev){ }; function viewUpdate(){ + var f=boot.features||{}; var html=navBar()+"

Firmware update

"; - html+="
"; - html+=""; - html+="
"; - html+="

Upload a .bin firmware. Device may restart after update.

"; + if(!f.showUpdate){html+="

Firmware update is disabled.

";setView(html);return;} + html+="
"; + html+=""; + html+="
"; + html+="

Upload a .bin firmware. The device restarts after a successful update.

"; + html+="
";
   setView(html);
 }
 
+window.portalOtaSubmit=function(ev){
+  ev.preventDefault();
+  var fi=$('wm-ota-file');
+  var logEl=$('wm-ota-log');
+  if(!fi||!fi.files||!fi.files[0]){
+    if(logEl)logEl.textContent='Choose a firmware file first.';
+    return false;
+  }
+  if(logEl)logEl.textContent='Uploading…';
+  var xhr=new XMLHttpRequest();
+  xhr.upload.onprogress=function(e){
+    if(!logEl||!e.lengthComputable)return;
+    logEl.textContent='Uploading… '+Math.round(100*e.loaded/e.total)+'%';
+  };
+  xhr.onreadystatechange=function(){
+    if(xhr.readyState!==4)return;
+    var t=xhr.responseText||'';
+    if(logEl)logEl.textContent=t;
+    var httpOk=xhr.status>=200&&xhr.status<300;
+    try{
+      var j=JSON.parse(t);
+      if(j&&j.message){
+        if(logEl)logEl.textContent=j.message;
+        showToast(j.message,j.ok===false);
+        return;
+      }
+    }catch(e){}
+    showToast(httpOk?'Upload finished.':('HTTP '+xhr.status),!httpOk);
+  };
+  xhr.open('POST','/u');
+  var fd=new FormData();
+  fd.append('update',fi.files[0]);
+  xhr.send(fd);
+  return false;
+};
+
 function route(){
   var h=location.hash||'#/';
   if(h.indexOf('#/')!==0)h='#/';
+  var f=boot.features||{};
+  if(h==='#/info'&&f.showInfo===false){viewHome();return;}
+  if(h==='#/update'&&f.showUpdate===false){viewHome();return;}
   if(h==='#/'||h==='#'){viewHome();return;}
   if(h==='#/wifi'){viewWifi();return;}
   if(h==='#/info'){viewInfo();return;}
diff --git a/lib/WiFiManager/include/templates/Root.h b/lib/WiFiManager/include/templates/Root.h
deleted file mode 100644
index 5a2b198..0000000
--- a/lib/WiFiManager/include/templates/Root.h
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Root.h
- * Whole-document root template for WiFiManager using DFTE placeholders
- */
-
-#ifndef _WM_ROOT_TEMPLATE_H_
-#define _WM_ROOT_TEMPLATE_H_
-
-#include 
-
-// Whole document template:
-// - Injects %SCRIPTS% and %STYLES% (these already include "
     "
-)rawliteral";
-
-#endif // _WIFI_POLLING_JS_H_
-
diff --git a/lib/WiFiManager/src/WiFiManager.cpp b/lib/WiFiManager/src/WiFiManager.cpp
index db7eecd..eb5b9ee 100644
--- a/lib/WiFiManager/src/WiFiManager.cpp
+++ b/lib/WiFiManager/src/WiFiManager.cpp
@@ -6,13 +6,13 @@
  * 
  * @author Creator tzapu
  * @author tablatronix
- * @author Alex Hope-O'Connor
+ * @author alexhopeoconnor
  * @license MIT
  */
 
 #include "WiFiManager.h"
 #include "WiFiManagerLogSink.h"
-#include "WiFiManagerServer.h" // Need menu tokens
+#include "WiFiManagerServer.h"
 
 #if defined(ESP8266) || defined(ESP32)
 
@@ -508,7 +508,6 @@ void WiFiManager::setupConfigPortal() {
   // Lazy initialization: only create server manager when config portal is actually needed
   if (!_serverManager) {
     _serverManager = std::make_unique(this);
-    _serverManager->registerTemplateSetupCallback(_templateSetupCallback);
   }
   _serverManager->createServer(_httpPort);
   _serverManager->registerRoutes();
@@ -2013,6 +2012,10 @@ void WiFiManager::setShowInfoUpdate(boolean enabled){
   _showInfoUpdate = enabled;
 }
 
+void WiFiManager::setShowInfo(boolean enabled){
+  _showInfo = enabled;
+}
+
 /**
  * check if the config portal is running
  * @return bool true if active
@@ -2054,20 +2057,6 @@ void WiFiManager::setTitle(String title){
   _title = title;
 }
 
-void WiFiManager::registerTemplateSetupCallback(std::function cb) {
-  _templateSetupCallback = std::move(cb);
-  if (_serverManager) {
-    _serverManager->registerTemplateSetupCallback(_templateSetupCallback);
-    _serverManager->rebuildPlaceholderRegistry();
-  }
-}
-
-void WiFiManager::rebuildPlaceholderRegistry(std::function customizer) {
-  if (_serverManager) {
-    _serverManager->rebuildPlaceholderRegistry(std::move(customizer));
-  }
-}
-
 /**
  * Set params as separate page not in wifi
  * NOT COMPATIBLE WITH setMenu!
diff --git a/lib/WiFiManager/src/WiFiManagerHandlers.cpp b/lib/WiFiManager/src/WiFiManagerHandlers.cpp
index 3dd8686..826ec89 100644
--- a/lib/WiFiManager/src/WiFiManagerHandlers.cpp
+++ b/lib/WiFiManager/src/WiFiManagerHandlers.cpp
@@ -3,24 +3,15 @@
  * 
  * HTTP request handlers and rendering logic implementation
  * 
- * @author tablatronix
- * @author Alex Hope-O'Connor
+ * @author alexhopeoconnor
  * @license MIT
  */
 
 #include "WiFiManagerHandlers.h"
-#include "WiFiManagerServer.h" // Need menu tokens and routes
-#include "templates/HTML.h"
+#include "WiFiManagerServer.h"
 #include "templates/CSS.h"
 #include "templates/JS.h"
-#include "templates/WiFiPollingJS.h"
-#include "templates/PageShell.h"
-#include "templates/Fragments.h"
-#include "templates/Info.h"
-#include "templates/Message.h"
-#include "templates/Param.h"
-#include "templates/WiFi.h"
-#include "templates/RootSelector.h"
+#include "templates/RootShell.h"
 #include "templates/PortalAppJS.h"
 #include 
 #include 
@@ -48,58 +39,6 @@ static void jsonAppendEscaped(String& out, const String& s) {
   }
 }
 
-/** Parse a single `
...
...
` row from `getInfoData` HTML. */ -static bool parseInfoRowDtDd(const String& html, String& label, String& value) { - const int dtOpen = html.indexOf(F("
")); - if (dtOpen < 0) { - return false; - } - const int dtClose = html.indexOf(F("
"), dtOpen); - if (dtClose < 0) { - return false; - } - label = html.substring(dtOpen + 4, dtClose); - const int ddOpen = html.indexOf(F("
"), dtClose); - if (ddOpen < 0) { - return false; - } - const int ddClose = html.indexOf(F("
"), ddOpen); - if (ddClose < 0) { - return false; - } - value = html.substring(ddOpen + 4, ddClose); - return true; -} - -static void appendJsonInfoArrayFromIds(WiFiManagerHandlers* handlers, - String& json, - const char* const* ids, - size_t count, - bool& first) { - for (size_t i = 0; i < count; i++) { - const String row = handlers->getInfoData(ids[i]); - if (row.length() == 0) { - continue; - } - String label; - String value; - if (!parseInfoRowDtDd(row, label, value)) { - continue; - } - if (!first) { - json += F(","); - } - first = false; - json += F("{\"key\":\""); - json += ids[i]; - json += F("\",\"label\":\""); - jsonAppendEscaped(json, label); - json += F("\",\"value\":\""); - jsonAppendEscaped(json, value); - json += F("\"}"); - } -} - namespace { const char kEmptyTemplateChunk[] PROGMEM = ""; @@ -142,7 +81,6 @@ void registerSharedShellPlaceholders(WiFiManagerServer* server, PlaceholderRegis if (server) { server->registerDefaultStyles(registry); server->registerDefaultScripts(registry); - server->applyTemplateSetupCallback(registry); return; } @@ -150,479 +88,6 @@ void registerSharedShellPlaceholders(WiFiManagerServer* server, PlaceholderRegis registry.registerProgmemData("%SCRIPTS%", JS_SCRIPT); } -void registerShellTemplate(PlaceholderRegistry& registry, - DynamicTemplateDescriptor& docTitleDescriptor, - String& docTitle, - const char* bodyClass, - const char* pageContentTemplate, - const char* pageScripts = kEmptyTemplateChunk, - const char* pageStyles = kEmptyTemplateChunk) { - configureDynamicStringDescriptor(docTitleDescriptor, docTitle); - registry.registerDynamicTemplate("%DOC_TITLE%", &docTitleDescriptor); - registry.registerProgmemData("%BODY_CLASS%", bodyClass); - registry.registerProgmemData("%PAGE_SCRIPTS%", pageScripts); - registry.registerProgmemData("%PAGE_STYLES%", pageStyles); - registry.registerProgmemTemplate("%PAGE_CONTENT%", pageContentTemplate); -} - -struct MessagePageState { - String docTitle; - const char* bodyClass; - String messageBody; - String actions; - - MessagePageState() : bodyClass(C_root) {} -}; - -struct InfoPageState { - String docTitle; - String status; - String deviceSection; - String wifiSection; - String aboutSection; - String footer; -}; - -struct WiFiPageState { - String docTitle; - String scanContent; - String ssidPlaceholder; - String passwordPlaceholder; - String staticFields; - String paramSection; - String formActions; - String pageActions; - String status; -}; - -struct ParamPageState { - String docTitle; - String fields; - String formActions; - String pageActions; - String status; -}; - -void escapePercentsForTemplate(String& value) { - if (value.indexOf('%') < 0) return; - - String escaped; - reservePage(escaped, value.length() + 32); - for (size_t i = 0; i < value.length(); i++) { - if (value[i] == '%') { - escaped += F("%"); - } else { - escaped += value[i]; - } - } - value = escaped; -} - -String renderActionForm(const String& action, - const String& method, - const String& label, - const String& buttonClassAttr = String(), - const String& prefix = String(), - const String& suffix = String()) { - String output; - reservePage(output, prefix.length() + suffix.length() + action.length() + method.length() + - label.length() + buttonClassAttr.length() + 48); - output += prefix; - output += F("
"); - output += label; - output += F(""); - output += suffix; - return output; -} - -String renderSubmitButton(const String& label) { - String output; - reservePage(output, label.length() + 40); - output += F("

"); - return output; -} - -String renderCenteredButton(const String& id, - const String& type, - const String& onClick, - const String& label, - const String& extraAttrs = String()) { - String output; - reservePage(output, id.length() + type.length() + onClick.length() + label.length() + - extraAttrs.length() + 72); - output += F("
"); - return output; -} - -String renderSectionBreak(const String& content) { - if (content.length() == 0) { - return String(); - } - - String output; - reservePage(output, content.length() + 16); - output += F("

"); - output += content; - return output; -} - -String renderScanMessage(const String& message) { - String output; - reservePage(output, message.length() + 16); - output += message; - output += F("

"); - return output; -} - -String renderScanRow(const String& ssidAttr, - const String& ssidText, - const String& qualityLabel, - const String& qualityIcon, - const String& lockClass, - const String& iconVisibilityClass, - const String& valueVisibilityClass, - const String& qualityValue) { - String output; - reservePage(output, ssidAttr.length() + ssidText.length() + qualityLabel.length() + - qualityIcon.length() + lockClass.length() + - iconVisibilityClass.length() + valueVisibilityClass.length() + - qualityValue.length() + 160); - output += F("
"); - output += ssidText; - output += F("
"); - output += qualityValue; - output += F("
"); - return output; -} - -String renderFieldTemplate(const char* templateData, - const String& id, - const String& label, - const String& maxLength, - const String& value, - const String& extraAttrs = String()) { - String input; - reservePage(input, id.length() + label.length() + maxLength.length() + value.length() + - extraAttrs.length() + 96); - input += F(""); - - if (templateData == WM_FIELD_LABEL_BEFORE_TEMPLATE) { - String output; - reservePage(output, label.length() + input.length() + 32); - output += F("
"); - output += input; - return output; - } - - if (templateData == WM_FIELD_LABEL_AFTER_TEMPLATE) { - String output; - reservePage(output, label.length() + input.length() + 32); - output += F("
"); - output += input; - output += F(""); - return output; - } - - String output; - reservePage(output, input.length() + 8); - output += F("
"); - output += input; - return output; -} - -String renderInfoRow(const String& label, const String& value) { - String output; - reservePage(output, label.length() + value.length() + 24); - output += F("
"); - output += label; - output += F("
"); - output += value; - output += F("
"); - return output; -} - -String renderInfoSection(const String& title, - const String& rows, - const String& prefix = String(), - const String& suffix = String()) { - String output; - reservePage(output, prefix.length() + suffix.length() + title.length() + rows.length() + 40); - output += prefix; - output += F("

"); - output += title; - output += F("


"); - output += rows; - output += F("
"); - output += suffix; - return output; -} - -String renderStatusMessage(const String& title, - const String& body, - const String& statusClassSuffix = String()) { - String output; - reservePage(output, title.length() + body.length() + statusClassSuffix.length() + 48); - output += F("
"); - output += title; - output += F(""); - output += body; - output += F("
"); - return output; -} - -String renderPageHeading(const String& title, const String& subtitle) { - String output; - reservePage(output, title.length() + subtitle.length() + 20); - output += F("

"); - output += title; - output += F("

"); - output += subtitle; - output += F("

"); - return output; -} - -void buildMessagePageState(MessagePageState& state, - const String& title, - const char* bodyClass, - const String& messageBodyHtml, - const String& actionsHtml) { - state.docTitle = title; - state.bodyClass = bodyClass; - state.messageBody = messageBodyHtml; - state.actions = actionsHtml; - escapePercentsForTemplate(state.messageBody); - escapePercentsForTemplate(state.actions); -} - -String buildUpdatePanelContent(const String& title, const String& subtitle) { - String content; - reservePage(content, 2048); - content += renderPageHeading(title, subtitle); - content += FPSTR(HTML_UPDATE); - return content; -} - -String buildUpdateResultContent(const String& title, const String& subtitle) { - String content; - reservePage(content, 2048); - content += renderPageHeading(title, subtitle); - - if (Update.hasError()) { - content += FPSTR(HTML_UPDATE_FAIL); - #ifdef ESP32 - content += "OTA Error: " + String(Update.errorString()); - #else - content += "OTA Error: " + String(Update.getError()); - #endif - } else { - content += FPSTR(HTML_UPDATE_SUCCESS); - } - - return 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); - reservePage(state.footer, 1024); - - state.docTitle = F("Info"); - handlers->reportStatus(state.status); - -#ifdef ESP8266 - static const char* const deviceIds[] = { - "uptime", "chipid", "fchipid", "idesize", "flashsize", "corever", - "bootver", "cpufreq", "freeheap", "memsketch", "memsmeter", "lastreset" - }; - static const char* const wifiIds[] = { - "conx", "stassid", "staip", "stagw", "stasub", "dnss", "host", - "stamac", "autoconx", "apssid", "apip", "apbssid", "apmac" - }; -#elif defined(ESP32) - static const char* const deviceIds[] = { - "uptime", "chipid", "chiprev", "idesize", "flashsize", - "cpufreq", "freeheap", "memsketch", "memsmeter", "lastreset", "temp" - }; - static const char* const wifiIds[] = { - "conx", "stassid", "staip", "stagw", "stasub", "dnss", "host", - "stamac", "apssid", "apip", "apmac", "aphost", "apbssid" - }; -#endif - - appendInfoEntries(handlers, state.deviceSection, deviceIds, sizeof(deviceIds) / sizeof(deviceIds[0])); - appendInfoEntries(handlers, state.wifiSection, wifiIds, sizeof(wifiIds) / sizeof(wifiIds[0])); - state.aboutSection += handlers->getInfoData("aboutver"); - state.aboutSection += handlers->getInfoData("aboutarduinover"); - state.aboutSection += handlers->getInfoData("aboutsdkver"); - state.aboutSection += handlers->getInfoData("aboutdate"); - - if (showInfoUpdate) { - state.footer += renderActionForm(F("/update"), F("get"), F("Update"), String(), F("

"), F("
\n")); - } - if (showInfoErase) { - state.footer += renderActionForm(F("/erase"), F("get"), F("Erase WiFi config"), F(" class='D'"), showInfoUpdate ? String() : String(F("

")), F("
\n")); - } - if (showBack) { - state.footer += renderActionForm(F("/"), F("get"), F("Back"), String(), (showInfoUpdate || showInfoErase) ? String() : String(F("

"))); - } - state.footer += FPSTR(HTML_HELP); - - state.deviceSection = renderInfoSection( -#ifdef ESP32 - F("ESP32"), -#else - F("ESP8266"), -#endif - state.deviceSection - ); - state.wifiSection = renderInfoSection(F("WiFi"), state.wifiSection, F("
")); - state.aboutSection = renderInfoSection(F("About"), state.aboutSection, F("
")); - - escapePercentsForTemplate(state.status); - escapePercentsForTemplate(state.deviceSection); - escapePercentsForTemplate(state.wifiSection); - escapePercentsForTemplate(state.aboutSection); - escapePercentsForTemplate(state.footer); -} - -void buildWiFiPageState(WiFiManagerHandlers* handlers, - WiFiPageState& state, - bool includeScanResults, - bool showBack, - const String& ssidPlaceholder, - const String& passwordPlaceholder, - bool paramsInWifi) { - reservePage(state.scanContent, includeScanResults ? 4096 : 64); - reservePage(state.staticFields, 2048); - reservePage(state.paramSection, 4096); - reservePage(state.formActions, 128); - reservePage(state.pageActions, 512); - reservePage(state.status, 512); - - state.docTitle = F("Config ESP"); - state.ssidPlaceholder = ssidPlaceholder; - state.passwordPlaceholder = passwordPlaceholder; - - if (includeScanResults) { - state.scanContent += handlers->getScanItemOut(); - } - - state.staticFields += handlers->getStaticOut(); - if (paramsInWifi) { - state.paramSection += renderSectionBreak(handlers->getParamOut()); - } - - state.formActions += renderSubmitButton(F("Save")); - state.pageActions += renderCenteredButton( - F("refresh-btn"), - F("button"), - F("return refreshScan()"), - F("Refresh"), - includeScanResults ? String() : String(F(" data-skip-initial-scan='true'")) - ); - if (showBack) state.pageActions += renderActionForm(F("/"), F("get"), F("Back"), String(), F("

")); - - handlers->reportStatus(state.status); - - escapePercentsForTemplate(state.scanContent); - escapePercentsForTemplate(state.ssidPlaceholder); - escapePercentsForTemplate(state.passwordPlaceholder); - escapePercentsForTemplate(state.staticFields); - escapePercentsForTemplate(state.paramSection); - escapePercentsForTemplate(state.formActions); - escapePercentsForTemplate(state.pageActions); - escapePercentsForTemplate(state.status); -} - -void buildParamPageState(WiFiManagerHandlers* handlers, - ParamPageState& state, - bool showBack) { - reservePage(state.fields, 4096); - reservePage(state.formActions, 128); - reservePage(state.pageActions, 256); - reservePage(state.status, 512); - - state.docTitle = F("Setup"); - state.fields += handlers->getParamOut(); - state.formActions += renderSubmitButton(F("Save")); - - if (showBack) state.pageActions += renderActionForm(F("/"), F("get"), F("Back"), String(), F("

")); - - handlers->reportStatus(state.status); - - escapePercentsForTemplate(state.fields); - escapePercentsForTemplate(state.formActions); - escapePercentsForTemplate(state.pageActions); - escapePercentsForTemplate(state.status); -} - struct ShellRenderBundle { String bootstrapJson; String pageTitleStatic; @@ -635,142 +100,10 @@ struct ShellRenderBundle { : registry(WM_TEMPLATE_REGISTRY_CAPACITY), bootstrapDescriptor{}, pageTitleDescriptor{} {} }; -struct MessageRenderBundle { - MessagePageState state; - PlaceholderRegistry registry; - TemplateContext context; - DynamicTemplateDescriptor docTitleDescriptor; - DynamicTemplateDescriptor messageBodyDescriptor; - DynamicTemplateDescriptor actionsDescriptor; - - MessageRenderBundle() - : registry(WM_TEMPLATE_REGISTRY_CAPACITY), - docTitleDescriptor{}, - messageBodyDescriptor{}, - actionsDescriptor{} {} -}; - -struct InfoRenderBundle { - InfoPageState state; - PlaceholderRegistry registry; - TemplateContext context; - DynamicTemplateDescriptor docTitleDescriptor; - DynamicTemplateDescriptor statusDescriptor; - DynamicTemplateDescriptor deviceDescriptor; - DynamicTemplateDescriptor wifiDescriptor; - DynamicTemplateDescriptor aboutDescriptor; - DynamicTemplateDescriptor footerDescriptor; - - InfoRenderBundle() - : registry(WM_TEMPLATE_REGISTRY_CAPACITY), - docTitleDescriptor{}, - statusDescriptor{}, - deviceDescriptor{}, - wifiDescriptor{}, - aboutDescriptor{}, - footerDescriptor{} {} -}; - -struct WiFiRenderBundle { - WiFiPageState state; - PlaceholderRegistry registry; - TemplateContext context; - DynamicTemplateDescriptor docTitleDescriptor; - DynamicTemplateDescriptor scanContentDescriptor; - DynamicTemplateDescriptor ssidPlaceholderDescriptor; - DynamicTemplateDescriptor passwordPlaceholderDescriptor; - DynamicTemplateDescriptor staticFieldsDescriptor; - DynamicTemplateDescriptor paramSectionDescriptor; - DynamicTemplateDescriptor formActionsDescriptor; - DynamicTemplateDescriptor pageActionsDescriptor; - DynamicTemplateDescriptor statusDescriptor; - - WiFiRenderBundle() - : registry(WM_TEMPLATE_REGISTRY_CAPACITY), - docTitleDescriptor{}, - scanContentDescriptor{}, - ssidPlaceholderDescriptor{}, - passwordPlaceholderDescriptor{}, - staticFieldsDescriptor{}, - paramSectionDescriptor{}, - formActionsDescriptor{}, - pageActionsDescriptor{}, - statusDescriptor{} {} -}; - -struct ParamRenderBundle { - ParamPageState state; - PlaceholderRegistry registry; - TemplateContext context; - DynamicTemplateDescriptor docTitleDescriptor; - DynamicTemplateDescriptor fieldsDescriptor; - DynamicTemplateDescriptor formActionsDescriptor; - DynamicTemplateDescriptor pageActionsDescriptor; - DynamicTemplateDescriptor statusDescriptor; - - ParamRenderBundle() - : registry(WM_TEMPLATE_REGISTRY_CAPACITY), - docTitleDescriptor{}, - fieldsDescriptor{}, - formActionsDescriptor{}, - pageActionsDescriptor{}, - statusDescriptor{} {} -}; - -void sendMessageTemplateResponse(WiFiManagerServer* server, - const std::shared_ptr& bundle, - const String& title, - const char* bodyClass, - const String& messageBody, - const String& actions = String()) { - buildMessagePageState(bundle->state, title, bodyClass, messageBody, actions); - - registerSharedShellPlaceholders(server, bundle->registry); - registerShellTemplate(bundle->registry, - bundle->docTitleDescriptor, - bundle->state.docTitle, - bundle->state.bodyClass, - WM_MESSAGE_CONTENT_TEMPLATE); - configureDynamicStringDescriptor(bundle->messageBodyDescriptor, bundle->state.messageBody); - configureDynamicStringDescriptor(bundle->actionsDescriptor, bundle->state.actions); - - bundle->registry.registerDynamicTemplate("%MESSAGE_BODY%", &bundle->messageBodyDescriptor); - bundle->registry.registerDynamicTemplate("%MESSAGE_ACTIONS%", &bundle->actionsDescriptor); -} - } // namespace WiFiManagerHandlers::WiFiManagerHandlers(WiFiManager* wm) : _wm(wm) {} -// Rendering Methods - -String WiFiManagerHandlers::getMenuOut(){ - return getMenuOut(nullptr); -} - -String WiFiManagerHandlers::getMenuOut(String* outOpt){ - String local; - String &out = outOpt ? *outOpt : local; - out += renderActionForm(F("/wifi"), F("get"), F("Configure WiFi"), String(), String(), F("
\n")); - - // Show PARAM (Setup) when params are on their own page - if(!_wm->_paramsInWifi && _wm->_paramsCount > 0){ - out += renderActionForm(F("/param"), F("get"), F("Setup"), String(), String(), F("
\n")); - } - - out += renderActionForm(F("/info"), F("get"), F("Info"), String(), String(), F("
\n")); - - // When captive/config portal is active, offer Close (keeps portal running) - if(_wm->configPortalActive){ - out += renderActionForm(F("/close"), F("get"), F("Close"), String(), String(), F("
\n")); - } - - out += renderActionForm(F("/exit"), F("get"), F("Exit"), String(), String(), F("
\n")); - out += F("

"); - out += renderActionForm(F("/update"), F("get"), F("Update"), String(), String(), F("
\n")); - return out; -} - void WiFiManagerHandlers::collectVisibleScanResults(std::vector& networks) { networks.clear(); networks.reserve(_wm->_scanResultsCache.size()); @@ -816,375 +149,345 @@ void WiFiManagerHandlers::collectVisibleScanResults(std::vector networks; - collectVisibleScanResults(networks); - const int n = static_cast(networks.size()); - reservePage(page, 256 + (n > 0 ? static_cast(n) * 96 : 96)); - - if (_wm->_scan.state == WiFiManager::WM_SCAN_RUNNING || _wm->_scan.state == WiFiManager::WM_SCAN_QUEUED) { - page += renderScanMessage(F("Scanning for networks...")); - return page; - } - - if (!_wm->_scan.resultsValid || n == 0) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, F("No networks found")); - #endif - if (_wm->_scan.state == WiFiManager::WM_SCAN_TIMEOUT) { - page += renderScanMessage(F("Scan timed out. Refresh to try again.")); - } else if (_wm->_scan.state == WiFiManager::WM_SCAN_FAILED) { - page += renderScanMessage(F("Scan failed. Refresh to try again.")); - } else { - page += renderScanMessage(F("No networks found. Refresh to scan again.")); +void WiFiManagerHandlers::appendPortalJsonStaticFields(String& json, bool& first) { + auto emitField = [&](const char* fieldId, const __FlashStringHelper* label, const String& val) { + if (!first) { + json += F(","); } - return page; + first = false; + json += F("{\"id\":\""); + json += fieldId; + json += F("\",\"name\":\""); + json += fieldId; + json += F("\",\"label\":\""); + json += label; + json += F("\",\"type\":\"text\",\"maxlength\":15,\"value\":\""); + jsonAppendEscaped(json, val); + json += F("\"}"); + }; + + if ((_wm->_staShowStaticFields || _wm->_sta_static_ip) && _wm->_staShowStaticFields >= 0) { + emitField("ip", F("Static IP"), (_wm->_sta_static_ip ? _wm->_sta_static_ip.toString() : "")); + emitField("gw", F("Static gateway"), (_wm->_sta_static_gw ? _wm->_sta_static_gw.toString() : "")); + emitField("sn", F("Subnet"), (_wm->_sta_static_sn ? _wm->_sta_static_sn.toString() : "")); } + if ((_wm->_staShowDns || _wm->_sta_static_dns) && _wm->_staShowDns >= 0) { + emitField("dns", F("Static DNS"), (_wm->_sta_static_dns ? _wm->_sta_static_dns.toString() : "")); + } +} - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, n,F("networks found")); - #endif - - String hiddenClass = _wm->_scanDispOptions ? "" : "h"; - String visibleClass = _wm->_scanDispOptions ? "h" : ""; - +void WiFiManagerHandlers::appendPortalJsonCustomParams(String& json, bool& first) { + const int n = _wm->getParametersCount(); + if (n <= 0) { + return; + } + WiFiManagerParameter** params = _wm->getParameters(); for (int i = 0; i < n; i++) { - const auto* network = networks[static_cast(i)]; - - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem,F("AP: "),(String)network->rssi + " " + network->ssid); - #endif - - int rssiperc = _wm->getRSSIasQuality(network->rssi); - String ssid_encoded = _wm->htmlEntities(network->ssid); - String ssid_display = _wm->htmlEntities(network->ssid, true); - int quality = int(round(map(rssiperc,0,100,1,4))); - String lockIcon = (network->encType != WM_WIFIOPEN) ? "l" : ""; - String qualityPercent = String(rssiperc) + "%"; - - page += renderScanRow( - ssid_encoded, - ssid_display, - qualityPercent, - String(quality), - lockIcon, - hiddenClass, - visibleClass, - qualityPercent - ); - - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem, F("Added network item")); - #endif - delay(0); - } - - page += F("
"); - return page; -} - -String WiFiManagerHandlers::getIpForm(String id, String title, String value){ - return renderFieldTemplate(WM_FIELD_LABEL_BEFORE_TEMPLATE, id, title, F("15"), value); -} - -String WiFiManagerHandlers::getStaticOut(){ - String page; - reservePage(page, 384); - String fields; - reservePage(fields, 384); - if ((_wm->_staShowStaticFields || _wm->_sta_static_ip) && _wm->_staShowStaticFields>=0) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem,F("_staShowStaticFields")); - #endif - fields += getIpForm(FPSTR(S_ip),F("Static IP"),(_wm->_sta_static_ip ? _wm->_sta_static_ip.toString() : "")); - fields += getIpForm(FPSTR(S_gw),F("Static gateway"),(_wm->_sta_static_gw ? _wm->_sta_static_gw.toString() : "")); - fields += getIpForm(FPSTR(S_sn),F("Subnet"),(_wm->_sta_static_sn ? _wm->_sta_static_sn.toString() : "")); - } - - if((_wm->_staShowDns || _wm->_sta_static_dns) && _wm->_staShowDns>=0){ - fields += getIpForm(FPSTR(S_dns),F("Static DNS"),(_wm->_sta_static_dns ? _wm->_sta_static_dns.toString() : "")); - } - - if(fields != ""){ - page += renderSectionBreak(fields); - page += F("
"); - } - - return page; -} - -String WiFiManagerHandlers::getParamOut(){ - String page; - reservePage(page, 256 + (_wm->_paramsCount > 0 ? static_cast(_wm->_paramsCount) * 128 : 64)); - - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem,F("getParamOut"),_wm->_paramsCount); - #endif - - if(_wm->_paramsCount > 0){ - - char valLength[5]; - - for (int i = 0; i < _wm->_paramsCount; i++) { - if (_wm->_params[i] == NULL || _wm->_params[i]->getValueLength() > 99999) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Error, kWiFiMgrLogSubsystem,F("[ERROR] WiFiManagerParameter is out of scope")); - #endif - return ""; - } + WiFiManagerParameter* p = params[i]; + if (p == NULL || p->getValueLength() > 99999) { + continue; } - - // add the extra parameters to the form - for (int i = 0; i < _wm->_paramsCount; i++) { - String pitem; - - if (_wm->_params[i]->getID() != NULL) { - String paramId = _wm->_params[i]->getID(); - String paramLabel = _wm->htmlEntities(_wm->_params[i]->getLabel(), true); - String paramValue = _wm->htmlEntities(_wm->_params[i]->getValue(), true); - String customHTML = _wm->_params[i]->getCustomHTML(); - snprintf(valLength, 5, "%d", _wm->_params[i]->getValueLength()); - String extraAttrs = customHTML.length() > 0 ? String(F(" ")) + customHTML : String(); - - switch (_wm->_params[i]->getLabelPlacement()) { - case WFM_LABEL_BEFORE: - pitem = renderFieldTemplate(WM_FIELD_LABEL_BEFORE_TEMPLATE, paramId, paramLabel, String(valLength), paramValue, extraAttrs); - break; - case WFM_LABEL_AFTER: - pitem = renderFieldTemplate(WM_FIELD_LABEL_AFTER_TEMPLATE, paramId, paramLabel, String(valLength), paramValue, extraAttrs); - break; - default: - pitem = renderFieldTemplate(WM_FIELD_INPUT_ONLY_TEMPLATE, paramId, paramLabel, String(valLength), paramValue, extraAttrs); - break; - } - } else { - // Custom HTML only (no ID) - pitem = _wm->_params[i]->getCustomHTML(); - } - - page += pitem; + if (!first) { + json += F(","); } - } - - return page; -} - -String WiFiManagerHandlers::getInfoData(String id){ - String p; - if(id==F("uptime")){ - p = renderInfoRow(F("Uptime"), String(millis() / 1000 / 60) + F(" mins ") + String((millis() / 1000) % 60) + F(" secs")); - } - else if(id==F("chipid")){ - #ifdef ESP8266 - p = renderInfoRow(F("Chip ID"), String(ESP.getChipId(),HEX)); - #elif defined(ESP32) - p = renderInfoRow(F("Chip ID"), String((uint32_t)ESP.getEfuseMac(),HEX)); - #endif - } - #ifdef ESP32 - else if(id==F("chiprev")){ - String rev = (String)ESP.getChipRevision(); - #ifdef _SOC_EFUSE_REG_H_ - String revb = (String)(REG_READ(EFUSE_BLK0_RDATA3_REG) >> (EFUSE_RD_CHIP_VER_RESERVE_S)&&EFUSE_RD_CHIP_VER_RESERVE_V); - p = renderInfoRow(F("Chip rev"), rev + F("
") + revb); - #else - p = renderInfoRow(F("Chip rev"), rev); - #endif - } - #endif - #ifdef ESP8266 - else if(id==F("fchipid")){ - p = renderInfoRow(F("Flash chip ID"), String(ESP.getFlashChipId())); - } - #endif - else if(id==F("idesize")){ - p = renderInfoRow(F("Flash size"), String(ESP.getFlashChipSize()) + F(" bytes")); - } - else if(id==F("flashsize")){ - #ifdef ESP8266 - p = renderInfoRow(F("Real flash size"), String(ESP.getFlashChipRealSize()) + F(" bytes")); - #elif defined ESP32 - p = renderInfoRow(F("PSRAM Size"), String(ESP.getPsramSize()) + F(" bytes")); - #endif - } - else if(id==F("corever")){ - #ifdef ESP8266 - p = renderInfoRow(F("Core version"), String(ESP.getCoreVersion())); - #endif - } - #ifdef ESP8266 - else if(id==F("bootver")){ - p = renderInfoRow(F("Boot version"), String(system_get_boot_version())); - } - #endif - else if(id==F("cpufreq")){ - p = renderInfoRow(F("CPU frequency"), String(ESP.getCpuFreqMHz()) + F("MHz")); - } - else if(id==F("freeheap")){ - p = renderInfoRow(F("Memory - Free heap"), String(ESP.getFreeHeap()) + F(" bytes available")); - } - else if(id==F("memsketch")){ - p = renderInfoRow(F("Memory - Sketch size"), String(F("Used / Total bytes
")) + String(ESP.getSketchSize()) + F(" / ") + String(ESP.getSketchSize()+ESP.getFreeSketchSpace())); - } - else if(id==F("memsmeter")){ - p = F("
"); - } - else if(id==F("lastreset")){ - #ifdef ESP8266 - p = renderInfoRow(F("Last reset reason"), String(ESP.getResetReason())); - #elif defined(ESP32) && defined(_ROM_RTC_H_) - String reasons[2]; - for(int i=0;i<2;i++){ - int reason = rtc_get_reset_reason(i); - switch (reason) - { - case 1 : reasons[i] = F("Vbat power on reset");break; - case 3 : reasons[i] = F("Software reset digital core");break; - case 4 : reasons[i] = F("Legacy watch dog reset digital core");break; - case 5 : reasons[i] = F("Deep Sleep reset digital core");break; - case 6 : reasons[i] = F("Reset by SLC module, reset digital core");break; - case 7 : reasons[i] = F("Timer Group0 Watch dog reset digital core");break; - case 8 : reasons[i] = F("Timer Group1 Watch dog reset digital core");break; - case 9 : reasons[i] = F("RTC Watch dog Reset digital core");break; - case 10 : reasons[i] = F("Instrusion tested to reset CPU");break; - case 11 : reasons[i] = F("Time Group reset CPU");break; - case 12 : reasons[i] = F("Software reset CPU");break; - case 13 : reasons[i] = F("RTC Watch dog Reset CPU");break; - case 14 : reasons[i] = F("for APP CPU, reseted by PRO CPU");break; - case 15 : reasons[i] = F("Reset when the vdd voltage is not stable");break; - case 16 : reasons[i] = F("RTC Watch dog reset digital core and rtc module");break; - default : reasons[i] = F("NO_MEAN"); + first = false; + if (p->getID() != nullptr) { + String pid = String(p->getID()); + json += F("{\"name\":\""); + jsonAppendEscaped(json, pid); + json += F("\",\"id\":\""); + jsonAppendEscaped(json, pid); + json += F("\",\"label\":\""); + jsonAppendEscaped(json, String(p->getLabel())); + json += F("\",\"type\":\"text\",\"maxlength\":"); + json += String(p->getValueLength()); + json += F(",\"value\":\""); + jsonAppendEscaped(json, String(p->getValue())); + json += F("\",\"labelPlacement\":"); + json += String(p->getLabelPlacement()); + { + String c = p->getCustomHTML(); + if (c.length() > 0) { + json += F(",\"customAttrs\":\""); + jsonAppendEscaped(json, c); + json += F("\""); } } - p = renderInfoRow(F("Last reset reason"), String(F("CPU0: ")) + reasons[0] + F("
CPU1: ") + reasons[1]); - #endif - } - else if(id==F("apip")){ - p = renderInfoRow(F("Access point IP"), WiFi.softAPIP().toString()); - } - else if(id==F("apmac")){ - p = renderInfoRow(F("Access point MAC"), WiFi.softAPmacAddress()); - } - #ifdef ESP32 - else if(id==F("aphost")){ - p = renderInfoRow(F("Access point hostname"), String(WiFi.softAPgetHostname())); - } - #endif - #ifndef WM_NOSOFTAPSSID - #ifdef ESP8266 - else if(id==F("apssid")){ - p = renderInfoRow(F("Access point SSID"), _wm->htmlEntities(WiFi.softAPSSID(), true)); - } - #endif - #endif - else if(id==F("apbssid")){ - p = renderInfoRow(F("BSSID"), WiFi.BSSIDstr()); - } - else if(id==F("stassid")){ - p = renderInfoRow(F("Station SSID"), _wm->htmlEntities((String)_wm->WiFi_SSID(), true)); - } - else if(id==F("staip")){ - p = renderInfoRow(F("Station IP"), WiFi.localIP().toString()); - } - else if(id==F("stagw")){ - p = renderInfoRow(F("Station gateway"), WiFi.gatewayIP().toString()); - } - else if(id==F("stasub")){ - p = renderInfoRow(F("Station subnet"), WiFi.subnetMask().toString()); - } - else if(id==F("dnss")){ - p = renderInfoRow(F("DNS Server"), WiFi.dnsIP().toString()); - } - else if(id==F("host")){ - #ifdef ESP32 - p = renderInfoRow(F("Hostname"), String(WiFi.getHostname())); - #else - p = renderInfoRow(F("Hostname"), String(WiFi.hostname())); - #endif - } - else if(id==F("stamac")){ - p = renderInfoRow(F("Station MAC"), WiFi.macAddress()); - } - else if(id==F("conx")){ - p = renderInfoRow(F("Connected"), WiFi.isConnected() ? F("Yes") : F("No")); - } - #ifdef ESP8266 - else if(id==F("autoconx")){ - p = renderInfoRow(F("Autoconnect"), WiFi.getAutoConnect() ? F("Enabled") : F("Disabled")); - } - #endif - #if defined(ESP32) && !defined(WM_NOTEMP) - else if(id==F("temp")){ - p = renderInfoRow(F("Temperature"), String(temperatureRead()) + F(" C° / ") + String((temperatureRead()+32)*1.8f) + F(" F°")); - } - #endif - else if(id==F("aboutver")){ - p = renderInfoRow(F("WiFiManager"), FPSTR(WM_VERSION_STR)); - } - else if(id==F("aboutarduinover")){ - #ifdef VER_ARDUINO_STR - p = renderInfoRow(F("Arduino"), String(VER_ARDUINO_STR)); - #endif - } - else if(id==F("aboutsdkver")){ - #ifdef ESP32 - p = renderInfoRow(F("SDK version"), String(esp_get_idf_version())); - #else - p = renderInfoRow(F("SDK version"), String(system_get_sdk_version())); - #endif - } - else if(id==F("aboutdate")){ - p = renderInfoRow(F("Build date"), String(__DATE__ " " __TIME__)); - } - return p; -} - -void WiFiManagerHandlers::reportStatus(String &page){ - _wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem,F("[WIFI] reportStatus prev:"),_wm->getWLStatusString(_wm->_lastconxresult)); - _wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem,F("[WIFI] reportStatus current:"),_wm->getWLStatusString(WiFi.status())); - String str; - if (_wm->WiFi_SSID() != ""){ - if (WiFi.status()==WL_CONNECTED){ - str = renderStatusMessage( - F("Connected"), - String(F(" to ")) + _wm->htmlEntities(_wm->WiFi_SSID(), true) + F("
with IP ") + WiFi.localIP().toString() + F(""), - F(" S") - ); - } - else { - String ssidEncoded = _wm->htmlEntities(_wm->WiFi_SSID(), true); - String statusClass = " D"; - String statusMsg = ""; - - if(_wm->_lastconxresult == _wm->WL_STATION_WRONG_PASSWORD){ - statusMsg = FPSTR(HTML_STATUS_OFFPW); - } - else if(_wm->_lastconxresult == WL_NO_SSID_AVAIL){ - statusMsg = FPSTR(HTML_STATUS_OFFNOAP); - } - else if(_wm->_lastconxresult == WL_CONNECT_FAILED || _wm->_lastconxresult == WL_CONNECTION_LOST){ - statusMsg = FPSTR(HTML_STATUS_OFFFAIL); - } - else{ - statusClass = ""; - } - - str = renderStatusMessage(F("Not connected"), String(F(" to ")) + ssidEncoded + statusMsg, statusClass); + json += F("}"); + } else { + json += F("{\"html\":\""); + jsonAppendEscaped(json, String(p->getCustomHTML())); + json += F("}"); } } - else { - str = FPSTR(HTML_STATUS_NONE); - } - page += str; } +void WiFiManagerHandlers::appendJsonKvItem(String& json, bool& first, const char* key, const String& label, + const String& value) { + if (!first) { + json += F(","); + } + first = false; + json += F("{\"key\":\""); + json += key; + json += F("\",\"label\":\""); + jsonAppendEscaped(json, label); + json += F("\",\"value\":\""); + jsonAppendEscaped(json, value); + json += F("}"); +} + +void WiFiManagerHandlers::appendInfoSectionFromIds(String& json, const char* const* ids, size_t count, bool& first) { + for (size_t i = 0; i < count; i++) { + appendOneInfoItemForId(json, first, ids[i]); + } +} + +void WiFiManagerHandlers::appendOneInfoItemForId(String& json, bool& first, const char* id) { + if (strcmp(id, "uptime") == 0) { + appendJsonKvItem(json, first, id, String(F("Uptime")), + String(millis() / 1000 / 60) + F(" mins ") + String((millis() / 1000) % 60) + F(" secs")); + return; + } + if (strcmp(id, "chipid") == 0) { +#ifdef ESP8266 + appendJsonKvItem(json, first, id, String(F("Chip ID")), String(ESP.getChipId(), HEX)); +#elif defined(ESP32) + appendJsonKvItem(json, first, id, String(F("Chip ID")), String((uint32_t)ESP.getEfuseMac(), HEX)); +#endif + return; + } +#ifdef ESP32 + if (strcmp(id, "chiprev") == 0) { + String rev = (String)ESP.getChipRevision(); +#ifdef _SOC_EFUSE_REG_H_ + String revb = + (String)(REG_READ(EFUSE_BLK0_RDATA3_REG) >> (EFUSE_RD_CHIP_VER_RESERVE_S)&EFUSE_RD_CHIP_VER_RESERVE_V); + appendJsonKvItem(json, first, id, String(F("Chip rev")), rev + F(" / ") + revb); +#else + appendJsonKvItem(json, first, id, String(F("Chip rev")), rev); +#endif + return; + } +#endif +#ifdef ESP8266 + if (strcmp(id, "fchipid") == 0) { + appendJsonKvItem(json, first, id, String(F("Flash chip ID")), String(ESP.getFlashChipId())); + return; + } +#endif + if (strcmp(id, "idesize") == 0) { + appendJsonKvItem(json, first, id, String(F("Flash size")), String(ESP.getFlashChipSize()) + F(" bytes")); + return; + } + if (strcmp(id, "flashsize") == 0) { +#ifdef ESP8266 + appendJsonKvItem(json, first, id, String(F("Real flash size")), String(ESP.getFlashChipRealSize()) + F(" bytes")); +#elif defined(ESP32) + appendJsonKvItem(json, first, id, String(F("PSRAM Size")), String(ESP.getPsramSize()) + F(" bytes")); +#endif + return; + } + if (strcmp(id, "corever") == 0) { +#ifdef ESP8266 + appendJsonKvItem(json, first, id, String(F("Core version")), String(ESP.getCoreVersion())); +#endif + return; + } +#ifdef ESP8266 + if (strcmp(id, "bootver") == 0) { + appendJsonKvItem(json, first, id, String(F("Boot version")), String(system_get_boot_version())); + return; + } +#endif + if (strcmp(id, "cpufreq") == 0) { + appendJsonKvItem(json, first, id, String(F("CPU frequency")), String(ESP.getCpuFreqMHz()) + F("MHz")); + return; + } + if (strcmp(id, "freeheap") == 0) { + appendJsonKvItem(json, first, id, String(F("Memory - Free heap")), String(ESP.getFreeHeap()) + F(" bytes available")); + return; + } + if (strcmp(id, "memsketch") == 0) { + appendJsonKvItem(json, first, id, String(F("Memory - Sketch size")), + String(ESP.getSketchSize()) + F(" / ") + String(ESP.getSketchSize() + ESP.getFreeSketchSpace()) + + F(" bytes")); + return; + } + if (strcmp(id, "memsmeter") == 0) { + appendJsonKvItem(json, first, id, String(F("Sketch usage (used / max)")), + String(ESP.getSketchSize()) + F(" / ") + String(ESP.getSketchSize() + ESP.getFreeSketchSpace())); + return; + } + if (strcmp(id, "lastreset") == 0) { +#ifdef ESP8266 + appendJsonKvItem(json, first, id, String(F("Last reset reason")), String(ESP.getResetReason())); +#elif defined(ESP32) && defined(_ROM_RTC_H_) + String reasons[2]; + for (int i = 0; i < 2; i++) { + int reason = rtc_get_reset_reason(i); + switch (reason) { + case 1: + reasons[i] = F("Vbat power on reset"); + break; + case 3: + reasons[i] = F("Software reset digital core"); + break; + case 4: + reasons[i] = F("Legacy watch dog reset digital core"); + break; + case 5: + reasons[i] = F("Deep Sleep reset digital core"); + break; + case 6: + reasons[i] = F("Reset by SLC module, reset digital core"); + break; + case 7: + reasons[i] = F("Timer Group0 Watch dog reset digital core"); + break; + case 8: + reasons[i] = F("Timer Group1 Watch dog reset digital core"); + break; + case 9: + reasons[i] = F("RTC Watch dog Reset digital core"); + break; + case 10: + reasons[i] = F("Instrusion tested to reset CPU"); + break; + case 11: + reasons[i] = F("Time Group reset CPU"); + break; + case 12: + reasons[i] = F("Software reset CPU"); + break; + case 13: + reasons[i] = F("RTC Watch dog Reset CPU"); + break; + case 14: + reasons[i] = F("for APP CPU, reseted by PRO CPU"); + break; + case 15: + reasons[i] = F("Reset when the vdd voltage is not stable"); + break; + case 16: + reasons[i] = F("RTC Watch dog reset digital core and rtc module"); + break; + default: + reasons[i] = F("NO_MEAN"); + } + } + appendJsonKvItem(json, first, id, String(F("Last reset reason")), + String(F("CPU0: ")) + reasons[0] + F(" — CPU1: ") + reasons[1]); +#endif + return; + } + if (strcmp(id, "apip") == 0) { + appendJsonKvItem(json, first, id, String(F("Access point IP")), WiFi.softAPIP().toString()); + return; + } + if (strcmp(id, "apmac") == 0) { + appendJsonKvItem(json, first, id, String(F("Access point MAC")), WiFi.softAPmacAddress()); + return; + } +#ifdef ESP32 + if (strcmp(id, "aphost") == 0) { + appendJsonKvItem(json, first, id, String(F("Access point hostname")), String(WiFi.softAPgetHostname())); + return; + } +#endif +#ifndef WM_NOSOFTAPSSID +#ifdef ESP8266 + if (strcmp(id, "apssid") == 0) { + appendJsonKvItem(json, first, id, String(F("Access point SSID")), WiFi.softAPSSID()); + return; + } +#endif +#endif + if (strcmp(id, "apbssid") == 0) { + appendJsonKvItem(json, first, id, String(F("BSSID")), WiFi.BSSIDstr()); + return; + } + if (strcmp(id, "stassid") == 0) { + appendJsonKvItem(json, first, id, String(F("Station SSID")), (String)_wm->WiFi_SSID()); + return; + } + if (strcmp(id, "staip") == 0) { + appendJsonKvItem(json, first, id, String(F("Station IP")), WiFi.localIP().toString()); + return; + } + if (strcmp(id, "stagw") == 0) { + appendJsonKvItem(json, first, id, String(F("Station gateway")), WiFi.gatewayIP().toString()); + return; + } + if (strcmp(id, "stasub") == 0) { + appendJsonKvItem(json, first, id, String(F("Station subnet")), WiFi.subnetMask().toString()); + return; + } + if (strcmp(id, "dnss") == 0) { + appendJsonKvItem(json, first, id, String(F("DNS Server")), WiFi.dnsIP().toString()); + return; + } + if (strcmp(id, "host") == 0) { +#ifdef ESP32 + appendJsonKvItem(json, first, id, String(F("Hostname")), String(WiFi.getHostname())); +#else + appendJsonKvItem(json, first, id, String(F("Hostname")), String(WiFi.hostname())); +#endif + return; + } + if (strcmp(id, "stamac") == 0) { + appendJsonKvItem(json, first, id, String(F("Station MAC")), WiFi.macAddress()); + return; + } + if (strcmp(id, "conx") == 0) { + appendJsonKvItem(json, first, id, String(F("Connected")), WiFi.isConnected() ? String(F("Yes")) : String(F("No"))); + return; + } +#ifdef ESP8266 + if (strcmp(id, "autoconx") == 0) { + appendJsonKvItem(json, first, id, String(F("Autoconnect")), + WiFi.getAutoConnect() ? String(F("Enabled")) : String(F("Disabled"))); + return; + } +#endif +#if defined(ESP32) && !defined(WM_NOTEMP) + if (strcmp(id, "temp") == 0) { + appendJsonKvItem(json, first, id, String(F("Temperature")), + String(temperatureRead()) + F(" C / ") + String((temperatureRead() + 32) * 1.8f) + F(" F")); + return; + } +#endif + if (strcmp(id, "aboutver") == 0) { + appendJsonKvItem(json, first, id, String(F("WiFiManager")), String(FPSTR(WM_VERSION_STR))); + return; + } + if (strcmp(id, "aboutarduinover") == 0) { +#ifdef VER_ARDUINO_STR + appendJsonKvItem(json, first, id, String(F("Arduino")), String(VER_ARDUINO_STR)); +#endif + return; + } + if (strcmp(id, "aboutsdkver") == 0) { +#ifdef ESP32 + appendJsonKvItem(json, first, id, String(F("SDK version")), String(esp_get_idf_version())); +#else + appendJsonKvItem(json, first, id, String(F("SDK version")), String(system_get_sdk_version())); +#endif + return; + } + if (strcmp(id, "aboutdate") == 0) { + appendJsonKvItem(json, first, id, String(F("Build date")), String(__DATE__ " " __TIME__)); + return; + } +} + + // Captive Portal +bool WiFiManagerHandlers::shouldRedirectCaptiveForHost(const String& requestHost, const String& serverLocWithPort) { + return serverLocWithPort.length() > 0 && requestHost != serverLocWithPort; +} + boolean WiFiManagerHandlers::captivePortal(AsyncWebServerRequest *request) { if(!_wm->_enableCaptivePortal || !_wm->configPortalActive) return false; @@ -1205,7 +508,7 @@ boolean WiFiManagerHandlers::captivePortal(AsyncWebServerRequest *request) { } if(_wm->_httpPort != 80) serverLoc += ":" + (String)_wm->_httpPort; - bool doredirect = serverLoc != request->host(); + bool doredirect = shouldRedirectCaptiveForHost(request->host(), serverLoc); if (doredirect) { #ifndef WM_NO_LOG @@ -1254,7 +557,6 @@ void WiFiManagerHandlers::handleRoot(AsyncWebServerRequest *request) { _wm->_serverManager->registerDefaultStyles(bundle->registry); _wm->_serverManager->registerDefaultScripts(bundle->registry); _wm->_serverManager->registerDefaultPageTitle(bundle->registry); - _wm->_serverManager->applyTemplateSetupCallback(bundle->registry); } else { registerSharedShellPlaceholders(nullptr, bundle->registry); bundle->pageTitleStatic = _wm->_title; @@ -1269,30 +571,6 @@ void WiFiManagerHandlers::handleRoot(AsyncWebServerRequest *request) { request->send(beginTemplateResponse(request, bundle, WM_ROOT_SHELL_TEMPLATE)); } -void WiFiManagerHandlers::handleWifi(AsyncWebServerRequest *request, boolean scan) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- HTTP Wifi")); - _wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem, F("handleWifi called, scan="), scan ? "true" : "false"); - #endif - (void)scan; - if (captivePortal(request)) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem, F("Captive portal redirect")); - #endif - return; - } - handleRequest(request); - request->redirect(String(F("/#/wifi"))); -} - -void WiFiManagerHandlers::handleParam(AsyncWebServerRequest *request){ - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- HTTP Param")); - #endif - handleRequest(request); - request->redirect(String(F("/#/setup"))); -} - void WiFiManagerHandlers::applyWifiAndParamsFromRequest(AsyncWebServerRequest *request) { WiFiManager::WiFiManagerRequestArgs requestArgs(request); @@ -1365,79 +643,6 @@ void WiFiManagerHandlers::applyWifiAndParamsFromRequest(AsyncWebServerRequest *r } } -void WiFiManagerHandlers::handleWifiSave(AsyncWebServerRequest *request) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- HTTP WiFi save ")); - _wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem, F("Method:"), request->method() == HTTP_GET ? F("GET") : F("POST")); - #endif - handleRequest(request); - - applyWifiAndParamsFromRequest(request); - - auto bundle = std::make_shared(); - String messageBody; - String actions; - reservePage(messageBody, 1024); - - if(_wm->_ssid == ""){ - messageBody += FPSTR(HTML_PARAMSAVED); - } - else { - messageBody += FPSTR(HTML_SAVED); - } - - if(_wm->_showBack) actions += renderActionForm(F("/"), F("get"), F("Back"), String(), F("

")); - - sendMessageTemplateResponse(_wm->_serverManager.get(), - bundle, - _wm->_ssid == "" ? F("Settings saved") : F("Credentials saved"), - C_wifi, - messageBody, - actions); - - AsyncWebServerResponse *response = beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE); - response->addHeader(FPSTR(HTTP_HEAD_CORS), FPSTR(HTTP_HEAD_CORS_ALLOW_ALL)); - request->send(response); - - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem, F("Sent wifi save page")); - #endif - - _wm->connect = true; -} - -void WiFiManagerHandlers::handleParamSave(AsyncWebServerRequest *request) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- HTTP Param save ")); - _wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem, F("Method:"), request->method() == HTTP_GET ? F("GET") : F("POST")); - #endif - handleRequest(request); - - WiFiManager::WiFiManagerRequestArgs requestArgs(request); - - doParamSave(requestArgs); - - auto bundle = std::make_shared(); - String messageBody; - String actions; - reservePage(messageBody, 1024); - messageBody += FPSTR(HTML_PARAMSAVED); - if(_wm->_showBack) actions += renderActionForm(F("/"), F("get"), F("Back"), String(), F("

")); - - sendMessageTemplateResponse(_wm->_serverManager.get(), - bundle, - F("Setup saved"), - C_param, - messageBody, - actions); - - request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); - - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem, F("Sent param save page")); - #endif -} - void WiFiManagerHandlers::doParamSave(WiFiManager::WiFiManagerRequestArgs requestArgs){ if ( _wm->_presaveparamscallback != NULL) { _wm->_presaveparamscallback(); @@ -1492,107 +697,6 @@ void WiFiManagerHandlers::doParamSave(WiFiManager::WiFiManagerRequestArgs reques } -void WiFiManagerHandlers::handleInfo(AsyncWebServerRequest *request) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- HTTP Info")); - #endif - handleRequest(request); - request->redirect(String(F("/#/info"))); -} - -void WiFiManagerHandlers::handleExit(AsyncWebServerRequest *request) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- HTTP Exit")); - #endif - handleRequest(request); - auto bundle = std::make_shared(); - sendMessageTemplateResponse(_wm->_serverManager.get(), - bundle, - F("Exit"), - C_exit, - String(F("Exiting"))); - - AsyncWebServerResponse *response = beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE); - response->addHeader(F("Cache-Control"), F("no-cache, no-store, must-revalidate")); - request->send(response); - - _wm->_abortScheduled = true; - _wm->_abortTime = millis() + _wm->EXIT_DELAY_MS; -} - -void WiFiManagerHandlers::handleReset(AsyncWebServerRequest *request) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- HTTP Reset")); - #endif - handleRequest(request); - auto bundle = std::make_shared(); - sendMessageTemplateResponse(_wm->_serverManager.get(), - bundle, - F("Reset"), - C_restart, - String(F("Module will reset in a few seconds."))); - - request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); - - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, F("RESETTING ESP")); - #endif - _wm->_rebootScheduled = true; - _wm->_rebootTime = millis() + _wm->REBOOT_DELAY_MS; -} - -void WiFiManagerHandlers::handleErase(AsyncWebServerRequest *request, boolean opt) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, F("<- HTTP Erase")); - #endif - handleRequest(request); - bool ret = _wm->erase(opt); - auto bundle = std::make_shared(); - String messageBody; - reservePage(messageBody, 256); - - if(ret) messageBody += F("Module will reset in a few seconds."); - else { - messageBody += F("An error occured"); - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Error, kWiFiMgrLogSubsystem, F("[ERROR] WiFi EraseConfig failed")); - #endif - } - - sendMessageTemplateResponse(_wm->_serverManager.get(), - bundle, - F("Erase"), - C_erase, - messageBody); - - request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); - - if(ret){ - _wm->_rebootScheduled = true; - _wm->_rebootTime = millis() + _wm->ERASE_REBOOT_DELAY_MS; - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, F("RESETTING ESP")); - #endif - } -} - -void WiFiManagerHandlers::handleClose(AsyncWebServerRequest *request){ - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("Disabling Captive Portal")); - stopCaptivePortal(); - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- HTTP close")); - #endif - handleRequest(request); - auto bundle = std::make_shared(); - sendMessageTemplateResponse(_wm->_serverManager.get(), - bundle, - F("Close"), - C_close, - String(F("You can close the page, portal will continue to run"))); - - request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); -} - void WiFiManagerHandlers::handleNotFound(AsyncWebServerRequest *request) { if (captivePortal(request)) return; handleRequest(request); @@ -1604,49 +708,6 @@ void WiFiManagerHandlers::handleNotFound(AsyncWebServerRequest *request) { request->send(response); } -void WiFiManagerHandlers::handleWiFiScanStatus(AsyncWebServerRequest *request){ - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- HTTP WiFi scan status")); - #endif - handleRequest(request); - - std::vector networks; - collectVisibleScanResults(networks); - - String json = "{"; - reservePage(json, 160 + (networks.size() > 0 ? networks.size() * 96 : 64)); - json += "\"state\":\""; - switch (_wm->_scan.state) { - case WiFiManager::WM_SCAN_IDLE: json += "idle"; break; - case WiFiManager::WM_SCAN_QUEUED: json += "queued"; break; - case WiFiManager::WM_SCAN_RUNNING: json += "running"; break; - case WiFiManager::WM_SCAN_COMPLETE: json += "complete"; break; - case WiFiManager::WM_SCAN_FAILED: json += "failed"; break; - case WiFiManager::WM_SCAN_TIMEOUT: json += "timeout"; break; - } - json += "\",\"scanning\":"; - json += (_wm->_scan.state == WiFiManager::WM_SCAN_RUNNING || _wm->_scan.state == WiFiManager::WM_SCAN_QUEUED) ? "true" : "false"; - json += ",\"results_valid\":"; - json += _wm->_scan.resultsValid ? "true" : "false"; - json += ",\"count\":"; - json += String(networks.size()); - json += ",\"lastscan\":"; - json += String(_wm->_lastscan); - json += ",\"error\":\""; - if (_wm->_scan.state == WiFiManager::WM_SCAN_TIMEOUT) { - json += "timeout"; - } else if (_wm->_scan.state == WiFiManager::WM_SCAN_FAILED) { - json += "failed"; - } - json += "\",\"networks\":["; - appendVisibleScanResultsJson(json, networks); - json += "]}"; - - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", json); - response->addHeader(F("Cache-Control"), F("no-cache")); - request->send(response); -} - void WiFiManagerHandlers::appendVisibleScanResultsJson(String& json, const std::vector& networks) { for (size_t i = 0; i < networks.size(); i++) { const auto* network = networks[i]; @@ -1655,7 +716,7 @@ void WiFiManagerHandlers::appendVisibleScanResultsJson(String& json, const std:: } json += "{"; json += "\"ssid\":\""; - json += _wm->htmlEntities(network->ssid, true); + jsonAppendEscaped(json, network->ssid); json += "\",\"rssi\":"; json += String(network->rssi); json += ",\"quality\":"; @@ -1668,28 +729,6 @@ void WiFiManagerHandlers::appendVisibleScanResultsJson(String& json, const std:: } } -void WiFiManagerHandlers::handleWiFiScanRequest(AsyncWebServerRequest *request){ - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- HTTP WiFi scan request")); - #endif - handleRequest(request); - _wm->requestAsyncScan(true); - - AsyncWebServerResponse *response = request->beginResponse(202, "application/json", - "{\"accepted\":true,\"state\":\"queued\"}"); - response->addHeader(F("Cache-Control"), F("no-cache")); - request->send(response); -} - -void WiFiManagerHandlers::handleUpdate(AsyncWebServerRequest *request) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- Handle update")); - #endif - if (captivePortal(request)) return; - handleRequest(request); - request->redirect(String(F("/#/update"))); -} - void WiFiManagerHandlers::handleUpdating(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { static unsigned long _configPortalTimeoutSAV = 0; static bool timeoutSaved = false; @@ -1757,25 +796,26 @@ void WiFiManagerHandlers::handleUpdating(AsyncWebServerRequest *request, String } void WiFiManagerHandlers::handleUpdateDone(AsyncWebServerRequest *request) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- Handle update done")); - #endif +#ifndef WM_NO_LOG + _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- Handle update done (POST /u)")); +#endif handleRequest(request); - auto bundle = std::make_shared(); - const String subtitle = _wm->configPortalActive ? _wm->_apName : WiFi.localIP().toString(); - sendMessageTemplateResponse(_wm->_serverManager.get(), - bundle, - F("Update"), - C_update, - buildUpdateResultContent(_wm->_title, subtitle)); - - request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); - - if (!Update.hasError()) { - delay(1000); - ESP.restart(); + if (Update.hasError()) { + String json = F("{\"ok\":false,\"message\":\"Update failed\",\"detail\":\""); +#ifdef ESP32 + jsonAppendEscaped(json, String(Update.errorString())); +#else + jsonAppendEscaped(json, String(Update.getError())); +#endif + json += F("\"}"); + sendApiJson(request, 500, json); + return; } + + sendApiJson(request, 200, jsonApiOtaUpdateSuccess()); + delay(1000); + ESP.restart(); } void WiFiManagerHandlers::sendApiJson(AsyncWebServerRequest *request, int code, const String& json) { @@ -1785,6 +825,40 @@ void WiFiManagerHandlers::sendApiJson(AsyncWebServerRequest *request, int code, request->send(response); } +void WiFiManagerHandlers::appendPortalUiFeatureFlagsJson(String& json) { + const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive; + json += F("\"showInfo\":"); + json += _wm->_showInfo ? F("true") : F("false"); + json += F(",\"showUpdate\":"); + json += _wm->_showInfoUpdate ? F("true") : F("false"); + json += F(",\"showErase\":"); + json += _wm->_showInfoErase ? F("true") : F("false"); + json += F(",\"paramsInWifi\":"); + json += _wm->_paramsInWifi ? F("true") : F("false"); + json += F(",\"showRestart\":"); + json += portalRunning ? F("true") : F("false"); + json += F(",\"showExitPortal\":"); + json += (_wm->_allowExit && portalRunning) ? F("true") : F("false"); + json += F(",\"showCloseCaptive\":"); + json += (_wm->_enableCaptivePortal && _wm->configPortalActive) ? F("true") : F("false"); +} + +void WiFiManagerHandlers::appendApiInfoActionsJson(String& json) { + const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive; + json += F("\"showUpdate\":"); + json += _wm->_showInfoUpdate ? F("true") : F("false"); + json += F(",\"showErase\":"); + json += _wm->_showInfoErase ? F("true") : F("false"); + json += F(",\"showBack\":"); + json += _wm->_showBack ? F("true") : F("false"); + json += F(",\"showRestart\":"); + json += portalRunning ? F("true") : F("false"); + json += F(",\"showExitPortal\":"); + json += (_wm->_allowExit && portalRunning) ? F("true") : F("false"); + json += F(",\"showCloseCaptive\":"); + json += (_wm->_enableCaptivePortal && _wm->configPortalActive) ? F("true") : F("false"); +} + String WiFiManagerHandlers::buildPortalBootstrapJson() { std::vector networks; collectVisibleScanResults(networks); @@ -1830,19 +904,20 @@ String WiFiManagerHandlers::buildPortalBootstrapJson() { json += F("\",\"portalActive\":"); json += _wm->configPortalActive ? F("true") : F("false"); json += F(",\"features\":{"); - json += F("\"showInfo\":true,\"showUpdate\":"); - json += _wm->_showInfoUpdate ? F("true") : F("false"); - json += F(",\"showErase\":"); - json += _wm->_showInfoErase ? F("true") : F("false"); - json += F(",\"paramsInWifi\":"); - json += _wm->_paramsInWifi ? F("true") : F("false"); + appendPortalUiFeatureFlagsJson(json); json += F("},\"showBack\":"); json += _wm->_showBack ? F("true") : F("false"); json += F(",\"scan\":{\"state\":\""); json += stateStr; json += F("\",\"count\":"); json += String((unsigned int)networks.size()); - json += F("}}"); + json += F("},\"initialStatus\":\""); + { + String st; + buildPlainStatusSummary(st); + jsonAppendEscaped(json, st); + } + json += F("\"}"); return json; } @@ -1876,15 +951,76 @@ void WiFiManagerHandlers::handleApiBootstrap(AsyncWebServerRequest *request) { } void WiFiManagerHandlers::handleApiWifiScanStatus(AsyncWebServerRequest *request) { - handleWiFiScanStatus(request); +#ifndef WM_NO_LOG + _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- API WiFi scan status")); +#endif + handleRequest(request); + + std::vector networks; + collectVisibleScanResults(networks); + + String json = "{"; + reservePage(json, 160 + (networks.size() > 0 ? networks.size() * 96 : 64)); + json += "\"state\":\""; + switch (_wm->_scan.state) { + case WiFiManager::WM_SCAN_IDLE: + json += "idle"; + break; + case WiFiManager::WM_SCAN_QUEUED: + json += "queued"; + break; + case WiFiManager::WM_SCAN_RUNNING: + json += "running"; + break; + case WiFiManager::WM_SCAN_COMPLETE: + json += "complete"; + break; + case WiFiManager::WM_SCAN_FAILED: + json += "failed"; + break; + case WiFiManager::WM_SCAN_TIMEOUT: + json += "timeout"; + break; + } + json += "\",\"scanning\":"; + json += (_wm->_scan.state == WiFiManager::WM_SCAN_RUNNING || _wm->_scan.state == WiFiManager::WM_SCAN_QUEUED) + ? "true" + : "false"; + json += ",\"results_valid\":"; + json += _wm->_scan.resultsValid ? "true" : "false"; + json += ",\"count\":"; + json += String(networks.size()); + json += ",\"lastscan\":"; + json += String(_wm->_lastscan); + json += ",\"error\":\""; + if (_wm->_scan.state == WiFiManager::WM_SCAN_TIMEOUT) { + json += "timeout"; + } else if (_wm->_scan.state == WiFiManager::WM_SCAN_FAILED) { + json += "failed"; + } + json += "\",\"networks\":["; + appendVisibleScanResultsJson(json, networks); + json += "]}"; + + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", json); + response->addHeader(F("Cache-Control"), F("no-cache")); + request->send(response); } void WiFiManagerHandlers::handleApiWifiScan(AsyncWebServerRequest *request) { - handleWiFiScanRequest(request); +#ifndef WM_NO_LOG + _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- API WiFi scan")); +#endif + handleRequest(request); + _wm->requestAsyncScan(true); + + AsyncWebServerResponse *response = + request->beginResponse(202, "application/json", "{\"accepted\":true,\"state\":\"queued\"}"); + response->addHeader(F("Cache-Control"), F("no-cache")); + request->send(response); } -void WiFiManagerHandlers::handleApiWifiMeta(AsyncWebServerRequest *request) { - handleRequest(request); +String WiFiManagerHandlers::buildApiWifiMetaJson() { String ssidPlaceholder = _wm->WiFi_SSID(); String passwordPlaceholder = ""; if (_wm->_showPassword) { @@ -1892,26 +1028,34 @@ void WiFiManagerHandlers::handleApiWifiMeta(AsyncWebServerRequest *request) { } else if (_wm->WiFi_psk() != "") { passwordPlaceholder = F("********"); } - String staticHtml = getStaticOut(); - String paramsHtml; - if (_wm->_paramsInWifi && _wm->_paramsCount > 0) { - paramsHtml = renderSectionBreak(getParamOut()); - } - String json = F("{\"ssidPlaceholder\":\""); + + String json = F("{\"wifiFields\":["); + json += F("{\"id\":\"s\",\"name\":\"s\",\"label\":\"SSID\",\"type\":\"text\",\"maxlength\":32,\"value\":\""); jsonAppendEscaped(json, ssidPlaceholder); - json += F("\",\"passwordPlaceholder\":\""); + json += F("\"},{\"id\":\"p\",\"name\":\"p\",\"label\":\"Password\",\"type\":\"password\",\"maxlength\":64,\"placeholder\":\""); jsonAppendEscaped(json, passwordPlaceholder); - json += F("\",\"staticFieldsHtml\":\""); - jsonAppendEscaped(json, staticHtml); - json += F("\",\"paramsHtml\":\""); - jsonAppendEscaped(json, paramsHtml); - json += F("\"}"); - sendApiJson(request, 200, json); + json += F("\"}],\"staticFields\":["); + bool first = true; + appendPortalJsonStaticFields(json, first); + json += F("],\"params\":["); + first = true; + if (_wm->_paramsInWifi && _wm->getParametersCount() > 0) { + appendPortalJsonCustomParams(json, first); + } + json += F("],\"actions\":{\"canRefreshScan\":true,\"showBack\":"); + json += _wm->_showBack ? F("true") : F("false"); + json += F("}}"); + return json; +} + +void WiFiManagerHandlers::handleApiWifiMeta(AsyncWebServerRequest *request) { + handleRequest(request); + sendApiJson(request, 200, buildApiWifiMetaJson()); } void WiFiManagerHandlers::handleApiWifiSave(AsyncWebServerRequest *request) { #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- API WiFi save")); + _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("<- WiFi save")); #endif handleRequest(request); applyWifiAndParamsFromRequest(request); @@ -1926,13 +1070,19 @@ void WiFiManagerHandlers::handleApiWifiSave(AsyncWebServerRequest *request) { _wm->connect = true; } +String WiFiManagerHandlers::buildApiParamsGetJson() { + String json = F("{\"params\":["); + bool first = true; + appendPortalJsonCustomParams(json, first); + json += F("],\"actions\":{\"showBack\":"); + json += _wm->_showBack ? F("true") : F("false"); + json += F("}}"); + return json; +} + void WiFiManagerHandlers::handleApiParamsGet(AsyncWebServerRequest *request) { handleRequest(request); - String fields = getParamOut(); - String json = F("{\"paramsHtml\":\""); - jsonAppendEscaped(json, fields); - json += F("\"}"); - sendApiJson(request, 200, json); + sendApiJson(request, 200, buildApiParamsGetJson()); } void WiFiManagerHandlers::handleApiParamsSave(AsyncWebServerRequest *request) { @@ -1942,16 +1092,25 @@ void WiFiManagerHandlers::handleApiParamsSave(AsyncWebServerRequest *request) { handleRequest(request); WiFiManager::WiFiManagerRequestArgs requestArgs(request); doParamSave(requestArgs); - sendApiJson(request, 200, F("{\"ok\":true,\"message\":\"Setup saved\"}")); + sendApiJson(request, 200, jsonApiParamsSaveOk()); } -void WiFiManagerHandlers::handleApiInfo(AsyncWebServerRequest *request) { - handleRequest(request); - String summary; - buildPlainStatusSummary(summary); - String json = F("{\"status\":[{\"key\":\"summary\",\"label\":\"Status\",\"value\":\""); - jsonAppendEscaped(json, summary); - json += F("\"}],\"device\":["); +String WiFiManagerHandlers::buildApiInfoJson() { + String json = F("{\"status\":{\"connected\":"); + json += WiFi.isConnected() ? F("true") : F("false"); + json += F(",\"ssid\":\""); + jsonAppendEscaped(json, _wm->WiFi_SSID()); + json += F("\",\"stationIp\":\""); + jsonAppendEscaped(json, WiFi.localIP().toString()); + json += F("\",\"apIp\":\""); + jsonAppendEscaped(json, WiFi.softAPIP().toString()); + json += F("\",\"summary\":\""); + { + String summary; + buildPlainStatusSummary(summary); + jsonAppendEscaped(json, summary); + } + json += F("\"},\"device\":["); bool first = true; #ifdef ESP8266 static const char *const deviceIds[] = {"uptime", "chipid", "fchipid", "idesize", "flashsize", @@ -1961,7 +1120,7 @@ void WiFiManagerHandlers::handleApiInfo(AsyncWebServerRequest *request) { static const char *const deviceIds[] = {"uptime", "chipid", "chiprev", "idesize", "flashsize", "cpufreq", "freeheap", "memsketch", "memsmeter", "lastreset", "temp"}; #endif - appendJsonInfoArrayFromIds(this, json, deviceIds, sizeof(deviceIds) / sizeof(deviceIds[0]), first); + appendInfoSectionFromIds(json, deviceIds, sizeof(deviceIds) / sizeof(deviceIds[0]), first); json += F("],\"wifi\":["); first = true; #ifdef ESP8266 @@ -1971,30 +1130,65 @@ void WiFiManagerHandlers::handleApiInfo(AsyncWebServerRequest *request) { static const char *const wifiIds[] = {"conx", "stassid", "staip", "stagw", "stasub", "dnss", "host", "stamac", "apssid", "apip", "apmac", "aphost", "apbssid"}; #endif - appendJsonInfoArrayFromIds(this, json, wifiIds, sizeof(wifiIds) / sizeof(wifiIds[0]), first); + appendInfoSectionFromIds(json, wifiIds, sizeof(wifiIds) / sizeof(wifiIds[0]), first); json += F("],\"about\":["); first = true; static const char *const aboutIds[] = {"aboutver", "aboutarduinover", "aboutsdkver", "aboutdate"}; - appendJsonInfoArrayFromIds(this, json, aboutIds, sizeof(aboutIds) / sizeof(aboutIds[0]), first); + appendInfoSectionFromIds(json, aboutIds, sizeof(aboutIds) / sizeof(aboutIds[0]), first); json += F("],\"actions\":{"); - json += F("\"showUpdate\":"); - json += _wm->_showInfoUpdate ? F("true") : F("false"); - json += F(",\"showErase\":"); - json += _wm->_showInfoErase ? F("true") : F("false"); - json += F(",\"showBack\":"); - json += _wm->_showBack ? F("true") : F("false"); + appendApiInfoActionsJson(json); json += F("}}"); - sendApiJson(request, 200, json); + return json; } -void WiFiManagerHandlers::handleApiStatus(AsyncWebServerRequest *request) { +void WiFiManagerHandlers::handleApiInfo(AsyncWebServerRequest *request) { handleRequest(request); + sendApiJson(request, 200, buildApiInfoJson()); +} + +String WiFiManagerHandlers::buildApiStatusJson() { String summary; buildPlainStatusSummary(summary); String json = F("{\"text\":\""); jsonAppendEscaped(json, summary); json += F("\"}"); - sendApiJson(request, 200, json); + return json; +} + +void WiFiManagerHandlers::handleApiStatus(AsyncWebServerRequest *request) { + handleRequest(request); + sendApiJson(request, 200, buildApiStatusJson()); +} + +String WiFiManagerHandlers::jsonApiDeviceRestartScheduled() { + return F("{\"ok\":true,\"message\":\"Restart scheduled\"}"); +} + +String WiFiManagerHandlers::jsonApiParamsSaveOk() { + return F("{\"ok\":true,\"message\":\"Setup saved\"}"); +} + +String WiFiManagerHandlers::jsonApiPortalCloseOk() { + return F("{\"ok\":true,\"message\":\"Captive portal detection disabled\"}"); +} + +String WiFiManagerHandlers::jsonApiPortalExitOk() { + return F("{\"ok\":true,\"message\":\"Exiting portal\"}"); +} + +String WiFiManagerHandlers::jsonApiPortalExitForbidden() { + return F("{\"ok\":false,\"message\":\"Exit not allowed\"}"); +} + +String WiFiManagerHandlers::jsonApiOtaUpdateSuccess() { + return F("{\"ok\":true,\"message\":\"Firmware updated. Restarting...\"}"); +} + +String WiFiManagerHandlers::jsonApiEraseResponse(boolean success) { + if (success) { + return F("{\"ok\":true,\"message\":\"WiFi configuration erased. Device will restart shortly.\"}"); + } + return F("{\"ok\":false,\"message\":\"Erase failed\"}"); } void WiFiManagerHandlers::handleApiDeviceRestart(AsyncWebServerRequest *request) { @@ -2002,27 +1196,18 @@ void WiFiManagerHandlers::handleApiDeviceRestart(AsyncWebServerRequest *request) _wm->log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, F("API device restart")); #endif handleRequest(request); - sendApiJson(request, 200, F("{\"ok\":true,\"message\":\"Restart scheduled\"}")); + sendApiJson(request, 200, jsonApiDeviceRestartScheduled()); _wm->_rebootScheduled = true; _wm->_rebootTime = millis() + _wm->REBOOT_DELAY_MS; } -void WiFiManagerHandlers::handleApiDeviceErase(AsyncWebServerRequest *request) { +void WiFiManagerHandlers::handleApiDeviceErase(AsyncWebServerRequest *request, boolean optionalErase) { #ifndef WM_NO_LOG _wm->log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, F("API device erase")); #endif handleRequest(request); - bool ret = _wm->erase(false); - String json = F("{\"ok\":"); - json += ret ? F("true") : F("false"); - json += F(",\"message\":\""); - if (ret) { - json += F("WiFi configuration erased. Device will restart shortly."); - } else { - json += F("Erase failed"); - } - json += F("\"}"); - sendApiJson(request, ret ? 200 : 500, json); + bool ret = _wm->erase(optionalErase); + sendApiJson(request, ret ? 200 : 500, jsonApiEraseResponse(ret)); if (ret) { _wm->_rebootScheduled = true; _wm->_rebootTime = millis() + _wm->ERASE_REBOOT_DELAY_MS; @@ -2035,7 +1220,7 @@ void WiFiManagerHandlers::handleApiPortalClose(AsyncWebServerRequest *request) { #endif stopCaptivePortal(); handleRequest(request); - sendApiJson(request, 200, F("{\"ok\":true,\"message\":\"Captive portal detection disabled\"}")); + sendApiJson(request, 200, jsonApiPortalCloseOk()); } void WiFiManagerHandlers::handleApiPortalExit(AsyncWebServerRequest *request) { @@ -2043,7 +1228,11 @@ void WiFiManagerHandlers::handleApiPortalExit(AsyncWebServerRequest *request) { _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("API portal exit")); #endif handleRequest(request); - sendApiJson(request, 200, F("{\"ok\":true,\"message\":\"Exiting portal\"}")); + if (!_wm->_allowExit) { + sendApiJson(request, 403, jsonApiPortalExitForbidden()); + return; + } + sendApiJson(request, 200, jsonApiPortalExitOk()); _wm->_abortScheduled = true; _wm->_abortTime = millis() + _wm->EXIT_DELAY_MS; } diff --git a/lib/WiFiManager/src/WiFiManagerParameter.cpp b/lib/WiFiManager/src/WiFiManagerParameter.cpp index 328969f..eab1ab1 100644 --- a/lib/WiFiManager/src/WiFiManagerParameter.cpp +++ b/lib/WiFiManager/src/WiFiManagerParameter.cpp @@ -1,3 +1,10 @@ +/** + * WiFiManagerParameter.cpp + * + * @author alexhopeoconnor + * @license MIT + */ + #include "WiFiManagerParameter.h" #include diff --git a/lib/WiFiManager/src/WiFiManagerServer.cpp b/lib/WiFiManager/src/WiFiManagerServer.cpp index 5e9d52a..c988b9a 100644 --- a/lib/WiFiManager/src/WiFiManagerServer.cpp +++ b/lib/WiFiManager/src/WiFiManagerServer.cpp @@ -1,16 +1,16 @@ /** * WiFiManagerServer.cpp - * - * Server lifecycle management implementation - * - * @author tablatronix - * @author Alex Hope-O'Connor + * + * @author alexhopeoconnor * @license MIT + * + * Server lifecycle: single shell + JSON APIs + OTA + captive portal. + * Route inventory matches WiFiManagerServer.h (GET /, /api/..., POST /u, onNotFound). */ #include "WiFiManagerServer.h" #include "WiFiManager.h" -#include "WiFiManagerHandlers.h" // Full definition needed for unique_ptr +#include "WiFiManagerHandlers.h" #ifdef WM_DFTE_LOGGING #include "WiFiManagerDfteLogger.h" #include @@ -26,51 +26,19 @@ WiFiManagerServer* WiFiManagerServer::instance() { return s_instance; } -WiFiManagerServer::WiFiManagerServer(WiFiManager* wm) - : _wm(wm), _handlers(std::make_unique(wm)) { +WiFiManagerServer::WiFiManagerServer(WiFiManager* wm) + : _wm(wm), _handlers(std::make_unique(wm)) { s_instance = this; } WiFiManagerServer::~WiFiManagerServer() = default; -// Static getters for RAM placeholders (registry requires zero-arg functions) using singleton const char* WiFiManagerServer::tplGetPageTitle() { auto srv = WiFiManagerServer::instance(); if (!srv || !srv->_wm) return ""; return srv->_wm->_title.c_str(); } -const char* WiFiManagerServer::tplGetSubtitle() { - static String buf; - auto srv = WiFiManagerServer::instance(); - if (!srv || !srv->_wm) { buf = ""; return buf.c_str(); } - WiFiManager* wm = srv->_wm; - if (wm->configPortalActive) { - buf = wm->_apName; - } else { - buf = wm->getWiFiHostname() + " - " + WiFi.localIP().toString(); - } - return buf.c_str(); -} - -const char* WiFiManagerServer::tplGetMenu() { - static String buf; - auto srv = WiFiManagerServer::instance(); - if (!srv || !srv->_handlers) { buf = ""; return buf.c_str(); } - buf = ""; - srv->_handlers->getMenuOut(&buf); - return buf.c_str(); -} - -const char* WiFiManagerServer::tplGetStatus() { - static String buf; - auto srv = WiFiManagerServer::instance(); - if (!srv || !srv->_handlers) { buf = ""; return buf.c_str(); } - buf = ""; - srv->_handlers->reportStatus(buf); - return buf.c_str(); -} - void WiFiManagerServer::registerDefaultStyles(PlaceholderRegistry& reg) { reg.registerProgmemData("%STYLES%", CSS_STYLE); } @@ -83,59 +51,21 @@ void WiFiManagerServer::registerDefaultPageTitle(PlaceholderRegistry& reg) { reg.registerRamData("%PAGE_TITLE%", &WiFiManagerServer::tplGetPageTitle); } -void WiFiManagerServer::registerDefaultSubtitle(PlaceholderRegistry& reg) { - reg.registerRamData("%SUBTITLE%", &WiFiManagerServer::tplGetSubtitle); -} - -void WiFiManagerServer::registerDefaultMenu(PlaceholderRegistry& reg) { - reg.registerRamData("%MENU%", &WiFiManagerServer::tplGetMenu); -} - -void WiFiManagerServer::registerDefaultStatus(PlaceholderRegistry& reg) { - reg.registerRamData("%STATUS%", &WiFiManagerServer::tplGetStatus); -} - void WiFiManagerServer::registerDefaultPlaceholders(PlaceholderRegistry& reg) { registerDefaultStyles(reg); registerDefaultScripts(reg); registerDefaultPageTitle(reg); - registerDefaultSubtitle(reg); - registerDefaultMenu(reg); - registerDefaultStatus(reg); -} - -void WiFiManagerServer::applyTemplateSetupCallback(PlaceholderRegistry& reg) { - if (_tplSetupCallback) { - _tplSetupCallback(reg); - } -} - -void WiFiManagerServer::rebuildPlaceholderRegistry(std::function customizer) { - _tplRegistry = std::unique_ptr(new PlaceholderRegistry(WM_TEMPLATE_REGISTRY_CAPACITY)); - registerDefaultPlaceholders(*_tplRegistry); - if (customizer) { - customizer(*_tplRegistry); - } else { - applyTemplateSetupCallback(*_tplRegistry); - } } void WiFiManagerServer::setupTemplateEngine() { - // Initialize shared placeholder registry once if (!_tplRegistry) { _tplRegistry = std::unique_ptr(new PlaceholderRegistry(WM_TEMPLATE_REGISTRY_CAPACITY)); } else { _tplRegistry->clear(); } - - // Register all default placeholders registerDefaultPlaceholders(*_tplRegistry); - - // Allow consumers to customize placeholders - applyTemplateSetupCallback(*_tplRegistry); #ifdef WM_DFTE_LOGGING - // Install DFTE log sink only if nothing else registered yet (e.g. DeviceFramework's bridge). if (!deviceFrameworkTemplateEngineIsLoggingEnabled()) { if (!_dfteLogger) { _dfteLogger = std::make_unique(_wm); @@ -147,53 +77,48 @@ void WiFiManagerServer::setupTemplateEngine() { } void WiFiManagerServer::createServer(uint16_t port) { - // If server already exists, shutdown first to prevent leaks and connection drops if (server) { - #ifndef WM_NO_LOG +#ifndef WM_NO_LOG _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("Server already exists, shutting down first")); - #endif +#endif shutdownServer(); } - #ifndef WM_NO_LOG +#ifndef WM_NO_LOG _wm->log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, F("Starting Web Portal")); - #endif +#endif - if(port != 80) { - #ifndef WM_NO_LOG + if (port != 80) { +#ifndef WM_NO_LOG _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("http server started with custom port: "), port); - #endif +#endif } server.reset(new AsyncWebServer(port)); - - // Ensure template engine is ready whenever a server is (re)created setupTemplateEngine(); } void WiFiManagerServer::registerRoutes() { if (!server) { - #ifndef WM_NO_LOG +#ifndef WM_NO_LOG _wm->log(WiFiManagerLogLevel::Error, kWiFiMgrLogSubsystem, F("[ERROR] Server not created, call createServer() first")); - #endif +#endif return; } - // Ensure template engine is initialized before routes use it if (!_tplRegistry) { setupTemplateEngine(); } - - if (_wm->_webservercallback != NULL) { - #ifndef WM_NO_LOG - _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("[CB] _webservercallback calling")); - #endif - _wm->_webservercallback(); // @CALLBACK - } - - /* Setup httpd callbacks, web pages: root, wifi config pages, SO captive portal detectors and not found. */ - // Register routes with lambdas + if (_wm->_webservercallback != NULL) { +#ifndef WM_NO_LOG + _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("[CB] _webservercallback calling")); +#endif + _wm->_webservercallback(); + } + + // Shell: only HTML document. JSON + actions + OTA below (no legacy /wifi, /info, … routes). + server->on(WM_G(R_root), HTTP_GET, [this](AsyncWebServerRequest *request) { this->_handlers->handleRoot(request); }); @@ -237,88 +162,32 @@ void WiFiManagerServer::registerRoutes() { server->on(WM_G(R_api_portal_exit), HTTP_POST, [this](AsyncWebServerRequest *request) { this->_handlers->handleApiPortalExit(request); }); - - server->on(WM_G(R_wifi), HTTP_GET, [this](AsyncWebServerRequest *request) { - this->_handlers->handleWifi(request, true); - }); - - server->on(WM_G(R_wifinoscan), HTTP_GET, [this](AsyncWebServerRequest *request) { - this->_handlers->handleWifi(request, false); - }); - - server->on(WM_G(R_wifisave), HTTP_POST, [this](AsyncWebServerRequest *request) { - this->_handlers->handleWifiSave(request); - }); - - server->on(WM_G(R_info), HTTP_GET, [this](AsyncWebServerRequest *request) { - this->_handlers->handleInfo(request); - }); - - server->on(WM_G(R_param), HTTP_GET, [this](AsyncWebServerRequest *request) { - this->_handlers->handleParam(request); - }); - - server->on(WM_G(R_paramsave), HTTP_POST, [this](AsyncWebServerRequest *request) { - this->_handlers->handleParamSave(request); - }); - - server->on(WM_G(R_restart), HTTP_GET, [this](AsyncWebServerRequest *request) { - this->_handlers->handleReset(request); - }); - - server->on(WM_G(R_exit), HTTP_GET, [this](AsyncWebServerRequest *request) { - this->_handlers->handleExit(request); - }); - - server->on(WM_G(R_close), HTTP_GET, [this](AsyncWebServerRequest *request) { - this->_handlers->handleClose(request); - }); - - server->on(WM_G(R_erase), HTTP_GET, [this](AsyncWebServerRequest *request) { - this->_handlers->handleErase(request, false); - }); - - server->on(WM_G(R_scanstatus), HTTP_GET, [this](AsyncWebServerRequest *request) { - this->_handlers->handleWiFiScanStatus(request); - }); - server->on(WM_G(R_scan), HTTP_POST, [this](AsyncWebServerRequest *request) { - this->_handlers->handleWiFiScanRequest(request); - }); - - // OTA Update routes - server->on(WM_G(R_update), HTTP_GET, [this](AsyncWebServerRequest *request) { - this->_handlers->handleUpdate(request); - }); - - server->on(WM_G(R_updatedone), HTTP_POST, - [this](AsyncWebServerRequest *request) { - this->_handlers->handleUpdateDone(request); - }, - [this](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { - this->_handlers->handleUpdating(request, filename, index, data, len, final); - } - ); - - // 404 handler + server->on(WM_G(R_updatedone), HTTP_POST, + [this](AsyncWebServerRequest *request) { + this->_handlers->handleUpdateDone(request); + }, + [this](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, + bool final) { + this->_handlers->handleUpdating(request, filename, index, data, len, final); + }); + server->onNotFound([this](AsyncWebServerRequest *request) { this->_handlers->handleNotFound(request); }); - - server->begin(); // Web server start - #ifndef WM_NO_LOG + + server->begin(); +#ifndef WM_NO_LOG _wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("HTTP server started")); - #endif +#endif } void WiFiManagerServer::setupDNSD() { dnsServer.reset(new DNSServer()); - - /* Setup the DNS server redirecting all the domains to the apIP */ dnsServer->setErrorReplyCode(DNSReplyCode::NoError); - #ifndef WM_NO_LOG +#ifndef WM_NO_LOG _wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem, F("dns server started with ip: "), WiFi.softAPIP()); - #endif +#endif const uint8_t DNS_PORT = 53; dnsServer->start(DNS_PORT, F("*"), WiFi.softAPIP()); } @@ -338,20 +207,16 @@ void WiFiManagerServer::shutdownServer() { } #endif - // AsyncWebServer doesn't have stop(), just reset the unique_ptr to free resources if (server) { server.reset(); } - - // Shutdown DNS server + if (dnsServer) { - dnsServer->stop(); // free heap ? + dnsServer->stop(); dnsServer.reset(); } - - // Clear singleton on shutdown + if (s_instance == this) s_instance = nullptr; } #endif - diff --git a/lib/WiFiManager/src/WiFiManagerUtils.cpp b/lib/WiFiManager/src/WiFiManagerUtils.cpp index 6d5c3de..b3421e8 100644 --- a/lib/WiFiManager/src/WiFiManagerUtils.cpp +++ b/lib/WiFiManager/src/WiFiManagerUtils.cpp @@ -2,7 +2,7 @@ * WiFiManagerUtils.cpp * Implementation of pure utility functions for WiFiManager * - * @author Alex Hope-O'Connor + * @author alexhopeoconnor * @license MIT */ diff --git a/test/test_wifimanager/test_main.cpp b/test/test_wifimanager/test_main.cpp index 489167c..4896c6c 100644 --- a/test/test_wifimanager/test_main.cpp +++ b/test/test_wifimanager/test_main.cpp @@ -30,8 +30,8 @@ TestCase tests[] = { TEST_ENTRY(test_config_portal_multiple_start_stop), TEST_ENTRY(test_config_portal_already_active), 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_bootstrap_json_portal_feature_flags), + TEST_ENTRY(test_bootstrap_json_snapshot_consistency), TEST_ENTRY(test_root_render_interleaved_context_isolation), // Non-blocking tests @@ -113,15 +113,15 @@ TestCase tests[] = { TEST_ENTRY(test_scan_generation_invalidated_on_reset), // Template rendering tests - TEST_ENTRY(test_page_shell_supports_page_assets), - TEST_ENTRY(test_root_template_renders_core_placeholders), - TEST_ENTRY(test_wifi_template_renders_scan_container_and_form_fields), - TEST_ENTRY(test_message_template_renders_body_and_actions), - TEST_ENTRY(test_param_template_renders_shared_actions), - TEST_ENTRY(test_info_template_renders_sections_and_footer), - TEST_ENTRY(test_fragment_action_form_renders_shared_button_markup), - TEST_ENTRY(test_fragment_info_row_and_section_render), - TEST_ENTRY(test_fragment_centered_button_supports_extra_attrs), + TEST_ENTRY(test_shell_template_renders_core_placeholders), + + TEST_ENTRY(test_captive_redirect_host_rule), + TEST_ENTRY(test_api_wifi_meta_json_shape), + TEST_ENTRY(test_api_info_json_shape), + TEST_ENTRY(test_api_params_json_shape), + TEST_ENTRY(test_api_status_json_shape), + TEST_ENTRY(test_api_action_response_json_literals), + TEST_ENTRY(test_api_ota_done_success_json_literal), // State transition tests TEST_ENTRY(test_portal_to_connected_transition), diff --git a/test/test_wifimanager/test_main.h b/test/test_wifimanager/test_main.h index 43ae1b4..8d7f64c 100644 --- a/test/test_wifimanager/test_main.h +++ b/test/test_wifimanager/test_main.h @@ -40,8 +40,8 @@ void test_stop_web_portal(); void test_config_portal_multiple_start_stop(); 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_bootstrap_json_portal_feature_flags(); +void test_bootstrap_json_snapshot_consistency(); void test_root_render_interleaved_context_isolation(); // Non-blocking tests @@ -123,15 +123,16 @@ void test_scan_cancels_when_lifecycle_blocked(); void test_scan_generation_invalidated_on_reset(); // Template rendering tests -void test_page_shell_supports_page_assets(); -void test_root_template_renders_core_placeholders(); -void test_wifi_template_renders_scan_container_and_form_fields(); -void test_message_template_renders_body_and_actions(); -void test_param_template_renders_shared_actions(); -void test_info_template_renders_sections_and_footer(); -void test_fragment_action_form_renders_shared_button_markup(); -void test_fragment_info_row_and_section_render(); -void test_fragment_centered_button_supports_extra_attrs(); +void test_shell_template_renders_core_placeholders(); + +// API JSON + captive helper tests +void test_captive_redirect_host_rule(); +void test_api_wifi_meta_json_shape(); +void test_api_info_json_shape(); +void test_api_params_json_shape(); +void test_api_status_json_shape(); +void test_api_action_response_json_literals(); +void test_api_ota_done_success_json_literal(); // State transition tests void test_portal_to_connected_transition(); diff --git a/test/test_wifimanager/tests/test_api_json_and_captive.cpp b/test/test_wifimanager/tests/test_api_json_and_captive.cpp new file mode 100644 index 0000000..f043445 --- /dev/null +++ b/test/test_wifimanager/tests/test_api_json_and_captive.cpp @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include "../test_main.h" + +void test_captive_redirect_host_rule() { + Serial.println("[TEST] Testing captive redirect host comparison..."); + + TEST_ASSERT_TRUE(WiFiManagerHandlers::shouldRedirectCaptiveForHost("captive.apple.com", "192.168.4.1")); + TEST_ASSERT_TRUE(WiFiManagerHandlers::shouldRedirectCaptiveForHost("192.168.4.2", "192.168.4.1")); + TEST_ASSERT_FALSE(WiFiManagerHandlers::shouldRedirectCaptiveForHost("192.168.4.1", "192.168.4.1")); + TEST_ASSERT_FALSE(WiFiManagerHandlers::shouldRedirectCaptiveForHost("example.com", "")); + + Serial.println("[TEST] Captive redirect host rule test completed successfully"); +} + +void test_api_wifi_meta_json_shape() { + Serial.println("[TEST] Testing /api/wifi/meta JSON shape..."); + + WiFiManager wm; + WiFiManagerHandlers handlers(&wm); + String j = handlers.buildApiWifiMetaJson(); + + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"wifiFields\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"staticFields\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"params\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"actions\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"canRefreshScan\":true"))); + + Serial.println("[TEST] WiFi meta JSON shape test completed successfully"); +} + +void test_api_info_json_shape() { + Serial.println("[TEST] Testing /api/info JSON shape..."); + + WiFiManager wm; + WiFiManagerHandlers handlers(&wm); + String j = handlers.buildApiInfoJson(); + + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"status\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"connected\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"device\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"wifi\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"about\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"actions\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"showUpdate\""))); + + Serial.println("[TEST] API info JSON shape test completed successfully"); +} + +void test_api_params_json_shape() { + Serial.println("[TEST] Testing /api/params JSON shape..."); + + WiFiManager wm; + WiFiManagerHandlers handlers(&wm); + String j = handlers.buildApiParamsGetJson(); + + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"params\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"actions\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"showBack\""))); + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"params\":["))); + + Serial.println("[TEST] API params JSON shape test completed successfully"); +} + +void test_api_status_json_shape() { + Serial.println("[TEST] Testing /api/status JSON shape..."); + + WiFiManager wm; + WiFiManagerHandlers handlers(&wm); + String j = handlers.buildApiStatusJson(); + + TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"text\""))); + TEST_ASSERT_EQUAL('{', j.charAt(0)); + TEST_ASSERT_EQUAL('}', j.charAt(j.length() - 1)); + + Serial.println("[TEST] API status JSON shape test completed successfully"); +} + +void test_api_action_response_json_literals() { + Serial.println("[TEST] Testing POST /api action JSON payloads..."); + + TEST_ASSERT_EQUAL_STRING( + "{\"ok\":true,\"message\":\"Restart scheduled\"}", + WiFiManagerHandlers::jsonApiDeviceRestartScheduled().c_str()); + TEST_ASSERT_EQUAL_STRING( + "{\"ok\":true,\"message\":\"Setup saved\"}", + WiFiManagerHandlers::jsonApiParamsSaveOk().c_str()); + TEST_ASSERT_EQUAL_STRING( + "{\"ok\":true,\"message\":\"Captive portal detection disabled\"}", + WiFiManagerHandlers::jsonApiPortalCloseOk().c_str()); + TEST_ASSERT_EQUAL_STRING( + "{\"ok\":true,\"message\":\"Exiting portal\"}", + WiFiManagerHandlers::jsonApiPortalExitOk().c_str()); + TEST_ASSERT_EQUAL_STRING( + "{\"ok\":false,\"message\":\"Exit not allowed\"}", + WiFiManagerHandlers::jsonApiPortalExitForbidden().c_str()); + TEST_ASSERT_EQUAL_STRING( + "{\"ok\":true,\"message\":\"WiFi configuration erased. Device will restart shortly.\"}", + WiFiManagerHandlers::jsonApiEraseResponse(true).c_str()); + TEST_ASSERT_EQUAL_STRING( + "{\"ok\":false,\"message\":\"Erase failed\"}", + WiFiManagerHandlers::jsonApiEraseResponse(false).c_str()); + + Serial.println("[TEST] Action response JSON literals test completed successfully"); +} + +void test_api_ota_done_success_json_literal() { + Serial.println("[TEST] Testing POST /u completion success JSON..."); + + TEST_ASSERT_EQUAL_STRING( + "{\"ok\":true,\"message\":\"Firmware updated. Restarting...\"}", + WiFiManagerHandlers::jsonApiOtaUpdateSuccess().c_str()); + + Serial.println("[TEST] OTA success JSON literal test completed successfully"); +} diff --git a/test/test_wifimanager/tests/test_root_render_lifecycle.cpp b/test/test_wifimanager/tests/test_root_render_lifecycle.cpp index 8c0df02..3296087 100644 --- a/test/test_wifimanager/tests/test_root_render_lifecycle.cpp +++ b/test/test_wifimanager/tests/test_root_render_lifecycle.cpp @@ -3,13 +3,12 @@ #include #include #include -#include "templates/RootSelector.h" +#include "templates/RootShell.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); @@ -56,37 +55,29 @@ void renderContextsInterleaved(TemplateContext& first, String& firstOutput, } // namespace -void test_root_render_menu_state_transitions() { - Serial.println("[TEST] Testing root menu state transitions..."); +void test_bootstrap_json_portal_feature_flags() { + Serial.println("[TEST] Testing bootstrap JSON portal feature flags..."); WiFiManager wm; WiFiManagerHandlers handlers(&wm); - String menuBefore; - handlers.getMenuOut(&menuBefore); - TEST_ASSERT_EQUAL(-1, menuBefore.indexOf("/close")); + String idleBootstrap = handlers.buildPortalBootstrapJson(); + TEST_ASSERT_EQUAL(-1, idleBootstrap.indexOf(F("\"showCloseCaptive\":true"))); - wm.setConfigPortalTimeout(5); - wm.startConfigPortal("RootStateAP"); - wm.process(); - delay(100); + wm.wmTestSetPortalActive(true); + wm.setCaptivePortalEnable(true); - String menuActive; - handlers.getMenuOut(&menuActive); - TEST_ASSERT_NOT_EQUAL(-1, menuActive.indexOf("/close")); + String apBootstrap = handlers.buildPortalBootstrapJson(); + TEST_ASSERT_NOT_EQUAL(-1, apBootstrap.indexOf(F("\"showCloseCaptive\":true"))); + TEST_ASSERT_NOT_EQUAL(-1, apBootstrap.indexOf(F("\"showExitPortal\":true"))); - wm.stopConfigPortal(); - delay(100); + wm.wmTestSetPortalActive(false); - String menuAfter; - handlers.getMenuOut(&menuAfter); - TEST_ASSERT_EQUAL(-1, menuAfter.indexOf("/close")); - - Serial.println("[TEST] Root menu state transitions test completed successfully"); + Serial.println("[TEST] Bootstrap portal feature flags test completed successfully"); } -void test_root_render_snapshot_consistency() { - Serial.println("[TEST] Testing root render snapshot consistency..."); +void test_bootstrap_json_snapshot_consistency() { + Serial.println("[TEST] Testing bootstrap JSON snapshot consistency..."); WiFiManager wm; WiFiManagerHandlers handlers(&wm); @@ -96,24 +87,15 @@ void test_root_render_snapshot_consistency() { wm.process(); delay(100); - String menuSnapshot1; - String statusSnapshot1; - handlers.getMenuOut(&menuSnapshot1); - handlers.reportStatus(statusSnapshot1); + String snap1 = handlers.buildPortalBootstrapJson(); + String snap2 = handlers.buildPortalBootstrapJson(); - String menuSnapshot2; - String statusSnapshot2; - handlers.getMenuOut(&menuSnapshot2); - handlers.reportStatus(statusSnapshot2); - - TEST_ASSERT_GREATER_THAN(0, menuSnapshot1.length()); - TEST_ASSERT_GREATER_THAN(0, statusSnapshot1.length()); - TEST_ASSERT_EQUAL_STRING(menuSnapshot1.c_str(), menuSnapshot2.c_str()); - TEST_ASSERT_EQUAL_STRING(statusSnapshot1.c_str(), statusSnapshot2.c_str()); + TEST_ASSERT_GREATER_THAN(32, snap1.length()); + TEST_ASSERT_EQUAL_STRING(snap1.c_str(), snap2.c_str()); wm.stopConfigPortal(); - Serial.println("[TEST] Root render snapshot consistency test completed successfully"); + Serial.println("[TEST] Bootstrap JSON snapshot consistency test completed successfully"); } void test_root_render_interleaved_context_isolation() { @@ -125,58 +107,56 @@ void test_root_render_interleaved_context_isolation() { 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
"); + String bootstrapA = F("{\"ctx\":\"A\"}"); + String appJsA = F("// shell A"); + String bootstrapB = F("{\"ctx\":\"B\"}"); + String appJsB = F("// shell B"); - DynamicTemplateDescriptor menuDescriptorA{}; - DynamicTemplateDescriptor statusDescriptorA{}; - DynamicTemplateDescriptor menuDescriptorB{}; - DynamicTemplateDescriptor statusDescriptorB{}; + DynamicTemplateDescriptor bootstrapDescriptorA{}; + DynamicTemplateDescriptor appJsDescriptorA{}; + DynamicTemplateDescriptor bootstrapDescriptorB{}; + DynamicTemplateDescriptor appJsDescriptorB{}; - configureDescriptor(menuDescriptorA, menuA); - configureDescriptor(statusDescriptorA, statusA); - configureDescriptor(menuDescriptorB, menuB); - configureDescriptor(statusDescriptorB, statusB); + configureDescriptor(bootstrapDescriptorA, bootstrapA); + configureDescriptor(appJsDescriptorA, appJsA); + configureDescriptor(bootstrapDescriptorB, bootstrapB); + configureDescriptor(appJsDescriptorB, appJsB); - 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"); + TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicTemplate("%BOOTSTRAP_JSON%", &bootstrapDescriptorA), + "Registry A bootstrap placeholder should register"); + TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicTemplate("%PORTAL_APP_JS%", &appJsDescriptorA), + "Registry A app JS placeholder should register"); + TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicTemplate("%BOOTSTRAP_JSON%", &bootstrapDescriptorB), + "Registry B bootstrap placeholder should register"); + TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicTemplate("%PORTAL_APP_JS%", &appJsDescriptorB), + "Registry B app JS 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); + TemplateRenderer::initializeContext(contextA, WM_ROOT_SHELL_TEMPLATE); + TemplateRenderer::initializeContext(contextB, WM_ROOT_SHELL_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, outputA.indexOf("{\"ctx\":\"A\"}")); + TEST_ASSERT_NOT_EQUAL(-1, outputA.indexOf("// shell A")); + TEST_ASSERT_EQUAL(-1, outputA.indexOf("{\"ctx\":\"B\"}")); + TEST_ASSERT_EQUAL(-1, outputA.indexOf("// shell 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")); + TEST_ASSERT_NOT_EQUAL(-1, outputB.indexOf("{\"ctx\":\"B\"}")); + TEST_ASSERT_NOT_EQUAL(-1, outputB.indexOf("// shell B")); + TEST_ASSERT_EQUAL(-1, outputB.indexOf("{\"ctx\":\"A\"}")); + TEST_ASSERT_EQUAL(-1, outputB.indexOf("// shell A")); Serial.println("[TEST] Root render interleaved context isolation test completed successfully"); } diff --git a/test/test_wifimanager/tests/test_template_rendering.cpp b/test/test_wifimanager/tests/test_template_rendering.cpp index de068d3..b85733f 100644 --- a/test/test_wifimanager/tests/test_template_rendering.cpp +++ b/test/test_wifimanager/tests/test_template_rendering.cpp @@ -1,13 +1,7 @@ #include #include #include -#include "templates/Fragments.h" -#include "templates/Info.h" -#include "templates/Param.h" -#include "templates/PageShell.h" -#include "templates/Root.h" -#include "templates/WiFi.h" -#include "templates/Message.h" +#include "templates/RootShell.h" #include "../test_main.h" namespace { @@ -31,221 +25,30 @@ String renderTemplate(const char* templateData, PlaceholderRegistry& registry) { const char kEmpty[] PROGMEM = ""; const char kDocTitle[] PROGMEM = "Config ESP"; -const char kBodyClass[] PROGMEM = "wifi"; -const char kPageScripts[] PROGMEM = ""; -const char kPageStyles[] PROGMEM = ""; -const char kPageContent[] PROGMEM = "
Body
"; -const char kScanContent[] PROGMEM = "Scanning for networks...

"; -const char kSsidPlaceholder[] PROGMEM = "Office"; -const char kPasswordPlaceholder[] PROGMEM = "********"; -const char kStaticFields[] PROGMEM = "

"; -const char kParamSection[] PROGMEM = "

"; -const char kFormActions[] PROGMEM = "

"; -const char kPageActions[] PROGMEM = "


"; -const char kMessageActions[] PROGMEM = "

"; -const char kStatus[] PROGMEM = "
Status
"; -const char kMessageBody[] PROGMEM = "
Saved
"; -const char kInfoSection[] PROGMEM = "

About


Build date
today
"; -const char kInfoFooter[] PROGMEM = "

"; -const char kPageTitle[] PROGMEM = "WiFiManager"; -const char kSubtitle[] PROGMEM = "Setup"; -const char kMenu[] PROGMEM = "
"; -const char kActionPrefix[] PROGMEM = "

"; -const char kActionPath[] PROGMEM = "/update"; -const char kActionMethod[] PROGMEM = "get"; -const char kActionLabel[] PROGMEM = "Update"; -const char kActionClassAttr[] PROGMEM = " class='D'"; -const char kActionSuffix[] PROGMEM = "
\n"; -const char kButtonId[] PROGMEM = "refresh-btn"; -const char kButtonType[] PROGMEM = "button"; -const char kButtonOnClick[] PROGMEM = "return refreshScan()"; -const char kButtonExtraAttrs[] PROGMEM = " data-skip-initial-scan='true'"; -const char kInfoLabel[] PROGMEM = "SDK version"; -const char kInfoValue[] PROGMEM = "3.1.2"; -const char kSectionTitle[] PROGMEM = "Device"; -const char kSectionRows[] PROGMEM = "
Chip ID
abc
"; +const char kBootstrapJson[] PROGMEM = "{\"title\":\"Test\"}"; +const char kPortalAppJs[] PROGMEM = "console.log('portal');"; } // namespace -void test_page_shell_supports_page_assets() { - Serial.println("[TEST] Testing page shell page-scoped assets..."); - - PlaceholderRegistry registry(12); - registry.registerProgmemData("%SCRIPTS%", kEmpty); - registry.registerProgmemData("%PAGE_SCRIPTS%", kPageScripts); - registry.registerProgmemData("%STYLES%", kEmpty); - registry.registerProgmemData("%PAGE_STYLES%", kPageStyles); - registry.registerProgmemData("%DOC_TITLE%", kDocTitle); - registry.registerProgmemData("%BODY_CLASS%", kBodyClass); - registry.registerProgmemData("%PAGE_CONTENT%", kPageContent); - - String output = renderTemplate(WM_PAGE_SHELL_TEMPLATE, registry); - - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("wifi page")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf(".wifi{color:#000;}")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("
Body
")); - - Serial.println("[TEST] Page shell page-scoped assets test completed successfully"); -} - -void test_wifi_template_renders_scan_container_and_form_fields() { - Serial.println("[TEST] Testing WiFi page template rendering..."); - - PlaceholderRegistry registry(16); - registry.registerProgmemData("%WIFI_SCAN_CONTENT%", kScanContent); - registry.registerProgmemData("%WIFI_SSID_PLACEHOLDER%", kSsidPlaceholder); - registry.registerProgmemData("%WIFI_PASSWORD_PLACEHOLDER%", kPasswordPlaceholder); - registry.registerProgmemData("%WIFI_STATIC_FIELDS%", kStaticFields); - registry.registerProgmemData("%WIFI_PARAM_SECTION%", kParamSection); - registry.registerProgmemData("%WIFI_FORM_ACTIONS%", kFormActions); - registry.registerProgmemData("%WIFI_PAGE_ACTIONS%", kPageActions); - registry.registerProgmemData("%WIFI_STATUS%", kStatus); - - String output = renderTemplate(WM_WIFI_CONTENT_TEMPLATE, registry); - - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='scan-results'")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Scanning for networks...")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("placeholder='Office'")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='refresh-btn'")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Token")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Save")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Back")); - - Serial.println("[TEST] WiFi page template rendering test completed successfully"); -} - -void test_message_template_renders_body_and_actions() { - Serial.println("[TEST] Testing message template rendering..."); - - PlaceholderRegistry registry(8); - registry.registerProgmemData("%MESSAGE_BODY%", kMessageBody); - registry.registerProgmemData("%MESSAGE_ACTIONS%", kMessageActions); - - String output = renderTemplate(WM_MESSAGE_CONTENT_TEMPLATE, registry); - - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Saved")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Back")); - - Serial.println("[TEST] Message template rendering test completed successfully"); -} - -void test_param_template_renders_shared_actions() { - Serial.println("[TEST] Testing param page template rendering..."); - - PlaceholderRegistry registry(8); - registry.registerProgmemData("%PARAM_FIELDS%", kParamSection); - registry.registerProgmemData("%PARAM_FORM_ACTIONS%", kFormActions); - registry.registerProgmemData("%PARAM_PAGE_ACTIONS%", kMessageActions); - registry.registerProgmemData("%PARAM_STATUS%", kStatus); - - String output = renderTemplate(WM_PARAM_CONTENT_TEMPLATE, registry); - - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Token")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Save")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Back")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Status")); - - Serial.println("[TEST] Param page template rendering test completed successfully"); -} - -void test_info_template_renders_sections_and_footer() { - Serial.println("[TEST] Testing info page template rendering..."); - - PlaceholderRegistry registry(8); - registry.registerProgmemData("%INFO_STATUS%", kStatus); - registry.registerProgmemData("%INFO_DEVICE_SECTION%", kInfoSection); - registry.registerProgmemData("%INFO_WIFI_SECTION%", kInfoSection); - registry.registerProgmemData("%INFO_ABOUT_SECTION%", kInfoSection); - registry.registerProgmemData("%INFO_FOOTER%", kInfoFooter); - - String output = renderTemplate(WM_INFO_CONTENT_TEMPLATE, registry); - - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Status")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Build date")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Update")); - - Serial.println("[TEST] Info page template rendering test completed successfully"); -} - -void test_fragment_action_form_renders_shared_button_markup() { - Serial.println("[TEST] Testing shared action fragment rendering..."); - - PlaceholderRegistry registry(8); - registry.registerProgmemData("%ACTION_PREFIX%", kActionPrefix); - registry.registerProgmemData("%ACTION%", kActionPath); - registry.registerProgmemData("%METHOD%", kActionMethod); - registry.registerProgmemData("%BUTTON_LABEL%", kActionLabel); - registry.registerProgmemData("%BUTTON_CLASS_ATTR%", kActionClassAttr); - registry.registerProgmemData("%ACTION_SUFFIX%", kActionSuffix); - - String output = renderTemplate(WM_ACTION_FORM_TEMPLATE, registry); - - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("action='/update'")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("class='D'")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf(">Update")); - - Serial.println("[TEST] Shared action fragment rendering test completed successfully"); -} - -void test_fragment_info_row_and_section_render() { - Serial.println("[TEST] Testing shared info fragment rendering..."); - - PlaceholderRegistry rowRegistry(4); - rowRegistry.registerProgmemData("%INFO_LABEL%", kInfoLabel); - rowRegistry.registerProgmemData("%INFO_VALUE%", kInfoValue); - - String rowOutput = renderTemplate(WM_INFO_ROW_TEMPLATE, rowRegistry); - TEST_ASSERT_NOT_EQUAL(-1, rowOutput.indexOf("
SDK version
3.1.2
")); - - PlaceholderRegistry sectionRegistry(6); - sectionRegistry.registerProgmemData("%SECTION_PREFIX%", kEmpty); - sectionRegistry.registerProgmemData("%SECTION_TITLE%", kSectionTitle); - sectionRegistry.registerProgmemData("%SECTION_ROWS%", kSectionRows); - sectionRegistry.registerProgmemData("%SECTION_SUFFIX%", kActionSuffix); - - String sectionOutput = renderTemplate(WM_INFO_SECTION_TEMPLATE, sectionRegistry); - TEST_ASSERT_NOT_EQUAL(-1, sectionOutput.indexOf("

Device

")); - TEST_ASSERT_NOT_EQUAL(-1, sectionOutput.indexOf("
Chip ID
abc
")); - - Serial.println("[TEST] Shared info fragment rendering test completed successfully"); -} - -void test_fragment_centered_button_supports_extra_attrs() { - Serial.println("[TEST] Testing centered button fragment extra attrs..."); - - PlaceholderRegistry registry(8); - registry.registerProgmemData("%BUTTON_ID%", kButtonId); - registry.registerProgmemData("%BUTTON_TYPE%", kButtonType); - registry.registerProgmemData("%BUTTON_ONCLICK%", kButtonOnClick); - registry.registerProgmemData("%BUTTON_LABEL%", kActionLabel); - registry.registerProgmemData("%BUTTON_EXTRA_ATTRS%", kButtonExtraAttrs); - - String output = renderTemplate(WM_CENTERED_BUTTON_TEMPLATE, registry); - - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='refresh-btn'")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("data-skip-initial-scan='true'")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf(">Update")); - - Serial.println("[TEST] Centered button fragment extra attrs test completed successfully"); -} - -void test_root_template_renders_core_placeholders() { - Serial.println("[TEST] Testing root template rendering..."); +void test_shell_template_renders_core_placeholders() { + Serial.println("[TEST] Testing portal shell template rendering..."); PlaceholderRegistry registry(8); registry.registerProgmemData("%SCRIPTS%", kEmpty); registry.registerProgmemData("%STYLES%", kEmpty); - registry.registerProgmemData("%PAGE_TITLE%", kPageTitle); - registry.registerProgmemData("%SUBTITLE%", kSubtitle); - registry.registerProgmemData("%MENU%", kMenu); - registry.registerProgmemData("%STATUS%", kStatus); + registry.registerProgmemData("%PAGE_TITLE%", kDocTitle); + registry.registerProgmemData("%BOOTSTRAP_JSON%", kBootstrapJson); + registry.registerProgmemData("%PORTAL_APP_JS%", kPortalAppJs); - String output = renderTemplate(WM_ROOT_TEMPLATE, registry); + String output = renderTemplate(WM_ROOT_SHELL_TEMPLATE, registry); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("WiFiManager")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Setup")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Configure WiFi")); - TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Status")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Config ESP")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("
")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='wm-toast'")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='wm-dialog'")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='wm-bootstrap'")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("{\"title\":\"Test\"}")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("console.log('portal')")); - Serial.println("[TEST] Root template rendering test completed successfully"); + Serial.println("[TEST] Portal shell template rendering test completed successfully"); }