diff --git a/include/eepp/network/cookiemanager.hpp b/include/eepp/network/cookiemanager.hpp new file mode 100644 index 000000000..4afc7d95c --- /dev/null +++ b/include/eepp/network/cookiemanager.hpp @@ -0,0 +1,46 @@ +#ifndef EE_NETWORK_COOKIEMANAGER_HPP +#define EE_NETWORK_COOKIEMANAGER_HPP + +#include +#include + +#include +#include + +namespace EE { namespace Network { + +class EE_API CookieManager { + public: + CookieManager(); + + /** Store Set-Cookie headers from an HTTP response for the given domain. */ + void storeCookies( const std::string& domain, const Http::Response& response ); + + /** Store cookies from a raw Set-Cookie header string. */ + void storeCookiesFromHeader( const std::string& domain, const std::string& setCookieHeader ); + + /** Build the Cookie header string for outgoing requests to the given domain. */ + std::string getCookieHeader( const std::string& domain ) const; + + /** Remove all stored cookies. */ + void clear(); + + /** @return The number of cookie entries across all domains. */ + size_t size() const; + + /** @return true if no cookies are stored. */ + bool empty() const; + + /** @return true if the domain has cookies */ + bool hasCookie( const std::string& domain ) const; + + protected: + mutable Mutex mMutex; + UnorderedMap> mCookies; + + void parseSetCookie( const std::string& domain, const std::string& setCookieHeader ); +}; + +}} // namespace EE::Network + +#endif diff --git a/include/eepp/network/http.hpp b/include/eepp/network/http.hpp index 246b7b564..67d2d01fb 100644 --- a/include/eepp/network/http.hpp +++ b/include/eepp/network/http.hpp @@ -49,8 +49,9 @@ class EE_API Http : NonCopyable { MultipleChoices = 300, ///< The requested page can be accessed from several locations MovedPermanently = 301, ///< The requested page has permanently moved to a new location MovedTemporarily = 302, ///< The requested page has temporarily moved to a new location - NotModified = 304, ///< For conditional requests, means the requested page hasn't - ///< changed and doesn't need to be refreshed + SeeOther = 303, ///< The response can be found under a different URI using a GET method + NotModified = 304, ///< For conditional requests, means the requested page hasn't + ///< changed and doesn't need to be refreshed TemporaryRedirect = 307, ///< The requested page has temporarily moved to a new location PermanentRedirect = 308, ///< The requested page has permanently moved to a new location @@ -96,6 +97,8 @@ class EE_API Http : NonCopyable { FieldTable getHeaders(); + const FieldTable& getHeaders() const; + /** @brief Get the value of a field ** If the field @a field is not found in the response header, ** the empty string is returned. This function uses @@ -180,7 +183,7 @@ class EE_API Http : NonCopyable { ///< target resource. Patch, ///< The PATCH method is used to apply partial modifications to a resource. Connect ///< The CONNECT method starts two-way communications with the requested - ///< resource. It can be used to open a tunnel. + ///< resource. It can be used to open a tunnel. }; /** @brief Enumerate the available states for a request */ @@ -188,7 +191,8 @@ class EE_API Http : NonCopyable { Connected, ///< Connected to server. Sent, ///< Request sent to the server. HeaderReceived, ///< Header received. - ContentReceived ///< Content received. + ContentReceived, ///< Content received. + Redirect, ///< A redirect has been handled }; static std::string statusToString( Status status ); @@ -199,6 +203,9 @@ class EE_API Http : NonCopyable { /** @return The method string from a method */ static std::string methodToString( const Method& method ); + static Method getRedirectMethodFromStatus( Method requestMethod, + Response::Status responseStatus ); + /** @brief Default constructor ** This constructor creates a GET request, with the root ** URI ("/") and an empty body. @@ -675,25 +682,29 @@ class EE_API Http : NonCopyable { const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "", const bool& validateCertificate = true, - const URI& proxy = URI() ); + const URI& proxy = URI(), bool followRedirect = true ); /** Creates an async HTTP GET Request using the global HTTP Client Pool ** @return The unique async request id */ - static Uint64 getAsync( - const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout = Time::Zero, - const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), - const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "", - const bool& validateCertificate = true, const URI& proxy = URI() ); + static Uint64 + getAsync( const Http::AsyncResponseCallback& cb, const URI& uri, + const Time& timeout = Time::Zero, + const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), + const Request::FieldTable& headers = Request::FieldTable(), + const std::string& body = "", const bool& validateCertificate = true, + const URI& proxy = URI(), bool followRedirect = true ); /** Creates an async HTTP POST Request using the global HTTP Client Pool ** @return The unique async request id */ - static Uint64 postAsync( - const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout = Time::Zero, - const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), - const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "", - const bool& validateCertificate = true, const URI& proxy = URI() ); + static Uint64 + postAsync( const Http::AsyncResponseCallback& cb, const URI& uri, + const Time& timeout = Time::Zero, + const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), + const Request::FieldTable& headers = Request::FieldTable(), + const std::string& body = "", const bool& validateCertificate = true, + const URI& proxy = URI(), bool followRedirect = true ); /** It will try to get the proxy from the environment variables. */ static URI getEnvProxyURI(); diff --git a/include/eepp/scene/node.hpp b/include/eepp/scene/node.hpp index 005452cdc..923434ab8 100644 --- a/include/eepp/scene/node.hpp +++ b/include/eepp/scene/node.hpp @@ -1777,6 +1777,13 @@ class EE_API Node : public Transformable { */ bool isClosing() const; + /** + * @brief Checks if the node is marked for closure or any node in its parent tree. + * + * @return True if node is about to close + */ + bool inClosingTree() const; + /** * @brief Checks if the node is in the process of closing children. * diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp index 924d7e8f4..f7408eaca 100644 --- a/include/eepp/ui/css/propertydefinition.hpp +++ b/include/eepp/ui/css/propertydefinition.hpp @@ -251,6 +251,9 @@ enum class PropertyId : Uint32 { ListStylePosition = String::hash( "list-style-position" ), ListStyleImage = String::hash( "list-style-image" ), DataLanguage = String::hash( "data-language" ), // Minor hack + Action = String::hash( "action" ), + Method = String::hash( "method" ), + Enctype = String::hash( "enctype" ), }; enum class PropertyType : Uint32 { diff --git a/include/eepp/ui/uihelper.hpp b/include/eepp/ui/uihelper.hpp index fecbd2837..bc151ccea 100644 --- a/include/eepp/ui/uihelper.hpp +++ b/include/eepp/ui/uihelper.hpp @@ -130,6 +130,7 @@ enum UINodeType { UI_TYPE_HTML_BODY, UI_TYPE_HTML_LIST_ITEM, UI_TYPE_HTML_IMAGE, + UI_TYPE_HTML_FORM, UI_TYPE_SVG, UI_TYPE_TEXTNODE, UI_TYPE_MODULES = 10000, diff --git a/include/eepp/ui/uihtmlform.hpp b/include/eepp/ui/uihtmlform.hpp new file mode 100644 index 000000000..a172b0d92 --- /dev/null +++ b/include/eepp/ui/uihtmlform.hpp @@ -0,0 +1,56 @@ +#ifndef EE_UI_UIHTMLFORM_HPP +#define EE_UI_UIHTMLFORM_HPP + +#include +#include +#include + +#include + +namespace EE { namespace UI { + +class UISceneNode; + +class EE_API UIHTMLForm : public UIRichText { + public: + static UIHTMLForm* New(); + + UIHTMLForm( const std::string& tag = "form" ); + + virtual Uint32 getType() const; + + virtual bool isType( const Uint32& type ) const; + + virtual bool applyProperty( const StyleSheetProperty& attribute ); + + virtual std::string getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex = 0 ) const; + + virtual std::vector getPropertiesImplemented() const; + + void submit(); + + const std::string& getAction() const { return mAction; } + void setAction( const std::string& action ) { mAction = action; } + + const std::string& getMethod() const { return mMethod; } + void setMethod( const std::string& method ) { mMethod = method; } + + const std::string& getEnctype() const { return mEnctype; } + void setEnctype( const std::string& enctype ) { mEnctype = enctype; } + + protected: + std::string mAction; + std::string mMethod{ "GET" }; + std::string mEnctype{ "application/x-www-form-urlencoded" }; + + virtual Uint32 onMessage( const NodeMessage* msg ); + + static void collectFormData( Node* node, + std::vector>& fields ); + bool isSubmitTrigger( Node* sender ) const; +}; + +}} // namespace EE::UI + +#endif diff --git a/include/eepp/ui/htmlinput.hpp b/include/eepp/ui/uihtmlinput.hpp similarity index 81% rename from include/eepp/ui/htmlinput.hpp rename to include/eepp/ui/uihtmlinput.hpp index cc2520ee1..91ce7f616 100644 --- a/include/eepp/ui/htmlinput.hpp +++ b/include/eepp/ui/uihtmlinput.hpp @@ -1,15 +1,15 @@ -#ifndef EE_UI_HTMLINPUT_HPP -#define EE_UI_HTMLINPUT_HPP +#ifndef EE_UI_UIHTMLINPUT_HPP +#define EE_UI_UIHTMLINPUT_HPP #include namespace EE { namespace UI { -class EE_API HTMLInput : public UIWidget { +class EE_API UIHTMLInput : public UIWidget { public: - static HTMLInput* New(); + static UIHTMLInput* New(); - HTMLInput(); + UIHTMLInput(); virtual Uint32 getType() const; @@ -32,10 +32,13 @@ class EE_API HTMLInput : public UIWidget { UIWidget* getChildWidget() const; + String getFormValue() const; + protected: std::string mInputType{ "text" }; UIWidget* mChildWidget{ nullptr }; std::map mProperties; + String mValue; void createChildWidget(); diff --git a/include/eepp/ui/htmltextarea.hpp b/include/eepp/ui/uihtmltextarea.hpp similarity index 84% rename from include/eepp/ui/htmltextarea.hpp rename to include/eepp/ui/uihtmltextarea.hpp index ac4f65d2b..a14bfb2ff 100644 --- a/include/eepp/ui/htmltextarea.hpp +++ b/include/eepp/ui/uihtmltextarea.hpp @@ -1,15 +1,15 @@ -#ifndef EE_UI_HTMLTEXTAREA_HPP -#define EE_UI_HTMLTEXTAREA_HPP +#ifndef EE_UI_UIHTMLTEXTAREA_HPP +#define EE_UI_UIHTMLTEXTAREA_HPP #include namespace EE { namespace UI { -class EE_API HTMLTextArea : public UITextEdit { +class EE_API UIHTMLTextArea : public UITextEdit { public: - static HTMLTextArea* New(); + static UIHTMLTextArea* New(); - HTMLTextArea(); + UIHTMLTextArea(); virtual Uint32 getType() const; diff --git a/include/eepp/ui/uihtmlwidget.hpp b/include/eepp/ui/uihtmlwidget.hpp index 16fd915e4..865d58fde 100644 --- a/include/eepp/ui/uihtmlwidget.hpp +++ b/include/eepp/ui/uihtmlwidget.hpp @@ -59,6 +59,8 @@ class EE_API UIHTMLWidget : public UILayout { virtual bool isMergeable() const { return false; } + virtual String getFormValue() const { return String(); } + virtual void invalidateIntrinsicSize(); bool isOutOfFlow() const; diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index 5dc830291..34b564f71 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -27,6 +28,13 @@ class UIWidget; class UILayout; class UIIcon; +struct NavigationRequest { + URI uri; + std::string method{ "GET" }; + std::string body; + std::map extraHeaders; +}; + class EE_API UISceneNode : public SceneNode { public: /** @@ -704,10 +712,14 @@ class EE_API UISceneNode : public SceneNode { /** Handles opening an specific URI */ void openURL( URI uri ); - /* Sets a callback to intercept the openURL calls, returns true if intercepted, false to leave - * the default openURL implementation handle it. - */ - void setURLInterceptorCb( std::function cb ) { mURLInterceptorCb = cb; }; + /** Handles navigation (GET/POST) with request body and custom headers. */ + void navigate( const NavigationRequest& request ); + + /** Sets a callback to intercept navigate() calls. Return true to handle the request, + * false to fall through to the URL interceptor and default handling. */ + void setNavigationInterceptorCb( std::function cb ) { + mNavigationInterceptorCb = cb; + }; /** * Solves a relative path with no scheme or authority into a complete URI. @@ -718,6 +730,10 @@ class EE_API UISceneNode : public SceneNode { /** @return The document referer */ URI getReferer() const { return mReferer; }; + const Network::CookieManager& getCookieManager() const { return mCookieManager; } + + Network::CookieManager& getCookieManager() { return mCookieManager; } + protected: friend class EE::UI::UIWindow; friend class EE::UI::UIWidget; @@ -747,7 +763,8 @@ class EE_API UISceneNode : public SceneNode { std::shared_ptr mThreadPool; URI mURI; URI mReferer; - std::function mURLInterceptorCb; + std::function mNavigationInterceptorCb; + Network::CookieManager mCookieManager; /** * @brief Protected constructor. diff --git a/src/eepp/network/cookiemanager.cpp b/src/eepp/network/cookiemanager.cpp new file mode 100644 index 000000000..cb285780f --- /dev/null +++ b/src/eepp/network/cookiemanager.cpp @@ -0,0 +1,79 @@ +#include +#include + +namespace EE { namespace Network { + +CookieManager::CookieManager() {} + +void CookieManager::storeCookies( const std::string& domain, + const Http::Response& response ) { + std::string setCookie = response.getField( "set-cookie" ); + if ( !setCookie.empty() ) + parseSetCookie( domain, setCookie ); +} + +void CookieManager::storeCookiesFromHeader( const std::string& domain, + const std::string& setCookieHeader ) { + if ( !setCookieHeader.empty() ) + parseSetCookie( domain, setCookieHeader ); +} + +std::string CookieManager::getCookieHeader( const std::string& domain ) const { + Lock l( mMutex ); + auto it = mCookies.find( domain ); + if ( it == mCookies.end() || it->second.empty() ) + return ""; + + std::string header; + for ( const auto& pair : it->second ) { + if ( !header.empty() ) + header += "; "; + header += pair.first + "=" + pair.second; + } + return header; +} + +void CookieManager::clear() { + Lock l( mMutex ); + mCookies.clear(); +} + +size_t CookieManager::size() const { + Lock l( mMutex ); + size_t total = 0; + for ( const auto& domainCookies : mCookies ) + total += domainCookies.second.size(); + return total; +} + +bool CookieManager::empty() const { + Lock l( mMutex ); + return mCookies.empty(); +} + +void CookieManager::parseSetCookie( const std::string& domain, + const std::string& setCookieHeader ) { + Lock l( mMutex ); + size_t end = setCookieHeader.find( ';' ); + std::string_view cookiePair( end != std::string::npos + ? std::string_view( setCookieHeader ).substr( 0, end ) + : std::string_view( setCookieHeader ) ); + + size_t eq = cookiePair.find( '=' ); + if ( eq == std::string::npos ) + return; + + std::string name( cookiePair.substr( 0, eq ) ); + std::string value( cookiePair.substr( eq + 1 ) ); + + if ( name.empty() ) + return; + + mCookies[domain][String::trim( name )] = String::trim( value ); +} + +bool CookieManager::hasCookie( const std::string& domain ) const { + return mCookies.find( domain ) != mCookies.end(); +} + +}} // namespace EE::Network diff --git a/src/eepp/network/http.cpp b/src/eepp/network/http.cpp index ed8bce07c..5a0875650 100644 --- a/src/eepp/network/http.cpp +++ b/src/eepp/network/http.cpp @@ -48,6 +48,8 @@ std::string Http::Request::statusToString( Http::Request::Status status ) { return "HeaderReceived"; case ContentReceived: return "ContentReceived"; + case Redirect: + return "Redirect"; } return ""; } @@ -449,6 +451,10 @@ Http::Response Http::Response::createFakeResponse( const Http::Response::FieldTa Http::Response::Response() : mStatus( ConnectionFailed ), mMajorVersion( 0 ), mMinorVersion( 0 ) {} +const Http::Response::FieldTable& Http::Response::getHeaders() const { + return mFields; +} + Http::Response::FieldTable Http::Response::getHeaders() { return mFields; } @@ -653,11 +659,13 @@ Uint64 Http::requestAsync( const Http::AsyncResponseCallback& cb, const URI& uri const Time& timeout, Request::Method method, const Http::Request::ProgressCallback& progressCallback, const Http::Request::FieldTable& headers, const std::string& body, - const bool& validateCertificate, const URI& proxy ) { + const bool& validateCertificate, const URI& proxy, + bool followRedirect ) { auto http = sGlobalHttpPool.get( uri, proxy ); Request request( uri.getPathAndQuery(), method, body, validateCertificate, validateCertificate, true, true ); request.setProgressCallback( progressCallback ); + request.setFollowRedirect( followRedirect ); for ( const auto& field : headers ) request.setField( field.first, field.second ); @@ -668,17 +676,17 @@ Uint64 Http::requestAsync( const Http::AsyncResponseCallback& cb, const URI& uri Uint64 Http::getAsync( const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout, const Http::Request::ProgressCallback& progressCallback, const Http::Request::FieldTable& headers, const std::string& body, - const bool& validateCertificate, const URI& proxy ) { + const bool& validateCertificate, const URI& proxy, bool followRedirect ) { return requestAsync( cb, uri, timeout, Request::Method::Get, progressCallback, headers, body, - validateCertificate, proxy ); + validateCertificate, proxy, followRedirect ); } Uint64 Http::postAsync( const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout, const Http::Request::ProgressCallback& progressCallback, const Http::Request::FieldTable& headers, const std::string& body, - const bool& validateCertificate, const URI& proxy ) { + const bool& validateCertificate, const URI& proxy, bool followRedirect ) { return requestAsync( cb, uri, timeout, Request::Method::Post, progressCallback, headers, body, - validateCertificate, proxy ); + validateCertificate, proxy, followRedirect ); } Http::Http() : mConnection( NULL ), mHost(), mPort( 0 ), mIsSSL( false ), mHostSolved( false ) {} @@ -1091,25 +1099,45 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr eeSAFE_DELETE( chunkedStream ); eeSAFE_DELETE( inflateStream ); - Http::Request newRequest( request ); - newRequest.setUri( uri.getPathAndQuery() ); - - request.mRedirectionCount++; - newRequest.mRedirectionCount = - request.mRedirectionCount; - - // Same host, expects a path in the same domain - if ( uri.getHost().empty() || - uri.getHost() == getHostName() ) { - return downloadRequest( newRequest, writeTo, - timeout ); + if ( !request.isCancelled() && + !sendProgress( *this, request, received, + Request::Redirect, contentLength, + currentTotalBytes ) ) { + request.mCancel = true; } else { - // New host, we need to solve the host - Http http( uri.getHost(), uri.getPort(), - uri.getScheme() == "https" ? true - : false ); - return http.downloadRequest( newRequest, writeTo, - timeout ); + Http::Request newRequest( request ); + newRequest.setUri( uri.getPathAndQuery() ); + newRequest.setMethod( + Http::Request::getRedirectMethodFromStatus( + request.getMethod(), + received.getStatus() ) ); + + newRequest.setProgressCallback( + request.getProgressCallback() ); + + if ( received.hasField( "set-cookie" ) ) { + newRequest.setField( + "Cookie", + received.getField( "set-cookie" ) ); + } + + request.mRedirectionCount++; + newRequest.mRedirectionCount = + request.mRedirectionCount; + + // Same host, expects a path in the same domain + if ( uri.getHost().empty() || + uri.getHost() == getHostName() ) { + return downloadRequest( newRequest, writeTo, + timeout ); + } else { + // New host, we need to solve the host + Http http( uri.getHost(), uri.getPort(), + uri.getScheme() == "https" ? true + : false ); + return http.downloadRequest( newRequest, + writeTo, timeout ); + } } } } @@ -1195,6 +1223,26 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr return received; } +Http::Request::Method +Http::Request::getRedirectMethodFromStatus( Method requestMethod, + Response::Status responseStatus ) { + // 1. 307 and 308 ALWAYS preserve the original method. + if ( responseStatus == Http::Response::PermanentRedirect || + responseStatus == Http::Response::TemporaryRedirect ) { + return requestMethod; + } + // 2. 303 See Other ALWAYS converts to GET (unless it + // was a HEAD). + if ( responseStatus == Http::Response::SeeOther ) { + return requestMethod == Http::Request::Method::Head ? Http::Request::Method::Head + : Http::Request::Method::Get; + } + // 3. 301 and 302 historically convert POST to GET, but + // preserve others. + return requestMethod == Http::Request::Method::Post ? Http::Request::Method::Get + : requestMethod; +} + void Http::endConnection() { if ( mConnection && !mConnection->isKeepAlive() ) { if ( mConnection->isConnected() ) diff --git a/src/eepp/scene/node.cpp b/src/eepp/scene/node.cpp index 6b0f94c42..68ae0889d 100644 --- a/src/eepp/scene/node.cpp +++ b/src/eepp/scene/node.cpp @@ -775,6 +775,18 @@ bool Node::isClosing() const { return 0 != ( mNodeFlags & NODE_FLAG_CLOSE ); } +bool Node::inClosingTree() const { + if ( isClosing() ) + return true; + Node* parent = mParentNode; + while ( parent != nullptr ) { + if ( parent->isClosing() ) + return true; + parent = parent->mParentNode; + } + return false; +} + bool Node::isClosingChildren() const { return 0 != ( mNodeFlags & NODE_FLAG_CLOSING_CHILDREN ); } @@ -784,7 +796,7 @@ const String::HashType& Node::getIdHash() const { } Node* Node::findIdHash( const String::HashType& idHash ) const { - if ( !isClosing() && mIdHash == idHash ) { + if ( !isClosing() && mIdHash == idHash && !inClosingTree() ) { return const_cast( this ); } else { Node* child = mChild; @@ -821,7 +833,7 @@ Node* Node::hasChild( const std::string& id ) const { } Node* Node::findByType( const Uint32& type ) const { - if ( !isClosing() && isType( type ) ) { + if ( !isClosing() && isType( type ) && !inClosingTree() ) { return const_cast( this ); } else { Node* child = mChild; @@ -839,7 +851,7 @@ Node* Node::findByType( const Uint32& type ) const { std::vector Node::findAllByType( const Uint32& type ) const { std::vector nodes; - if ( !isClosing() && isType( type ) ) + if ( !isClosing() && isType( type ) && !inClosingTree() ) nodes.push_back( const_cast( this ) ); Node* child = mChild; diff --git a/src/eepp/ui/blocklayouter.cpp b/src/eepp/ui/blocklayouter.cpp index bae9a6309..63068ddb3 100644 --- a/src/eepp/ui/blocklayouter.cpp +++ b/src/eepp/ui/blocklayouter.cpp @@ -137,6 +137,9 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) { constexpr Float lowF = std::numeric_limits::lowest(); Rectf bounds( maxF, maxF, lowF, lowF ); + if ( !node->isVisible() ) + return bounds; + // UITextNode is a logical marker; its text is rendered by the // RichText engine — just advance the character index and return // empty bounds so it does not affect any widget's geometry. diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index e9f85813a..d4365b460 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -469,6 +469,11 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "data-language", "" ).setType( PropertyType::String ); + registerProperty( "action", "" ).setType( PropertyType::String ); + registerProperty( "method", "GET" ).setType( PropertyType::String ); + registerProperty( "enctype", "application/x-www-form-urlencoded" ) + .setType( PropertyType::String ); + // Shorthands registerShorthand( "margin", { "margin-top", "margin-right", "margin-bottom", "margin-left" }, "box" ); diff --git a/src/eepp/ui/uihtmlform.cpp b/src/eepp/ui/uihtmlform.cpp new file mode 100644 index 000000000..b6af9bda9 --- /dev/null +++ b/src/eepp/ui/uihtmlform.cpp @@ -0,0 +1,196 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace EE { namespace UI { + +UIHTMLForm* UIHTMLForm::New() { + return eeNew( UIHTMLForm, () ); +} + +UIHTMLForm::UIHTMLForm( const std::string& tag ) : UIRichText( tag ) {} + +Uint32 UIHTMLForm::getType() const { + return static_cast( UI_TYPE_HTML_FORM ); +} + +bool UIHTMLForm::isType( const Uint32& type ) const { + return UI_TYPE_HTML_FORM == type ? true : UIRichText::isType( type ); +} + +bool UIHTMLForm::applyProperty( const StyleSheetProperty& attribute ) { + if ( !attribute.getPropertyDefinition() ) + return false; + + switch ( attribute.getPropertyDefinition()->getPropertyId() ) { + case PropertyId::Action: + mAction = attribute.value(); + return true; + case PropertyId::Method: + mMethod = attribute.value(); + return true; + case PropertyId::Enctype: + mEnctype = attribute.value(); + return true; + default: + break; + } + + return UIRichText::applyProperty( attribute ); +} + +std::string UIHTMLForm::getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex ) const { + if ( !propertyDef ) + return ""; + + switch ( propertyDef->getPropertyId() ) { + case PropertyId::Action: + return mAction; + case PropertyId::Method: + return mMethod; + case PropertyId::Enctype: + return mEnctype; + default: + break; + } + + return UIRichText::getPropertyString( propertyDef, propertyIndex ); +} + +std::vector UIHTMLForm::getPropertiesImplemented() const { + auto props = UIRichText::getPropertiesImplemented(); + props.push_back( PropertyId::Action ); + props.push_back( PropertyId::Method ); + props.push_back( PropertyId::Enctype ); + return props; +} + +void UIHTMLForm::submit() { + std::vector> fields; + collectFormData( this, fields ); + + UISceneNode* sceneNode = getUISceneNode(); + + NavigationRequest request; + request.uri = URI( mAction ); + + if ( mEnctype == "multipart/form-data" ) { + request.method = "POST"; + Http::MultipartEntitiesBuilder builder; + for ( auto& field : fields ) + builder.addParameter( field.first, field.second ); + request.body = builder.build(); + request.extraHeaders["Content-Type"] = builder.getContentType(); + } else if ( mEnctype == "text/plain" ) { + request.method = "POST"; + for ( size_t i = 0; i < fields.size(); i++ ) { + if ( i > 0 ) + request.body += "\r\n"; + request.body += fields[i].first + "=" + fields[i].second; + } + request.extraHeaders["Content-Type"] = mEnctype; + } else { + std::string queryString; + for ( size_t i = 0; i < fields.size(); i++ ) { + if ( i > 0 ) + queryString += "&"; + queryString += URI::encode( fields[i].first ) + "=" + URI::encode( fields[i].second ); + } + + if ( mMethod == "GET" ) { + if ( !queryString.empty() ) { + std::string existingQuery = request.uri.getQuery(); + if ( existingQuery.empty() ) + request.uri.setQuery( queryString ); + else + request.uri.setQuery( existingQuery + "&" + queryString ); + request.uri.setRawQuery( queryString ); + } + } else { + request.method = "POST"; + request.body = queryString; + request.extraHeaders["Content-Type"] = mEnctype; + } + } + + sceneNode->navigate( request ); +} + +static String getWidgetFormValue( UIWidget* widget ) { + if ( widget->isType( UI_TYPE_HTML_WIDGET ) ) + return static_cast( widget )->getFormValue(); + if ( widget->isType( UI_TYPE_HTML_INPUT ) ) + return static_cast( widget )->getFormValue(); + if ( widget->isType( UI_TYPE_TEXTINPUT ) ) + return static_cast( widget )->getText(); + if ( widget->isType( UI_TYPE_TEXTEDIT ) ) + return static_cast( widget )->getText(); + return String(); +} + +void UIHTMLForm::collectFormData( Node* node, + std::vector>& fields ) { + if ( !node ) + return; + + UIWidget* widget = node->asType(); + if ( widget ) { + std::string name; + UIStyle* style = widget->getUIStyle(); + if ( style ) { + const CSS::StyleSheetProperty* prop = style->getProperty( PropertyId::Name ); + if ( prop ) + name = prop->value(); + } + if ( name.empty() ) + name = widget->getPropertyString( "name" ); + if ( !name.empty() ) { + String value = getWidgetFormValue( widget ); + if ( !value.empty() ) + fields.emplace_back( name, value.toUtf8() ); + } + } + + Node* child = node->getFirstChild(); + while ( child ) { + collectFormData( child, fields ); + child = child->getNextNode(); + } +} + +Uint32 UIHTMLForm::onMessage( const NodeMessage* msg ) { + if ( msg->getMsg() == NodeMessage::MouseClick && isSubmitTrigger( msg->getSender() ) ) { + submit(); + return 1; + } + return UIRichText::onMessage( msg ); +} + +bool UIHTMLForm::isSubmitTrigger( Node* sender ) const { + while ( sender ) { + if ( sender->isWidget() ) { + auto* widget = static_cast( sender ); + if ( widget->isType( UI_TYPE_HTML_INPUT ) && + static_cast( widget )->getInputType() == "submit" ) + return true; + if ( widget->isType( UI_TYPE_PUSHBUTTON ) && + widget->getPropertyString( "type" ) == "submit" ) + return true; + } + sender = sender->getParent(); + } + return false; +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/htmlinput.cpp b/src/eepp/ui/uihtmlinput.cpp similarity index 57% rename from src/eepp/ui/htmlinput.cpp rename to src/eepp/ui/uihtmlinput.cpp index 734e2f1ea..f3bc670f3 100644 --- a/src/eepp/ui/htmlinput.cpp +++ b/src/eepp/ui/uihtmlinput.cpp @@ -1,40 +1,46 @@ #include -#include #include #include +#include +#include #include #include #include #include +#include namespace EE { namespace UI { -HTMLInput* HTMLInput::New() { - return eeNew( HTMLInput, () ); +UIHTMLInput* UIHTMLInput::New() { + return eeNew( UIHTMLInput, () ); } -HTMLInput::HTMLInput() : UIWidget( "input" ) { +UIHTMLInput::UIHTMLInput() : UIWidget( "input" ) { mFlags |= UI_HTML_ELEMENT; mWidthPolicy = SizePolicy::WrapContent; mHeightPolicy = SizePolicy::WrapContent; createChildWidget(); } -Uint32 HTMLInput::getType() const { +Uint32 UIHTMLInput::getType() const { return UI_TYPE_HTML_INPUT; } -bool HTMLInput::isType( const Uint32& type ) const { - return HTMLInput::getType() == type || UIWidget::isType( type ); +bool UIHTMLInput::isType( const Uint32& type ) const { + return UIHTMLInput::getType() == type || UIWidget::isType( type ); } -bool HTMLInput::applyProperty( const StyleSheetProperty& attribute ) { +bool UIHTMLInput::applyProperty( const StyleSheetProperty& attribute ) { if ( !attribute.getPropertyDefinition() ) return false; PropertyId id = attribute.getPropertyDefinition()->getPropertyId(); switch ( id ) { + case PropertyId::Value: + case PropertyId::Text: + mValue = attribute.value(); + break; case PropertyId::Type: setInputType( attribute.value() ); return true; @@ -52,12 +58,14 @@ bool HTMLInput::applyProperty( const StyleSheetProperty& attribute ) { return UIWidget::applyProperty( attribute ); } -std::string HTMLInput::getPropertyString( const PropertyDefinition* propertyDef, - const Uint32& propertyIndex ) const { +std::string UIHTMLInput::getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex ) const { if ( !propertyDef ) return ""; switch ( propertyDef->getPropertyId() ) { + case PropertyId::Value: + return mValue; case PropertyId::Type: return mInputType; default: @@ -73,36 +81,37 @@ std::string HTMLInput::getPropertyString( const PropertyDefinition* propertyDef, return UIWidget::getPropertyString( propertyDef, propertyIndex ); } -std::vector HTMLInput::getPropertiesImplemented() const { +std::vector UIHTMLInput::getPropertiesImplemented() const { auto props = UIWidget::getPropertiesImplemented(); props.push_back( PropertyId::Type ); + props.push_back( PropertyId::Value ); return props; } -Float HTMLInput::getMinIntrinsicWidth() const { +Float UIHTMLInput::getMinIntrinsicWidth() const { return mChildWidget ? mChildWidget->getMinIntrinsicWidth() : 0; } -Float HTMLInput::getMaxIntrinsicWidth() const { +Float UIHTMLInput::getMaxIntrinsicWidth() const { return mChildWidget ? mChildWidget->getMaxIntrinsicWidth() : 0; } -const std::string& HTMLInput::getInputType() const { +const std::string& UIHTMLInput::getInputType() const { return mInputType; } -void HTMLInput::setInputType( const std::string& type ) { +void UIHTMLInput::setInputType( const std::string& type ) { if ( mInputType != type ) { mInputType = type; createChildWidget(); } } -UIWidget* HTMLInput::getChildWidget() const { +UIWidget* UIHTMLInput::getChildWidget() const { return mChildWidget; } -void HTMLInput::createChildWidget() { +void UIHTMLInput::createChildWidget() { if ( mChildWidget ) { mChildWidget->close(); mChildWidget = nullptr; @@ -113,8 +122,7 @@ void HTMLInput::createChildWidget() { } else if ( mInputType == "checkbox" ) { mChildWidget = UICheckBox::New(); } else if ( mInputType == "hidden" ) { - mChildWidget = UIWidget::New(); - mChildWidget->setVisible( false ); + // We don't need it } else if ( mInputType == "number" ) { mChildWidget = UISpinBox::New(); } else if ( mInputType == "password" ) { @@ -125,21 +133,45 @@ void HTMLInput::createChildWidget() { mChildWidget = HTMLTextInput::New(); } + if ( mChildWidget == nullptr ) + return; + mChildWidget->setFlags( UI_HTML_ELEMENT ); if ( mChildWidget ) { mChildWidget->setParent( this ); mChildWidget->setLayoutWidthPolicy( SizePolicy::WrapContent ); mChildWidget->setLayoutHeightPolicy( SizePolicy::WrapContent ); - mChildWidget->on( Event::OnSizeChange, - [this]( auto ) { setPixelsSize( mChildWidget->getPixelsSize() ); } ); + mChildWidget->on( Event::OnSizeChange, [this]( auto ) { + if ( mChildWidget ) + setPixelsSize( mChildWidget->getPixelsSize() ); + } ); for ( const auto& propIt : mProperties ) { mChildWidget->applyProperty( propIt.second ); } } } -void HTMLInput::onSizeChange() { +String UIHTMLInput::getFormValue() const { + if ( !mChildWidget ) + return String(); + + if ( mInputType == "checkbox" ) + return static_cast( mChildWidget )->isChecked() ? "on" : ""; + if ( mInputType == "radio" ) + return static_cast( mChildWidget )->isActive() ? "on" : ""; + if ( mInputType == "number" ) + return static_cast( mChildWidget )->getTextInput()->getText(); + if ( mInputType == "button" || mInputType == "submit" ) + return static_cast( mChildWidget )->getText(); + + if ( mChildWidget->isType( UI_TYPE_TEXTINPUT ) ) + return static_cast( mChildWidget )->getText(); + + return mValue; +} + +void UIHTMLInput::onSizeChange() { UIWidget::onSizeChange(); } diff --git a/src/eepp/ui/htmltextarea.cpp b/src/eepp/ui/uihtmltextarea.cpp similarity index 69% rename from src/eepp/ui/htmltextarea.cpp rename to src/eepp/ui/uihtmltextarea.cpp index 29e4b9762..43a311015 100644 --- a/src/eepp/ui/htmltextarea.cpp +++ b/src/eepp/ui/uihtmltextarea.cpp @@ -1,31 +1,31 @@ #include #include -#include +#include #include #include namespace EE { namespace UI { -HTMLTextArea* HTMLTextArea::New() { - return eeNew( HTMLTextArea, () ); +UIHTMLTextArea* UIHTMLTextArea::New() { + return eeNew( UIHTMLTextArea, () ); } -HTMLTextArea::HTMLTextArea() : UITextEdit( "textarea" ) { +UIHTMLTextArea::UIHTMLTextArea() : UITextEdit( "textarea" ) { mFlags |= UI_HTML_ELEMENT; mWidthPolicy = SizePolicy::WrapContent; mHeightPolicy = SizePolicy::WrapContent; invalidateIntrinsicSize(); } -Uint32 HTMLTextArea::getType() const { +Uint32 UIHTMLTextArea::getType() const { return UI_TYPE_HTML_TEXTAREA; } -bool HTMLTextArea::isType( const Uint32& type ) const { - return HTMLTextArea::getType() == type || UITextEdit::isType( type ); +bool UIHTMLTextArea::isType( const Uint32& type ) const { + return UIHTMLTextArea::getType() == type || UITextEdit::isType( type ); } -bool HTMLTextArea::applyProperty( const StyleSheetProperty& attribute ) { +bool UIHTMLTextArea::applyProperty( const StyleSheetProperty& attribute ) { if ( !attribute.getPropertyDefinition() ) return false; @@ -43,8 +43,8 @@ bool HTMLTextArea::applyProperty( const StyleSheetProperty& attribute ) { return UITextEdit::applyProperty( attribute ); } -std::string HTMLTextArea::getPropertyString( const PropertyDefinition* propertyDef, - const Uint32& propertyIndex ) const { +std::string UIHTMLTextArea::getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex ) const { if ( !propertyDef ) return ""; @@ -60,14 +60,14 @@ std::string HTMLTextArea::getPropertyString( const PropertyDefinition* propertyD return UITextEdit::getPropertyString( propertyDef, propertyIndex ); } -std::vector HTMLTextArea::getPropertiesImplemented() const { +std::vector UIHTMLTextArea::getPropertiesImplemented() const { auto props = UITextEdit::getPropertiesImplemented(); props.push_back( PropertyId::Rows ); props.push_back( PropertyId::Cols ); return props; } -Float HTMLTextArea::getMinIntrinsicWidth() const { +Float UIHTMLTextArea::getMinIntrinsicWidth() const { if ( mCols > 0 && getFont() ) { Float advance = getFont()->getGlyph( 'M', getFontSize(), false, false ).advance; Float sbWidth = getVScrollBar() ? getVScrollBar()->getPixelsSize().getWidth() : 0; @@ -76,11 +76,11 @@ Float HTMLTextArea::getMinIntrinsicWidth() const { return UITextEdit::getMinIntrinsicWidth(); } -Float HTMLTextArea::getMaxIntrinsicWidth() const { +Float UIHTMLTextArea::getMaxIntrinsicWidth() const { return getMinIntrinsicWidth(); } -Float HTMLTextArea::getMinIntrinsicHeight() const { +Float UIHTMLTextArea::getMinIntrinsicHeight() const { if ( mRows > 0 && getFont() ) { return mRows * getFont()->getFontHeight( getFontSize() ) + mPaddingPx.Top + mPaddingPx.Bottom; @@ -88,11 +88,11 @@ Float HTMLTextArea::getMinIntrinsicHeight() const { return 0; } -Float HTMLTextArea::getMaxIntrinsicHeight() const { +Float UIHTMLTextArea::getMaxIntrinsicHeight() const { return getMinIntrinsicHeight(); } -void HTMLTextArea::onAutoSize() { +void UIHTMLTextArea::onAutoSize() { if ( mPacking ) return; mPacking = true; @@ -113,11 +113,11 @@ void HTMLTextArea::onAutoSize() { mPacking = false; } -Uint32 HTMLTextArea::getRows() const { +Uint32 UIHTMLTextArea::getRows() const { return mRows; } -void HTMLTextArea::setRows( Uint32 rows ) { +void UIHTMLTextArea::setRows( Uint32 rows ) { if ( mRows != rows ) { mRows = rows; invalidateIntrinsicSize(); @@ -125,11 +125,11 @@ void HTMLTextArea::setRows( Uint32 rows ) { } } -Uint32 HTMLTextArea::getCols() const { +Uint32 UIHTMLTextArea::getCols() const { return mCols; } -void HTMLTextArea::setCols( Uint32 cols ) { +void UIHTMLTextArea::setCols( Uint32 cols ) { if ( mCols != cols ) { mCols = cols; invalidateIntrinsicSize(); diff --git a/src/eepp/ui/uinode.cpp b/src/eepp/ui/uinode.cpp index c2e2ad0ce..d4f5a61ec 100644 --- a/src/eepp/ui/uinode.cpp +++ b/src/eepp/ui/uinode.cpp @@ -168,12 +168,6 @@ void UINode::setInternalPixelsSize( const Sizef& size ) { Node* UINode::setSize( const Sizef& size ) { Sizef s( fitMinMaxSizeDp( size ) ); - if ( s.x < mMinSize.x ) - s.x = mMinSize.x; - - if ( s.y < mMinSize.y ) - s.y = mMinSize.y; - if ( s != mDpSize ) { Vector2f sizeChange( s.x - mDpSize.x, s.y - mDpSize.y ); @@ -415,11 +409,11 @@ Sizef UINode::getMinSizePx() const { Sizef UINode::fitMinMaxSizePx( const Sizef& size ) const { Sizef s( size ); - if ( mMinSize.x != 0.f && s.x < PixelDensity::pxToDp( mMinSize.x ) ) - s.x = PixelDensity::pxToDp( mMinSize.x ); + if ( mMinSize.x != 0.f ) + s.x = std::max( s.x, PixelDensity::dpToPx( mMinSize.x ) ); - if ( mMinSize.y != 0.f && s.y < PixelDensity::pxToDp( mMinSize.y ) ) - s.y = PixelDensity::pxToDp( mMinSize.y ); + if ( mMinSize.y != 0.f ) + s.y = std::max( s.y, PixelDensity::dpToPx( mMinSize.y ) ); if ( !mMinWidthEq.empty() ) { Float length = lengthFromValue( mMinWidthEq, PropertyRelativeTarget::ContainingBlockWidth ); @@ -453,11 +447,8 @@ bool UINode::isScrollable() const { Sizef UINode::fitMinMaxSizeDp( const Sizef& size ) const { Sizef s( size ); - if ( s.x < mMinSize.x ) - s.x = mMinSize.x; - - if ( s.y < mMinSize.y ) - s.y = mMinSize.y; + s.x = std::max( s.x, mMinSize.x ); + s.y = std::max( s.y, mMinSize.y ); if ( !mMinWidthEq.empty() ) { Float length = diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 83fe6a026..0145460f3 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -652,7 +652,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri return; } - if ( !node->isWidget() ) + if ( !node->isWidget() || !node->isVisible() ) return; UIWidget* widget = node->asType(); diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index 9c3e6c20b..18acaf166 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -1366,9 +1366,13 @@ void UISceneNode::setURIFromURL( const URI& url ) { } void UISceneNode::openURL( URI uri ) { - if ( mURLInterceptorCb && mURLInterceptorCb( uri ) ) + navigate( NavigationRequest{ std::move( uri ) } ); +} + +void UISceneNode::navigate( const NavigationRequest& request ) { + if ( mNavigationInterceptorCb && mNavigationInterceptorCb( request ) ) return; - Engine::instance()->openURI( uri.toString() ); + Engine::instance()->openURI( request.uri.toString() ); } }} // namespace EE::UI diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp index f9597abfb..2fdfbfc3e 100644 --- a/src/eepp/ui/uiwidget.cpp +++ b/src/eepp/ui/uiwidget.cpp @@ -1434,7 +1434,7 @@ const Uint32& UIWidget::getStylePreviousState() const { std::vector UIWidget::findAllByClass( const std::string& className ) { std::vector widgets; - if ( !isClosing() && hasClass( className ) ) { + if ( !isClosing() && hasClass( className ) && !inClosingTree() ) { widgets.push_back( this ); } @@ -1458,7 +1458,7 @@ std::vector UIWidget::findAllByClass( const std::string& className ) std::vector UIWidget::findAllByTag( const std::string& tag ) { std::vector widgets; - if ( !isClosing() && getElementTag() == tag ) { + if ( !isClosing() && getElementTag() == tag && !inClosingTree() ) { widgets.push_back( this ); } @@ -1479,7 +1479,7 @@ std::vector UIWidget::findAllByTag( const std::string& tag ) { } UIWidget* UIWidget::findByClass( const std::string& className ) { - if ( !isClosing() && hasClass( className ) ) { + if ( !isClosing() && hasClass( className ) && !inClosingTree() ) { return this; } else { Node* child = mChild; @@ -1500,7 +1500,7 @@ UIWidget* UIWidget::findByClass( const std::string& className ) { } UIWidget* UIWidget::findByTag( const std::string& tag ) { - if ( !isClosing() && getElementTag() == tag ) { + if ( !isClosing() && getElementTag() == tag && !inClosingTree() ) { return this; } else { Node* child = mChild; @@ -1521,7 +1521,7 @@ UIWidget* UIWidget::findByTag( const std::string& tag ) { } UIWidget* UIWidget::querySelector( const CSS::StyleSheetSelector& selector ) { - if ( !isClosing() && selector.select( this ) ) { + if ( !isClosing() && !inClosingTree() && selector.select( this ) ) { return this; } else { Node* child = mChild; @@ -1544,7 +1544,7 @@ UIWidget* UIWidget::querySelector( const CSS::StyleSheetSelector& selector ) { std::vector UIWidget::querySelectorAll( const CSS::StyleSheetSelector& selector ) { std::vector widgets; - if ( !isClosing() && selector.select( this ) ) { + if ( !isClosing() && !inClosingTree() && selector.select( this ) ) { widgets.push_back( this ); } @@ -1758,11 +1758,13 @@ std::string UIWidget::getPropertyString( const PropertyDefinition* propertyDef, case PropertyId::BlendMode: return ""; case PropertyId::MinWidth: - return mMinWidthEq; + return !mMinWidthEq.empty() ? mMinWidthEq + : String::fromFloat( mMinSize.getWidth(), "px" ); case PropertyId::MaxWidth: return mMaxWidthEq; case PropertyId::MinHeight: - return mMinHeightEq; + return !mMinHeightEq.empty() ? mMinHeightEq + : String::fromFloat( mMinSize.getHeight(), "px" ); case PropertyId::MaxHeight: return mMaxHeightEq; case PropertyId::BorderLeftColor: diff --git a/src/eepp/ui/uiwidgetcreator.cpp b/src/eepp/ui/uiwidgetcreator.cpp index ff424f181..2d9d4edfb 100644 --- a/src/eepp/ui/uiwidgetcreator.cpp +++ b/src/eepp/ui/uiwidgetcreator.cpp @@ -1,5 +1,3 @@ -#include -#include #include #include #include @@ -10,9 +8,12 @@ #include #include #include +#include #include +#include #include #include +#include #include #include #include @@ -189,7 +190,7 @@ void UIWidgetCreator::createBaseWidgetList() { svg->setFlags( UI_HTML_ELEMENT ); return svg; }; - registeredWidget["input"] = [] { return HTMLInput::New(); }; + registeredWidget["input"] = [] { return UIHTMLInput::New(); }; registeredWidget["header"] = [] { return UIRichText::NewWithTag( "header" ); }; registeredWidget["article"] = [] { return UIRichText::NewWithTag( "article" ); }; registeredWidget["footer"] = [] { return UIRichText::NewWithTag( "footer" ); }; @@ -200,7 +201,7 @@ void UIWidgetCreator::createBaseWidgetList() { registeredWidget["html"] = UIRichText::NewHtml; registeredWidget["head"] = [] { return UIWidget::NewWithTag( "head" ); }; registeredWidget["body"] = UIRichText::NewBody; - registeredWidget["form"] = [] { return UIRichText::NewWithTag( "form" ); }; + registeredWidget["form"] = [] { return UIHTMLForm::New(); }; registeredWidget["table"] = UIHTMLTable::New; registeredWidget["tr"] = UIHTMLTableRow::New; registeredWidget["thead"] = UIHTMLTableHead::New; @@ -208,8 +209,8 @@ void UIWidgetCreator::createBaseWidgetList() { registeredWidget["tfoot"] = UIHTMLTableFooter::New; registeredWidget["th"] = [] { return UIHTMLTableCell::New( "th" ); }; registeredWidget["td"] = [] { return UIHTMLTableCell::New( "td" ); }; - registeredWidget["input"] = HTMLInput::New; - registeredWidget["textarea"] = HTMLTextArea::New; + registeredWidget["input"] = UIHTMLInput::New; + registeredWidget["textarea"] = UIHTMLTextArea::New; registeredWidget["button"] = [] { auto but = UIPushButton::NewWithTag( "button" ); but->setFlags( UI_HTML_ELEMENT ); diff --git a/src/examples/ui_html/ui_html.cpp b/src/examples/ui_html/ui_html.cpp index 4d1046afd..ef36780de 100644 --- a/src/examples/ui_html/ui_html.cpp +++ b/src/examples/ui_html/ui_html.cpp @@ -112,16 +112,21 @@ EE_MAIN_FUNC int main( int argc, char** argv ) { if ( htmlNode && bodyNode ) { auto html = htmlNode->asType(); auto body = bodyNode->asType(); - html->setMinHeight( scrollView->getPixelsSize().getHeight() ); - body->setMinHeight( scrollView->getPixelsSize().getHeight() ); - scrollView->on( Event::OnSizeChange, [scrollView, html, body]( auto ) { - body->setMinHeight( scrollView->getSize().getHeight() ); + const auto updateMinHeight = []( auto scrollView, auto html, auto body ) { + html->setMinHeight( + PixelDensity::pxToDp( scrollView->getPixelsSize().getHeight() ) ); + body->setMinHeight( + PixelDensity::pxToDp( scrollView->getPixelsSize().getHeight() ) ); body->setPixelsSize( { html->getPixelsSize().getWidth(), 0 } ); - html->setMinHeight( scrollView->getSize().getHeight() ); html->setPixelsSize( { html->getPixelsSize().getWidth(), 0 } ); - } ); - body->on( Event::OnClose, [scrollView]( auto ) { - scrollView->removeEventsOfType( Event::OnSizeChange ); + }; + updateMinHeight( scrollView, html, body ); + auto eventId = scrollView->on( Event::OnSizeChange, + [scrollView, html, body, updateMinHeight]( auto ) { + updateMinHeight( scrollView, html, body ); + } ); + body->on( Event::OnClose, [scrollView, eventId]( auto ) { + scrollView->removeEventListener( eventId ); } ); } urlBar->setText( urlStr ); @@ -168,15 +173,15 @@ EE_MAIN_FUNC int main( int argc, char** argv ) { // We add a default `isHistoryNav` parameter to determine if we are pushing to history or just // navigating back/forth - const auto loadDocument = [&]( URI url, bool isHistoryNav = false ) { + const auto loadDocument = [&]( URI url, bool isHistoryNav = false, + const std::string& method = "GET", const std::string& body = "", + const Http::Request::FieldTable& headers = + Http::Request::FieldTable() ) { if ( !isHistoryNav ) { - // If we navigate to a new URL while in the middle of history, clear out the "forward" - // history if ( historyIndex >= 0 && historyIndex < static_cast( history.size() ) - 1 ) { history.resize( historyIndex + 1 ); } - // Don't add to history if we are just reloading the exact same current page manually if ( history.empty() || history.back().toString() != url.toString() ) { history.push_back( url ); historyIndex = static_cast( history.size() ) - 1; @@ -186,12 +191,37 @@ EE_MAIN_FUNC int main( int argc, char** argv ) { if ( !url.getScheme().empty() ) { if ( url.getScheme() == "https" || url.getScheme() == "http" ) { - Http::getAsync( + auto reqHeaders = headers; + if ( ui->getCookieManager().hasCookie( url.getAuthority() ) ) { + std::string cookieHeader = + ui->getCookieManager().getCookieHeader( url.getAuthority() ); + if ( !cookieHeader.empty() ) + reqHeaders["Cookie"] = cookieHeader; + } + Http::requestAsync( [=]( const Http&, Http::Request&, Http::Response& response ) { - std::string data = response.getBody(); - loadDocumentData( url, data ); + if ( response.isOK() ) { + std::string data = response.getBody(); + if ( response.hasField( "set-cookie" ) ) { + ui->getCookieManager().storeCookiesFromHeader( + url.getAuthority(), response.getField( "set-cookie" ) ); + } + loadDocumentData( url, data ); + } }, - url, Seconds( 5 ) ); + url, Seconds( 5 ), + method == "POST" ? Http::Request::Method::Post : Http::Request::Method::Get, + [=]( const Http& http, const Http::Request& request, + const Http::Response& response, const Http::Request::Status& status, + std::size_t totalBytes, std::size_t currentBytes ) { + if ( status == Http::Request::Status::Redirect && + response.hasField( "set-cookie" ) ) { + ui->getCookieManager().storeCookiesFromHeader( + url.getAuthority(), response.getField( "set-cookie" ) ); + } + return true; + }, + reqHeaders, body, true, {} ); } else if ( url.getScheme() == "file" ) { std::string data; FileSystem::fileGet( url.getPath(), data ); @@ -226,8 +256,9 @@ EE_MAIN_FUNC int main( int argc, char** argv ) { urlBar->on( Event::OnPressEnter, [&]( auto event ) { loadDocument( urlBar->getText().toUtf8() ); } ); - ui->setURLInterceptorCb( [&]( URI uri ) { - loadDocument( ui->solveRelativePath( uri ) ); + ui->setNavigationInterceptorCb( [&]( const NavigationRequest& request ) { + URI uri = ui->solveRelativePath( request.uri ); + loadDocument( uri, false, request.method, request.body, request.extraHeaders ); return true; } ); diff --git a/src/tests/unit_tests/cookiemanager_tests.cpp b/src/tests/unit_tests/cookiemanager_tests.cpp new file mode 100644 index 000000000..938f9486f --- /dev/null +++ b/src/tests/unit_tests/cookiemanager_tests.cpp @@ -0,0 +1,77 @@ +#include "utest.h" + +#include +#include + +using namespace EE; +using namespace EE::Network; + +static Network::Http::Response fakeResponse( const std::string& setCookieValue ) { + Network::Http::Request::FieldTable fields; + fields["set-cookie"] = setCookieValue; + Network::Http::Response::Status status{}; + return Network::Http::Response::createFakeResponse( fields, status, "" ); +} + +UTEST( CookieManager, storeAndRetrieve ) { + CookieManager cm; + auto response = fakeResponse( "session=abc123; Path=/; HttpOnly" ); + cm.storeCookies( "example.com", response ); + + EXPECT_EQ( cm.size(), 1u ); + EXPECT_TRUE( !cm.empty() ); + EXPECT_TRUE( cm.getCookieHeader( "example.com" ) == "session=abc123" ); +} + +UTEST( CookieManager, domainIsolation ) { + CookieManager cm; + cm.storeCookies( "site-a.com", fakeResponse( "a=1" ) ); + cm.storeCookies( "site-b.com", fakeResponse( "b=2" ) ); + + EXPECT_EQ( cm.size(), 2u ); + EXPECT_TRUE( cm.getCookieHeader( "site-a.com" ) == "a=1" ); + EXPECT_TRUE( cm.getCookieHeader( "site-b.com" ) == "b=2" ); + EXPECT_TRUE( cm.getCookieHeader( "other.com" ).empty() ); +} + +UTEST( CookieManager, multipleCookiesPerDomain ) { + CookieManager cm; + cm.storeCookies( "example.com", fakeResponse( "a=1; Path=/" ) ); + cm.storeCookies( "example.com", fakeResponse( "b=2" ) ); + + EXPECT_EQ( cm.size(), 2u ); + std::string header = cm.getCookieHeader( "example.com" ); + EXPECT_TRUE( header.find( "a=1" ) != std::string::npos ); + EXPECT_TRUE( header.find( "b=2" ) != std::string::npos ); +} + +UTEST( CookieManager, ignoresAttributes ) { + CookieManager cm; + cm.storeCookies( "example.com", + fakeResponse( "token=xyz; Path=/app; HttpOnly; Secure; SameSite=Lax" ) ); + + EXPECT_TRUE( cm.getCookieHeader( "example.com" ) == "token=xyz" ); +} + +UTEST( CookieManager, clearsAll ) { + CookieManager cm; + cm.storeCookies( "a.com", fakeResponse( "a=1" ) ); + cm.storeCookies( "b.com", fakeResponse( "b=2" ) ); + + EXPECT_EQ( cm.size(), 2u ); + cm.clear(); + EXPECT_TRUE( cm.empty() ); + EXPECT_EQ( cm.size(), 0u ); +} + +UTEST( CookieManager, emptyCookieRejected ) { + CookieManager cm; + cm.storeCookies( "example.com", fakeResponse( "=empty" ) ); + EXPECT_TRUE( cm.empty() ); +} + +UTEST( CookieManager, storeFromRawHeader ) { + CookieManager cm; + cm.storeCookiesFromHeader( "example.com", "key=value; Path=/" ); + EXPECT_TRUE( cm.getCookieHeader( "example.com" ) == "key=value" ); +} diff --git a/src/tests/unit_tests/uidiffview_test.cpp b/src/tests/unit_tests/uidiffview_test.cpp index 0f48c2af1..5c365ad9d 100644 --- a/src/tests/unit_tests/uidiffview_test.cpp +++ b/src/tests/unit_tests/uidiffview_test.cpp @@ -22,26 +22,26 @@ UTEST( UIDiffView, LoadFromStringsAndVerifyDiffLines ) { ASSERT_EQ( (size_t)7, lines.size() ); - ASSERT_EQ( UIDiffView::DiffLineType::Common, lines[0].type ); - ASSERT_TRUE( lines[0].text.toUtf8() == "line 1" ); + EXPECT_EQ( UIDiffView::DiffLineType::Common, lines[0].type ); + EXPECT_TRUE( lines[0].text.toUtf8() == "line 1" ); - ASSERT_EQ( UIDiffView::DiffLineType::Added, lines[1].type ); - ASSERT_TRUE( lines[1].text.toUtf8() == "line 2 changed" ); + EXPECT_EQ( UIDiffView::DiffLineType::Added, lines[1].type ); + EXPECT_TRUE( lines[1].text.toUtf8() == "line 2 changed" ); - ASSERT_EQ( UIDiffView::DiffLineType::Removed, lines[2].type ); - ASSERT_TRUE( lines[2].text.toUtf8() == "line 2" ); + EXPECT_EQ( UIDiffView::DiffLineType::Removed, lines[2].type ); + EXPECT_TRUE( lines[2].text.toUtf8() == "line 2" ); - ASSERT_EQ( UIDiffView::DiffLineType::Common, lines[3].type ); - ASSERT_TRUE( lines[3].text.toUtf8() == "line 3" ); + EXPECT_EQ( UIDiffView::DiffLineType::Common, lines[3].type ); + EXPECT_TRUE( lines[3].text.toUtf8() == "line 3" ); - ASSERT_EQ( UIDiffView::DiffLineType::Added, lines[4].type ); - ASSERT_TRUE( lines[4].text.toUtf8() == "line 4 added" ); + EXPECT_EQ( UIDiffView::DiffLineType::Added, lines[4].type ); + EXPECT_TRUE( lines[4].text.toUtf8() == "line 4 added" ); - ASSERT_EQ( UIDiffView::DiffLineType::Added, lines[5].type ); - ASSERT_TRUE( lines[5].text.toUtf8() == "line 5" ); + EXPECT_EQ( UIDiffView::DiffLineType::Added, lines[5].type ); + EXPECT_TRUE( lines[5].text.toUtf8() == "line 5" ); - ASSERT_EQ( UIDiffView::DiffLineType::Removed, lines[6].type ); - ASSERT_TRUE( lines[6].text.toUtf8() == "line 4" ); + EXPECT_EQ( UIDiffView::DiffLineType::Removed, lines[6].type ); + EXPECT_TRUE( lines[6].text.toUtf8() == "line 4" ); const auto& text = diffView->getEditor()->getDocument().getText(); @@ -49,7 +49,7 @@ UTEST( UIDiffView, LoadFromStringsAndVerifyDiffLines ) { "line 1\nline 2 changed\nline 2\nline 3\nline 4 added\nline 5\nline 4\n"; std::string textUtf8 = text.toUtf8(); - ASSERT_TRUE( expectedCleanText == textUtf8 ); + EXPECT_TRUE( expectedCleanText == textUtf8 ); eeDelete( diffView ); } @@ -73,30 +73,30 @@ UTEST( UIDiffView, LoadFromPatchAndVerifyCleanText ) { ASSERT_EQ( (size_t)5, lines.size() ); - ASSERT_EQ( UIDiffView::DiffLineType::Common, lines[0].type ); - ASSERT_TRUE( lines[0].text.toUtf8() == "int main() {" ); - ASSERT_EQ( 1, lines[0].oldLineNum ); - ASSERT_EQ( 1, lines[0].newLineNum ); + EXPECT_EQ( UIDiffView::DiffLineType::Common, lines[0].type ); + EXPECT_TRUE( lines[0].text.toUtf8() == "int main() {" ); + EXPECT_EQ( 1, lines[0].oldLineNum ); + EXPECT_EQ( 1, lines[0].newLineNum ); - ASSERT_EQ( UIDiffView::DiffLineType::Removed, lines[1].type ); - ASSERT_TRUE( lines[1].text.toUtf8() == " return 0;" ); - ASSERT_EQ( 2, lines[1].oldLineNum ); + EXPECT_EQ( UIDiffView::DiffLineType::Removed, lines[1].type ); + EXPECT_TRUE( lines[1].text.toUtf8() == " return 0;" ); + EXPECT_EQ( 2, lines[1].oldLineNum ); - ASSERT_EQ( UIDiffView::DiffLineType::Added, lines[2].type ); - ASSERT_TRUE( lines[2].text.toUtf8() == " return 1;" ); - ASSERT_EQ( 2, lines[2].newLineNum ); + EXPECT_EQ( UIDiffView::DiffLineType::Added, lines[2].type ); + EXPECT_TRUE( lines[2].text.toUtf8() == " return 1;" ); + EXPECT_EQ( 2, lines[2].newLineNum ); - ASSERT_EQ( UIDiffView::DiffLineType::Common, lines[3].type ); - ASSERT_TRUE( lines[3].text.toUtf8() == "}" ); - ASSERT_EQ( 3, lines[3].oldLineNum ); - ASSERT_EQ( 3, lines[3].newLineNum ); + EXPECT_EQ( UIDiffView::DiffLineType::Common, lines[3].type ); + EXPECT_TRUE( lines[3].text.toUtf8() == "}" ); + EXPECT_EQ( 3, lines[3].oldLineNum ); + EXPECT_EQ( 3, lines[3].newLineNum ); const auto& text = diffView->getEditor()->getDocument().getText(); std::string expectedCleanText = "int main() {\n return 0;\n return 1;\n}\n\n"; std::string textUtf8 = text.toUtf8(); - ASSERT_TRUE( expectedCleanText == textUtf8 ); + EXPECT_TRUE( expectedCleanText == textUtf8 ); eeDelete( diffView ); } diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 53b3752e6..dd191c08d 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -9,13 +9,13 @@ #include #include #include -#include -#include #include #include #include #include +#include #include +#include #include #include #include @@ -373,7 +373,7 @@ UTEST( UIHTMLTable, nestedSpecifiedWidth ) { Engine::destroySingleton(); } -UTEST( HTMLInput, sizeAttribute ) { +UTEST( UIHTMLInput, sizeAttribute ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( R"html( @@ -387,12 +387,12 @@ UTEST( HTMLInput, sizeAttribute ) { )html" ); - auto c1 = sceneNode->getRoot()->find( "i1" )->asType(); - auto c2 = sceneNode->getRoot()->find( "i2" )->asType(); - auto c3 = sceneNode->getRoot()->find( "i3" )->asType(); - auto cp = sceneNode->getRoot()->find( "i_pwd" )->asType(); - auto cm = sceneNode->getRoot()->find( "i_mode_pwd" )->asType(); - auto cc = sceneNode->getRoot()->find( "i_chk" )->asType(); + auto c1 = sceneNode->getRoot()->find( "i1" )->asType(); + auto c2 = sceneNode->getRoot()->find( "i2" )->asType(); + auto c3 = sceneNode->getRoot()->find( "i3" )->asType(); + auto cp = sceneNode->getRoot()->find( "i_pwd" )->asType(); + auto cm = sceneNode->getRoot()->find( "i_mode_pwd" )->asType(); + auto cc = sceneNode->getRoot()->find( "i_chk" )->asType(); ASSERT_TRUE( c1 != nullptr ); ASSERT_TRUE( c2 != nullptr ); @@ -428,7 +428,7 @@ UTEST( HTMLInput, sizeAttribute ) { Engine::destroySingleton(); } -UTEST( HTMLTextArea, rowsColsAttribute ) { +UTEST( UIHTMLTextArea, rowsColsAttribute ) { init_ui_test(); auto* scene = SceneManager::instance()->getUISceneNode(); auto* c1_raw = scene->loadLayoutFromString( R"html( @@ -438,8 +438,8 @@ UTEST( HTMLTextArea, rowsColsAttribute ) { )html" ); ASSERT_TRUE( c1_raw != nullptr ); - auto* t1 = c1_raw->find( "t1" )->asType(); - auto* t2 = c1_raw->find( "t2" )->asType(); + auto* t1 = c1_raw->find( "t1" )->asType(); + auto* t2 = c1_raw->find( "t2" )->asType(); ASSERT_TRUE( t1 != nullptr ); ASSERT_TRUE( t2 != nullptr ); EXPECT_EQ( t1->getRows(), 2u ); diff --git a/src/tests/unit_tests/uihtmlform_tests.cpp b/src/tests/unit_tests/uihtmlform_tests.cpp new file mode 100644 index 000000000..1701c4788 --- /dev/null +++ b/src/tests/unit_tests/uihtmlform_tests.cpp @@ -0,0 +1,259 @@ +#include "utest.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::Graphics; +using namespace EE::Window; +using namespace EE::Scene; +using namespace EE::UI; + +static UISceneNode* initFormTest( const std::string& title ) { + Engine::instance()->createWindow( WindowSettings( 800, 600, title, WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, + true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); + font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + FontFamily::loadFromRegular( font ); + + UI::UISceneNode* sceneNode = UI::UISceneNode::New(); + SceneManager::instance()->add( sceneNode ); + sceneNode->getUIThemeManager()->setDefaultFont( font ); + return sceneNode; +} + +UTEST( UIHTMLForm, submitGET ) { + auto* sceneNode = initFormTest( "Form Submit GET Test" ); + + std::string interceptedUri; + std::string interceptedBody; + std::string interceptedMethod; + sceneNode->setNavigationInterceptorCb( [&]( const NavigationRequest& req ) -> bool { + interceptedUri = req.uri.toString(); + interceptedBody = req.body; + interceptedMethod = req.method; + return true; + } ); + + auto* form = UIHTMLForm::New(); + form->setParent( sceneNode->getRoot() ); + form->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); + form->setAction( "https://example.com/search" ); + form->setMethod( "GET" ); + + auto* input = UIHTMLInput::New(); + input->setParent( form ); + input->setInputType( "text" ); + input->setStyleSheetInlineProperty( "name", "q" ); + static_cast( input->getChildWidget() )->setText( "hello world" ); + + auto* submit = UIHTMLInput::New(); + submit->setParent( form ); + submit->setInputType( "submit" ); + + SceneManager::instance()->update(); + form->submit(); + + EXPECT_TRUE( interceptedUri.find( "example.com/search" ) != std::string::npos ); + EXPECT_TRUE( interceptedUri.find( "q=" ) != std::string::npos ); + EXPECT_TRUE( interceptedUri.find( URI::encode( "hello world" ) ) != std::string::npos ); + EXPECT_TRUE( interceptedMethod == "GET" ); + EXPECT_TRUE( interceptedBody.empty() ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLForm, submitPOST ) { + auto* sceneNode = initFormTest( "Form Submit POST Test" ); + + std::string interceptedUri; + std::string interceptedBody; + std::string interceptedMethod; + sceneNode->setNavigationInterceptorCb( [&]( const NavigationRequest& req ) -> bool { + interceptedUri = req.uri.toString(); + interceptedBody = req.body; + interceptedMethod = req.method; + return true; + } ); + + auto* form = UIHTMLForm::New(); + form->setParent( sceneNode->getRoot() ); + form->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); + form->setAction( "https://example.com/login" ); + form->setMethod( "POST" ); + + auto* userInput = UIHTMLInput::New(); + userInput->setParent( form ); + userInput->setInputType( "text" ); + userInput->setStyleSheetInlineProperty( "name", "username" ); + static_cast( userInput->getChildWidget() )->setText( "admin" ); + + auto* passInput = UIHTMLInput::New(); + passInput->setParent( form ); + passInput->setInputType( "password" ); + passInput->setStyleSheetInlineProperty( "name", "password" ); + static_cast( passInput->getChildWidget() )->setText( "secret" ); + + auto* submit = UIHTMLInput::New(); + submit->setParent( form ); + submit->setInputType( "submit" ); + + SceneManager::instance()->update(); + form->submit(); + + EXPECT_TRUE( interceptedUri.find( "example.com/login" ) != std::string::npos ); + EXPECT_TRUE( interceptedMethod == "POST" ); + EXPECT_TRUE( interceptedBody.find( "username=" ) != std::string::npos ); + EXPECT_TRUE( interceptedBody.find( URI::encode( "admin" ) ) != std::string::npos ); + EXPECT_TRUE( interceptedBody.find( "password=" ) != std::string::npos ); + EXPECT_TRUE( interceptedBody.find( URI::encode( "secret" ) ) != std::string::npos ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLForm, getValueCheckbox ) { + auto* sceneNode = initFormTest( "Form Checkbox Value" ); + + auto* checkbox = UIHTMLInput::New(); + checkbox->setParent( sceneNode->getRoot() ); + checkbox->setInputType( "checkbox" ); + checkbox->setStyleSheetInlineProperty( "name", "agree" ); + + EXPECT_TRUE( checkbox->getFormValue().empty() ); + static_cast( checkbox->getChildWidget() )->setChecked( true ); + EXPECT_TRUE( checkbox->getFormValue() == "on" ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLForm, getValueTextArea ) { + auto* sceneNode = initFormTest( "Form TextArea Value" ); + + auto* textarea = UIHTMLTextArea::New(); + textarea->setParent( sceneNode->getRoot() ); + textarea->setStyleSheetInlineProperty( "name", "bio" ); + textarea->setText( "Hello World" ); + + EXPECT_TRUE( textarea->getText() == "Hello World" ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLForm, submitOnButtonMessage ) { + auto* sceneNode = initFormTest( "Form Button Message" ); + + bool intercepted = false; + sceneNode->setNavigationInterceptorCb( [&]( const NavigationRequest& ) -> bool { + intercepted = true; + return true; + } ); + + auto* form = UIHTMLForm::New(); + form->setParent( sceneNode->getRoot() ); + form->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); + form->setAction( "https://example.com/submit" ); + + auto* submit = UIHTMLInput::New(); + submit->setParent( form ); + submit->setInputType( "submit" ); + + SceneManager::instance()->update(); + + NodeMessage msg( submit, NodeMessage::MouseClick, 0 ); + msg.getSender()->messagePost( &msg ); + + EXPECT_TRUE( intercepted ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLForm, submitMultipartEnctype ) { + auto* sceneNode = initFormTest( "Form Multipart Enctype" ); + + std::string interceptedBody; + std::string interceptedContentType; + sceneNode->setNavigationInterceptorCb( [&]( const NavigationRequest& req ) -> bool { + interceptedBody = req.body; + auto it = req.extraHeaders.find( "Content-Type" ); + if ( it != req.extraHeaders.end() ) + interceptedContentType = it->second; + return true; + } ); + + auto* form = UIHTMLForm::New(); + form->setParent( sceneNode->getRoot() ); + form->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); + form->setAction( "https://example.com/upload" ); + form->setMethod( "POST" ); + form->setEnctype( "multipart/form-data" ); + + auto* nameInput = UIHTMLInput::New(); + nameInput->setParent( form ); + nameInput->setInputType( "text" ); + nameInput->setStyleSheetInlineProperty( "name", "username" ); + static_cast( nameInput->getChildWidget() )->setText( "admin" ); + + SceneManager::instance()->update(); + form->submit(); + + EXPECT_TRUE( interceptedBody.find( "name=\"username\"" ) != std::string::npos ); + EXPECT_TRUE( interceptedBody.find( "admin" ) != std::string::npos ); + EXPECT_TRUE( interceptedContentType.find( "multipart/form-data" ) != std::string::npos ); + EXPECT_TRUE( interceptedContentType.find( "boundary=" ) != std::string::npos ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLForm, submitTextPlainEnctype ) { + auto* sceneNode = initFormTest( "Form Text Plain Enctype" ); + + std::string interceptedBody; + std::string interceptedContentType; + sceneNode->setNavigationInterceptorCb( [&]( const NavigationRequest& req ) -> bool { + interceptedBody = req.body; + auto it = req.extraHeaders.find( "Content-Type" ); + if ( it != req.extraHeaders.end() ) + interceptedContentType = it->second; + return true; + } ); + + auto* form = UIHTMLForm::New(); + form->setParent( sceneNode->getRoot() ); + form->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); + form->setAction( "https://example.com/data" ); + form->setMethod( "POST" ); + form->setEnctype( "text/plain" ); + + auto* nameInput = UIHTMLInput::New(); + nameInput->setParent( form ); + nameInput->setInputType( "text" ); + nameInput->setStyleSheetInlineProperty( "name", "msg" ); + static_cast( nameInput->getChildWidget() )->setText( "hello" ); + + SceneManager::instance()->update(); + form->submit(); + + EXPECT_TRUE( interceptedBody.find( "msg=hello" ) != std::string::npos ); + EXPECT_TRUE( interceptedContentType == "text/plain" ); + + Engine::destroySingleton(); +}