/** * IotWebConfTParameter.h -- IotWebConf is an ESP8266/ESP32 * non blocking WiFi/AP web configuration library for Arduino. * https://github.com/prampec/IotWebConf * * Copyright (C) 2021 Balazs Kelemen * rovo89 * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef IotWebConfTParameter_h #define IotWebConfTParameter_h // TODO: This file is a mess. Help wanted to organize thing! #include "IotWebConfParameter.h" #include #include #include // At least in PlatformIO, strtoimax/strtoumax are defined, but not implemented. #if 1 #define strtoimax strtoll #define strtoumax strtoull #endif namespace iotwebconf { /** * This class is to hide web related properties from the * data manipulation. */ class ConfigItemBridge : public ConfigItem { public: virtual void update(WebRequestWrapper* webRequestWrapper) override { if (webRequestWrapper->hasArg(this->getId())) { String newValue = webRequestWrapper->arg(this->getId()); this->update(newValue); } } void debugTo(Stream* out) override { out->print("'"); out->print(this->getId()); out->print("' with value: '"); out->print(this->toString()); out->println("'"); } protected: ConfigItemBridge(const char* id) : ConfigItem(id) { } virtual int getInputLength() { return 0; }; virtual bool update(String newValue, bool validateOnly = false) = 0; virtual String toString() = 0; }; /////////////////////////////////////////////////////////////////////////// /** * DataType is the data related part of the parameter. * It does not care about web and visualization, but takes care of the * data validation and storing. */ template class DataType : virtual public ConfigItemBridge { public: using DefaultValueType = _DefaultValueType; DataType(const char* id, DefaultValueType defaultValue) : ConfigItemBridge(id), _defaultValue(defaultValue) { } /** * value() can be used to get the value, but it can also * be used set it like this: p.value() = newValue */ ValueType& value() { return this->_value; } ValueType& operator*() { return this->_value; } protected: int getStorageSize() override { return sizeof(ValueType); } virtual bool update(String newValue, bool validateOnly = false) = 0; bool validate(String newValue) { return update(newValue, true); } virtual String toString() override { return String(this->_value); } ValueType _value; const DefaultValueType _defaultValue; }; /////////////////////////////////////////////////////////////////////////// class StringDataType : public DataType { public: using DataType::DataType; protected: virtual bool update(String newValue, bool validateOnly) override { if (!validateOnly) { this->_value = newValue; } return true; } virtual String toString() override { return this->_value; } }; /////////////////////////////////////////////////////////////////////////// template class CharArrayDataType : public DataType { public: using DataType::DataType; CharArrayDataType(const char* id, const char* defaultValue) : ConfigItemBridge::ConfigItemBridge(id), DataType::DataType(id, defaultValue) { }; virtual void applyDefaultValue() override { strncpy(this->_value, this->_defaultValue, len); } protected: virtual bool update(String newValue, bool validateOnly) override { if (newValue.length() + 1 > len) { return false; } if (!validateOnly) { #ifdef IOTWEBCONF_DEBUG_TO_SERIAL Serial.print(this->getId()); Serial.print(": "); Serial.println(newValue); #endif strncpy(this->_value, newValue.c_str(), len); } return true; } void storeValue(std::function doStore) override { SerializationData serializationData; serializationData.length = len; serializationData.data = (byte*)this->_value; doStore(&serializationData); } void loadValue(std::function doLoad) override { SerializationData serializationData; serializationData.length = len; serializationData.data = (byte*)this->_value; doLoad(&serializationData); } virtual int getInputLength() override { return len; }; }; /////////////////////////////////////////////////////////////////////////// /** * All non-complex types should be inherited from this base class. */ template class PrimitiveDataType : public DataType { public: using DataType::DataType; PrimitiveDataType(const char* id, ValueType defaultValue) : ConfigItemBridge::ConfigItemBridge(id), DataType::DataType(id, defaultValue) { }; void setMax(ValueType val) { this->_max = val; this->_maxDefined = true; } void setMin(ValueType val) { this->_min = val; this->_minDefined = true; } virtual void applyDefaultValue() override { this->_value = this->_defaultValue; } protected: virtual bool update(String newValue, bool validateOnly) override { errno = 0; ValueType val = fromString(newValue); if ((errno == ERANGE) || (this->_minDefined && (val < this->_min)) || (this->_maxDefined && (val > this->_max))) { #ifdef IOTWEBCONF_DEBUG_TO_SERIAL Serial.print(this->getId()); Serial.print(" value not accepted: "); Serial.println(val); #endif return false; } if (!validateOnly) { #ifdef IOTWEBCONF_DEBUG_TO_SERIAL Serial.print(this->getId()); Serial.print(": "); Serial.println((ValueType)val); #endif this->_value = (ValueType) val; } return true; } void storeValue(std::function doStore) override { SerializationData serializationData; serializationData.length = this->getStorageSize(); serializationData.data = reinterpret_cast(&this->_value); doStore(&serializationData); } void loadValue(std::function doLoad) override { byte buf[this->getStorageSize()]; SerializationData serializationData; serializationData.length = this->getStorageSize(); serializationData.data = buf; doLoad(&serializationData); ValueType* valuePointer = reinterpret_cast(buf); this->_value = *valuePointer; } virtual ValueType fromString(String stringValue) = 0; ValueType getMax() { return this->_max; } ValueType getMin() { return this->_min; } ValueType isMaxDefined() { return this->_maxDefined; } ValueType isMinDefined() { return this->_minDefined; } private: ValueType _min; ValueType _max; bool _minDefined = false; bool _maxDefined = false; }; /////////////////////////////////////////////////////////////////////////// template class SignedIntDataType : public PrimitiveDataType { public: SignedIntDataType(const char* id, ValueType defaultValue) : ConfigItemBridge::ConfigItemBridge(id), PrimitiveDataType::PrimitiveDataType(id, defaultValue) { }; protected: virtual ValueType fromString(String stringValue) { return (ValueType)strtoimax(stringValue.c_str(), nullptr, base); } }; template class UnsignedIntDataType : public PrimitiveDataType { public: UnsignedIntDataType(const char* id, ValueType defaultValue) : ConfigItemBridge::ConfigItemBridge(id), PrimitiveDataType::PrimitiveDataType(id, defaultValue) { }; protected: virtual ValueType fromString(String stringValue) { return (ValueType)strtoumax(stringValue.c_str(), nullptr, base); } }; class BoolDataType : public PrimitiveDataType { public: BoolDataType(const char* id, bool defaultValue) : ConfigItemBridge::ConfigItemBridge(id), PrimitiveDataType::PrimitiveDataType(id, defaultValue) { }; protected: virtual bool fromString(String stringValue) { return stringValue.c_str()[0] == 1; } }; class FloatDataType : public PrimitiveDataType { public: FloatDataType(const char* id, float defaultValue) : ConfigItemBridge::ConfigItemBridge(id), PrimitiveDataType::PrimitiveDataType(id, defaultValue) { }; protected: virtual float fromString(String stringValue) { return atof(stringValue.c_str()); } }; class DoubleDataType : public PrimitiveDataType { public: DoubleDataType(const char* id, double defaultValue) : ConfigItemBridge::ConfigItemBridge(id), PrimitiveDataType::PrimitiveDataType(id, defaultValue) { }; protected: virtual double fromString(String stringValue) { return strtod(stringValue.c_str(), nullptr); } }; ///////////////////////////////////////////////////////////////////////// class IpDataType : public DataType { using DataType::DataType; protected: virtual bool update(String newValue, bool validateOnly) override { if (validateOnly) { IPAddress ip; return ip.fromString(newValue); } else { return this->_value.fromString(newValue); } } virtual String toString() override { return this->_value.toString(); } }; /////////////////////////////////////////////////////////////////////////// /** * Input parameter is the part of the parameter that is responsible * for the appearance of the parameter in HTML. */ class InputParameter : virtual public ConfigItemBridge { public: InputParameter(const char* id, const char* label) : ConfigItemBridge::ConfigItemBridge(id), label(label) { } virtual void renderHtml( bool dataArrived, WebRequestWrapper* webRequestWrapper) override { String content = this->renderHtml( dataArrived, webRequestWrapper->hasArg(this->getId()), webRequestWrapper->arg(this->getId())); webRequestWrapper->sendContent(content); } const char* label; /** * This variable is meant to store a value that is displayed in an empty * (not filled) field. */ const char* placeholder = nullptr; virtual void setPlaceholder(const char* placeholder) { this->placeholder = placeholder; } /** * Usually this variable is used when rendering the form input field * so one can customize the rendered outcome of this particular item. */ const char* customHtml = nullptr; /** * Used when rendering the input field. Is is overridden by different * implementations. */ virtual String getCustomHtml() { return String(customHtml == nullptr ? "" : customHtml); } const char* errorMessage = nullptr; protected: void clearErrorMessage() override { this->errorMessage = nullptr; } virtual String renderHtml( bool dataArrived, bool hasValueFromPost, String valueFromPost) { String pitem = String(this->getHtmlTemplate()); pitem.replace("{b}", this->label); pitem.replace("{t}", this->getInputType()); pitem.replace("{i}", this->getId()); pitem.replace( "{p}", this->placeholder == nullptr ? "" : this->placeholder); int length = this->getInputLength(); if (length > 0) { char parLength[11]; snprintf(parLength, 11, "%d", length - 1); // To allow "\0" at the end of the string. String maxLength = String("maxlength=") + parLength; pitem.replace("{l}", maxLength); } else { pitem.replace("{l}", ""); } if (hasValueFromPost) { // -- Value from previous submit pitem.replace("{v}", valueFromPost); } else { // -- Value from config pitem.replace("{v}", this->toString()); } pitem.replace("{c}", this->getCustomHtml()); pitem.replace( "{s}", this->errorMessage == nullptr ? "" : "de"); // Div style class. pitem.replace( "{e}", this->errorMessage == nullptr ? "" : this->errorMessage); return pitem; } /** * One can override this method in case a specific HTML template is required * for a parameter. */ virtual String getHtmlTemplate() { return FPSTR(IOTWEBCONF_HTML_FORM_PARAM); }; virtual const char* getInputType() = 0; }; template class TextTParameter : public CharArrayDataType, public InputParameter { public: using CharArrayDataType::CharArrayDataType; TextTParameter(const char* id, const char* label, const char* defaultValue) : ConfigItemBridge(id), CharArrayDataType::CharArrayDataType(id, defaultValue), InputParameter::InputParameter(id, label) { } protected: virtual const char* getInputType() override { return "text"; } }; class CheckboxTParameter : public BoolDataType, public InputParameter { public: CheckboxTParameter(const char* id, const char* label, const bool defaultValue) : ConfigItemBridge(id), BoolDataType::BoolDataType(id, defaultValue), InputParameter::InputParameter(id, label) { } bool isChecked() { return this->value(); } protected: virtual const char* getInputType() override { return "checkbox"; } virtual void update(WebRequestWrapper* webRequestWrapper) override { bool selected = false; if (webRequestWrapper->hasArg(this->getId())) { String valueFromPost = webRequestWrapper->arg(this->getId()); selected = valueFromPost.equals("selected"); } // this->update(String(selected ? "1" : "0")); #ifdef IOTWEBCONF_DEBUG_TO_SERIAL Serial.print(this->getId()); Serial.print(": "); Serial.println(selected ? "selected" : "not selected"); #endif this->_value = selected; } virtual String renderHtml( bool dataArrived, bool hasValueFromPost, String valueFromPost) override { bool checkSelected = false; if (dataArrived) { if (hasValueFromPost && valueFromPost.equals("selected")) { checkSelected = true; } } else { if (this->isChecked()) { checkSelected = true; } } if (checkSelected) { this->customHtml = CheckboxTParameter::_checkedStr; } else { this->customHtml = nullptr; } return InputParameter::renderHtml(dataArrived, true, String("selected")); } private: const char* _checkedStr = "checked='checked'"; }; template class PasswordTParameter : public CharArrayDataType, public InputParameter { public: using CharArrayDataType::CharArrayDataType; PasswordTParameter(const char* id, const char* label, const char* defaultValue) : ConfigItemBridge(id), CharArrayDataType::CharArrayDataType(id, defaultValue), InputParameter::InputParameter(id, label) { this->customHtml = _customHtmlPwd; } void debugTo(Stream* out) { out->print("'"); out->print(this->getId()); out->print("' with value: "); #ifdef IOTWEBCONF_DEBUG_PWD_TO_SERIAL out->print("'"); out->print(this->_value); out->println("'"); #else out->println(F("")); #endif } virtual bool update(String newValue, bool validateOnly) override { if (newValue.length() + 1 > len) { return false; } if (validateOnly) { return true; } #ifdef IOTWEBCONF_DEBUG_TO_SERIAL Serial.print(this->getId()); Serial.print(": "); #endif if (newValue != this->_value) { // -- Value was set. strncpy(this->_value, newValue.c_str(), len); #ifdef IOTWEBCONF_DEBUG_TO_SERIAL # ifdef IOTWEBCONF_DEBUG_PWD_TO_SERIAL Serial.println(this->_value); # else Serial.println(""); # endif #endif } else { #ifdef IOTWEBCONF_DEBUG_TO_SERIAL Serial.println(""); #endif } return true; } protected: virtual const char* getInputType() override { return "password"; } virtual String renderHtml( bool dataArrived, bool hasValueFromPost, String valueFromPost) override { return InputParameter::renderHtml(dataArrived, true, String(this->_value)); } private: const char* _customHtmlPwd = "ondblclick=\"pw(this.id)\""; }; /** * All non-complex type input parameters should be inherited from this * base class. */ template class PrimitiveInputParameter : public InputParameter { public: PrimitiveInputParameter(const char* id, const char* label) : ConfigItemBridge::ConfigItemBridge(id), InputParameter::InputParameter(id, label) { } virtual String getCustomHtml() override { String modifiers = String(this->customHtml); if (this->isMinDefined()) { modifiers += " min='" ; modifiers += this->getMin(); modifiers += "'"; } if (this->isMaxDefined()) { modifiers += " max='"; modifiers += this->getMax(); modifiers += "'"; } if (this->step != 0) { modifiers += " step='"; modifiers += this->step; modifiers += "'"; } return modifiers; } ValueType step = 0; void setStep(ValueType step) { this->step = step; } virtual ValueType getMin() = 0; virtual ValueType getMax() = 0; virtual bool isMinDefined() = 0; virtual bool isMaxDefined() = 0; }; template class IntTParameter : public virtual SignedIntDataType, public PrimitiveInputParameter { public: IntTParameter(const char* id, const char* label, ValueType defaultValue) : ConfigItemBridge(id), SignedIntDataType::SignedIntDataType(id, defaultValue), PrimitiveInputParameter::PrimitiveInputParameter(id, label) { } // TODO: somehow organize these methods into common parent. virtual ValueType getMin() override { return PrimitiveDataType::getMin(); } virtual ValueType getMax() override { return PrimitiveDataType::getMax(); } virtual bool isMinDefined() override { return PrimitiveDataType::isMinDefined(); } virtual bool isMaxDefined() override { return PrimitiveDataType::isMaxDefined(); } protected: virtual const char* getInputType() override { return "number"; } }; template class UIntTParameter : public virtual UnsignedIntDataType, public PrimitiveInputParameter { public: UIntTParameter(const char* id, const char* label, ValueType defaultValue) : ConfigItemBridge(id), UnsignedIntDataType::UnsignedIntDataType(id, defaultValue), PrimitiveInputParameter::PrimitiveInputParameter(id, label) { } // TODO: somehow organize these methods into common parent. virtual ValueType getMin() override { return PrimitiveDataType::getMin(); } virtual ValueType getMax() override { return PrimitiveDataType::getMax(); } virtual bool isMinDefined() override { return PrimitiveDataType::isMinDefined(); } virtual bool isMaxDefined() override { return PrimitiveDataType::isMaxDefined(); } protected: virtual const char* getInputType() override { return "number"; } }; class FloatTParameter : public FloatDataType, public PrimitiveInputParameter { public: FloatTParameter(const char* id, const char* label, float defaultValue) : ConfigItemBridge(id), FloatDataType::FloatDataType(id, defaultValue), PrimitiveInputParameter::PrimitiveInputParameter(id, label) { } virtual float getMin() override { return PrimitiveDataType::getMin(); } virtual float getMax() override { return PrimitiveDataType::getMax(); } virtual bool isMinDefined() override { return PrimitiveDataType::isMinDefined(); } virtual bool isMaxDefined() override { return PrimitiveDataType::isMaxDefined(); } protected: virtual const char* getInputType() override { return "number"; } }; /** * Options parameter is a structure, that handles multiple values when redering * the HTML representation. */ template class OptionsTParameter : public TextTParameter { public: /** * @optionValues - List of values to choose from with, where each value * can have a maximal size of 'length'. Contains 'optionCount' items. * @optionNames - List of names to render for the values, where each * name can have a maximal size of 'nameLength'. Contains 'optionCount' * items. * @optionCount - Size of both 'optionValues' and 'optionNames' lists. * @nameLength - Size of any item in optionNames list. * (See TextParameter for arguments!) */ OptionsTParameter( const char* id, const char* label, const char* defaultValue, const char* optionValues, const char* optionNames, size_t optionCount, size_t nameLength) : ConfigItemBridge(id), TextTParameter(id, label, defaultValue) { this->_optionValues = optionValues; this->_optionNames = optionNames; this->_optionCount = optionCount; this->_nameLength = nameLength; } // TODO: make these protected void setOptionValues(const char* optionValues) { this->_optionValues = optionValues; } void setOptionNames(const char* optionNames) { this->_optionNames = optionNames; } void setOptionCount(size_t optionCount) { this->_optionCount = optionCount; } void setNameLength(size_t nameLength) { this->_nameLength = nameLength; } protected: OptionsTParameter( const char* id, const char* label, const char* defaultValue) : ConfigItemBridge(id), TextTParameter(id, label, defaultValue) { } const char* _optionValues; const char* _optionNames; size_t _optionCount; size_t _nameLength; }; /////////////////////////////////////////////////////////////////////////////// /** * Select parameter is an option parameter, that rendered as HTML SELECT. * Basically it is a dropdown combobox. */ template class SelectTParameter : public OptionsTParameter { public: /** * Create a select parameter for the config portal. * * (See OptionsParameter for arguments!) */ SelectTParameter( const char* id, const char* label, const char* defaultValue, const char* optionValues, const char* optionNames, size_t optionCount, size_t nameLength) : ConfigItemBridge(id), OptionsTParameter( id, label, defaultValue, optionValues, optionNames, optionCount, nameLength) { } // TODO: make this protected SelectTParameter( const char* id, const char* label, const char* defaultValue) : ConfigItemBridge(id), OptionsTParameter(id, label, defaultValue) { } protected: // Overrides virtual String renderHtml( bool dataArrived, bool hasValueFromPost, String valueFromPost) override { String options = ""; for (size_t i=0; i_optionCount; i++) { const char *optionValue = (this->_optionValues + (i*len) ); const char *optionName = (this->_optionNames + (i*this->_nameLength) ); String oitem = FPSTR(IOTWEBCONF_HTML_FORM_OPTION); oitem.replace("{v}", optionValue); // if (sizeof(this->_optionNames) > i) { oitem.replace("{n}", optionName); } // else // { // oitem.replace("{n}", "?"); // } if ((hasValueFromPost && (valueFromPost == optionValue)) || (strncmp(this->value(), optionValue, len) == 0)) { // -- Value from previous submit oitem.replace("{s}", " selected"); } else { // -- Value from config oitem.replace("{s}", ""); } options += oitem; } String pitem = FPSTR(IOTWEBCONF_HTML_FORM_SELECT_PARAM); pitem.replace("{b}", this->label); pitem.replace("{i}", this->getId()); pitem.replace( "{c}", this->customHtml == nullptr ? "" : this->customHtml); pitem.replace( "{s}", this->errorMessage == nullptr ? "" : "de"); // Div style class. pitem.replace( "{e}", this->errorMessage == nullptr ? "" : this->errorMessage); pitem.replace("{o}", options); return pitem; } private: }; /////////////////////////////////////////////////////////////////////////////// /** * Color chooser. */ class ColorTParameter : public CharArrayDataType<8>, public InputParameter { public: using CharArrayDataType<8>::CharArrayDataType; ColorTParameter(const char* id, const char* label, const char* defaultValue) : ConfigItemBridge(id), CharArrayDataType<8>::CharArrayDataType(id, defaultValue), InputParameter::InputParameter(id, label) { } protected: virtual const char* getInputType() override { return "color"; } }; /////////////////////////////////////////////////////////////////////////////// /** * Date chooser. */ class DateTParameter : public CharArrayDataType<11>, public InputParameter { public: using CharArrayDataType<11>::CharArrayDataType; DateTParameter(const char* id, const char* label, const char* defaultValue) : ConfigItemBridge(id), CharArrayDataType<11>::CharArrayDataType(id, defaultValue), InputParameter::InputParameter(id, label) { } protected: virtual const char* getInputType() override { return "date"; } }; /////////////////////////////////////////////////////////////////////////////// /** * Time chooser. */ class TimeTParameter : public CharArrayDataType<6>, public InputParameter { public: using CharArrayDataType<6>::CharArrayDataType; TimeTParameter(const char* id, const char* label, const char* defaultValue) : ConfigItemBridge(id), CharArrayDataType<6>::CharArrayDataType(id, defaultValue), InputParameter::InputParameter(id, label) { } protected: virtual const char* getInputType() override { return "time"; } }; } // end namespace #include "IotWebConfTParameterBuilder.h" #endif