diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 8d88a10b27..5af6ab29a2 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -47,8 +47,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) { request->send(stream); } void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { - std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr) - std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr) + const auto &ssid = request->arg("ssid"); + const auto &psk = request->arg("psk"); ESP_LOGI(TAG, "Requested WiFi Settings Change:\n" " SSID='%s'\n" @@ -56,10 +56,10 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ssid.c_str(), psk.c_str()); #ifdef USE_ESP8266 // ESP8266 is single-threaded, call directly - wifi::global_wifi_component->save_wifi_sta(ssid, psk); + wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); #else // Defer save to main loop thread to avoid NVS operations from HTTP thread - this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); }); #endif request->redirect(ESPHOME_F("/?save")); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 7da8b49c6d..c7a0639382 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -585,8 +585,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c // Helper to get request detail parameter static JsonDetail get_request_detail(AsyncWebServerRequest *request) { - auto *param = request->getParam(ESPHOME_F("detail")); - return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE; + return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE; } #ifdef USE_SENSOR @@ -863,10 +862,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } auto call = is_on ? obj->turn_on() : obj->turn_off(); - parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed); + parse_num_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed); - if (request->hasParam(ESPHOME_F("oscillation"))) { - auto speed = request->getParam(ESPHOME_F("oscillation"))->value(); + if (request->hasArg(ESPHOME_F("oscillation"))) { + auto speed = request->arg(ESPHOME_F("oscillation")); auto val = parse_on_off(speed.c_str()); switch (val) { case PARSE_ON: @@ -1042,14 +1041,14 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } auto traits = obj->get_traits(); - if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) || - (request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) { + if ((request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) || + (request->hasArg(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) { request->send(409); return; } - parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); - parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt); + parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); + parse_num_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1108,7 +1107,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); + parse_num_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1176,12 +1175,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + const auto &value = request->arg(ESPHOME_F("value")); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (value.length() == 0) { // NOLINT(readability-container-size-empty) request->send(409); return; } - - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date); + call.set_date(value.c_str(), value.length()); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1236,12 +1236,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + const auto &value = request->arg(ESPHOME_F("value")); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (value.length() == 0) { // NOLINT(readability-container-size-empty) request->send(409); return; } - - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time); + call.set_time(value.c_str(), value.length()); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1295,12 +1296,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + const auto &value = request->arg(ESPHOME_F("value")); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (value.length() == 0) { // NOLINT(readability-container-size-empty) request->send(409); return; } - - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime); + call.set_datetime(value.c_str(), value.length()); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1479,10 +1481,14 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode); // Parse temperature parameters - parse_float_param_(request, ESPHOME_F("target_temperature_high"), call, - &decltype(call)::set_target_temperature_high); - parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low); - parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature); + // static_cast needed to disambiguate overloaded setters (float vs optional) + using ClimateCall = decltype(call); + parse_num_param_(request, ESPHOME_F("target_temperature_high"), call, + static_cast(&ClimateCall::set_target_temperature_high)); + parse_num_param_(request, ESPHOME_F("target_temperature_low"), call, + static_cast(&ClimateCall::set_target_temperature_low)); + parse_num_param_(request, ESPHOME_F("target_temperature"), call, + static_cast(&ClimateCall::set_target_temperature)); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1723,12 +1729,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } auto traits = obj->get_traits(); - if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) { + if (request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) { request->send(409); return; } - parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); + parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1872,12 +1878,12 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode); // Parse temperature parameters - parse_float_param_(request, ESPHOME_F("target_temperature"), base_call, - &water_heater::WaterHeaterCall::set_target_temperature); - parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call, - &water_heater::WaterHeaterCall::set_target_temperature_low); - parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call, - &water_heater::WaterHeaterCall::set_target_temperature_high); + parse_num_param_(request, ESPHOME_F("target_temperature"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature); + parse_num_param_(request, ESPHOME_F("target_temperature_low"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature_low); + parse_num_param_(request, ESPHOME_F("target_temperature_high"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature_high); // Parse away mode parameter parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away); @@ -1981,16 +1987,16 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur auto call = obj->make_call(); // Parse carrier frequency (optional) - if (request->hasParam(ESPHOME_F("carrier_frequency"))) { - auto value = parse_number(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str()); + { + auto value = parse_number(request->arg(ESPHOME_F("carrier_frequency")).c_str()); if (value.has_value()) { call.set_carrier_frequency(*value); } } // Parse repeat count (optional, defaults to 1) - if (request->hasParam(ESPHOME_F("repeat_count"))) { - auto value = parse_number(request->getParam(ESPHOME_F("repeat_count"))->value().c_str()); + { + auto value = parse_number(request->arg(ESPHOME_F("repeat_count")).c_str()); if (value.has_value()) { call.set_repeat_count(*value); } @@ -1998,18 +2004,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Parse base64url-encoded raw timings (required) // Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping) - if (!request->hasParam(ESPHOME_F("data"))) { - request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter")); - return; - } + const auto &data_arg = request->arg(ESPHOME_F("data")); - // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string - std::string encoded = - request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr) - - // Validate base64url is not empty - if (encoded.empty()) { - request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter")); + // Validate base64url is not empty (also catches missing parameter since arg() returns empty string) + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty) + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter")); return; } @@ -2017,7 +2017,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // it outlives the call - set_raw_timings_base64url stores a pointer, so the string // must remain valid until perform() completes. // ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context. - this->defer([call, encoded = std::move(encoded)]() mutable { + this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable { call.set_raw_timings_base64url(encoded); call.perform(); }); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index ce09ebf7a9..026da763ea 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -513,11 +513,9 @@ class WebServer : public Controller, template void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float), float scale = 1.0f) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value / scale); - } + auto value = parse_number(request->arg(param_name).c_str()); + if (value.has_value()) { + (call.*setter)(*value / scale); } } @@ -525,34 +523,19 @@ class WebServer : public Controller, template void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(uint32_t), uint32_t scale = 1) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value * scale); - } + auto value = parse_number(request->arg(param_name).c_str()); + if (value.has_value()) { + (call.*setter)(*value * scale); } } #endif - // Generic helper to parse and apply a float parameter - template - void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value); - } - } - } - - // Generic helper to parse and apply an int parameter - template - void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value); - } + // Generic helper to parse and apply a numeric parameter + template + void parse_num_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(NumT)) { + auto value = parse_number(request->arg(param_name).c_str()); + if (value.has_value()) { + (call.*setter)(*value); } } @@ -560,10 +543,9 @@ class WebServer : public Controller, template void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(const std::string &)) { - if (request->hasParam(param_name)) { - // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string - std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr) - (call.*setter)(value); + if (request->hasArg(param_name)) { + const auto &value = request->arg(param_name); + (call.*setter)(std::string(value.c_str(), value.length())); } } @@ -573,8 +555,9 @@ class WebServer : public Controller, // Invalid values are ignored (setter not called) template void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) { - if (request->hasParam(param_name)) { - auto param_value = request->getParam(param_name)->value(); + const auto ¶m_value = request->arg(param_name); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (param_value.length() > 0) { // NOLINT(readability-container-size-empty) // First check on/off (default), then true/false (custom) auto val = parse_on_off(param_value.c_str()); if (val == PARSE_NONE) { diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index 81ae626277..c58ca2a24f 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -1,17 +1,13 @@ #ifdef USE_ESP32 -#include #include #include #include "esphome/core/helpers.h" -#include "esphome/core/log.h" #include "http_parser.h" #include "utils.h" namespace esphome::web_server_idf { -static const char *const TAG = "web_server_idf_utils"; - size_t url_decode(char *str) { char *start = str; char *ptr = str, buf; @@ -54,32 +50,15 @@ optional request_get_header(httpd_req_t *req, const char *name) { return {str}; } -optional request_get_url_query(httpd_req_t *req) { - auto len = httpd_req_get_url_query_len(req); - if (len == 0) { - return {}; - } - - std::string str; - str.resize(len); - - auto res = httpd_req_get_url_query_str(req, &str[0], len + 1); - if (res != ESP_OK) { - ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res)); - return {}; - } - - return {str}; -} - optional query_key_value(const char *query_url, size_t query_len, const char *key) { if (query_url == nullptr || query_len == 0) { return {}; } - // Use stack buffer for typical query strings, heap fallback for large ones - SmallBufferWithHeapFallback<256, char> val(query_len); - + // Value can't exceed query_len. Use small stack buffer for typical values, + // heap fallback for long ones (e.g. base64 IR data) to limit stack usage + // since callers may also have stack buffers for the query string. + SmallBufferWithHeapFallback<128, char> val(query_len); if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) { return {}; } @@ -88,6 +67,18 @@ optional query_key_value(const char *query_url, size_t query_len, c return {val.get()}; } +bool query_has_key(const char *query_url, size_t query_len, const char *key) { + if (query_url == nullptr || query_len == 0) { + return false; + } + // Minimal buffer — we only care if the key exists, not the value + char buf[1]; + // httpd_query_key_value returns ESP_OK if found, ESP_ERR_HTTPD_RESULT_TRUNC if found + // but value truncated (expected with 1-byte buffer), or other errors for invalid input + auto err = httpd_query_key_value(query_url, key, buf, sizeof(buf)); + return err == ESP_OK || err == ESP_ERR_HTTPD_RESULT_TRUNC; +} + // Helper function for case-insensitive string region comparison bool str_ncmp_ci(const char *s1, const char *s2, size_t n) { for (size_t i = 0; i < n; i++) { diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 87635c0458..027a2f7b6c 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -13,11 +13,8 @@ size_t url_decode(char *str); bool request_has_header(httpd_req_t *req, const char *name); optional request_get_header(httpd_req_t *req, const char *name); -optional request_get_url_query(httpd_req_t *req); optional query_key_value(const char *query_url, size_t query_len, const char *key); -inline optional query_key_value(const std::string &query_url, const std::string &key) { - return query_key_value(query_url.c_str(), query_url.size(), key.c_str()); -} +bool query_has_key(const char *query_url, size_t query_len, const char *key); // Helper function for case-insensitive character comparison inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); } diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index f1f89beb49..1798159e7f 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -393,13 +393,7 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { } // Look up value from query strings - optional val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name); - if (!val.has_value()) { - auto url_query = request_get_url_query(*this); - if (url_query.has_value()) { - val = query_key_value(url_query.value().c_str(), url_query.value().size(), name); - } - } + auto val = this->find_query_value_(name); // Don't cache misses to avoid wasting memory when handlers check for // optional parameters that don't exist in the request @@ -412,6 +406,50 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { return param; } +/// Search post_query then URL query with a callback. +/// Returns first truthy result, or value-initialized default. +/// URL query is accessed directly from req->uri (same pattern as url_to()). +template +static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func) + -> decltype(func(nullptr, size_t{0}, name)) { + if (!post_query.empty()) { + auto result = func(post_query.c_str(), post_query.size(), name); + if (result) { + return result; + } + } + // Use httpd API for query length, then access string directly from URI. + // http_parser identifies components by offset/length without modifying the URI string. + // This is the same pattern used by url_to(). + auto len = httpd_req_get_url_query_len(req); + if (len == 0) { + return {}; + } + const char *query = strchr(req->uri, '?'); + if (query == nullptr) { + return {}; + } + query++; // skip '?' + return func(query, len, name); +} + +optional AsyncWebServerRequest::find_query_value_(const char *name) const { + return search_query_sources(this->req_, this->post_query_, name, + [](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); }); +} + +bool AsyncWebServerRequest::hasArg(const char *name) { + return search_query_sources(this->req_, this->post_query_, name, query_has_key); +} + +std::string AsyncWebServerRequest::arg(const char *name) { + auto val = this->find_query_value_(name); + if (val.has_value()) { + return std::move(val.value()); + } + return {}; +} + void AsyncWebServerResponse::addHeader(const char *name, const char *value) { httpd_resp_set_hdr(*this->req_, name, value); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 6a409de74e..12df0303de 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -170,14 +170,8 @@ class AsyncWebServerRequest { AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); } // NOLINTNEXTLINE(readability-identifier-naming) - bool hasArg(const char *name) { return this->hasParam(name); } - std::string arg(const char *name) { - auto *param = this->getParam(name); - if (param) { - return param->value(); - } - return {}; - } + bool hasArg(const char *name); + std::string arg(const char *name); std::string arg(const std::string &name) { return this->arg(name.c_str()); } operator httpd_req_t *() const { return this->req_; } @@ -192,6 +186,7 @@ class AsyncWebServerRequest { // is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid // duplicate storage. Only successful lookups are cached to prevent cache pollution when // handlers check for optional parameters that don't exist. + optional find_query_value_(const char *name) const; std::vector params_; std::string post_query_; AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}