From 0179c17cf9f1e8fdc47147e73410c5588bfa8b38 Mon Sep 17 00:00:00 2001 From: Alexandr Zarubkin Date: Fri, 18 Aug 2017 17:59:06 +0300 Subject: [PATCH] Added simple template processor to AsyncFileResponse. (#189) Unzipped files in SPIFFS, Streams, PROGMEM strings, callback/chunked responses may have template placeholders like %TEMPLATE_VAR% inside. If callback is specified in Async...Response constructor call, it will be used to replace these with actual strings. The prototype of callback is String(const String&), i.e. it gets variable name and returns its value. Template variables' delimiter is currently percent sign ('%'). Maximal placeholder length is 32 chars (chosen somewhat arbitrarily, it may be stored on stack during processing). It is not guaranteed that placeholders longer than that will be processed. Signed-off-by: Alexandr Zarubkin # Conflicts: # src/WebResponses.cpp --- README.md | 42 +++++++++ src/ESPAsyncWebServer.h | 29 +++--- src/WebHandlerImpl.h | 2 + src/WebHandlers.cpp | 4 +- src/WebRequest.cpp | 58 ++++++------ src/WebResponseImpl.h | 31 +++++-- src/WebResponses.cpp | 191 +++++++++++++++++++++++++++++++++++----- 7 files changed, 285 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 6f081b4..62c4a2f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ To use this library you might need to have the latest git versions of [ESP8266]( - [Respond with content coming from a Stream and extra headers](#respond-with-content-coming-from-a-stream-and-extra-headers) - [Respond with content coming from a File](#respond-with-content-coming-from-a-file) - [Respond with content coming from a File and extra headers](#respond-with-content-coming-from-a-file-and-extra-headers) + - [Respond with content coming from a File containing templates](#respond-with-content-coming-from-a-file-containing-templates) - [Respond with content using a callback](#respond-with-content-using-a-callback) - [Respond with content using a callback and extra headers](#respond-with-content-using-a-callback-and-extra-headers) - [Chunked Response](#chunked-response) @@ -48,6 +49,7 @@ To use this library you might need to have the latest git versions of [ESP8266]( - [Serving files in directory](#serving-files-in-directory) - [Specifying Cache-Control header](#specifying-cache-control-header) - [Specifying Date-Modified header](#specifying-date-modified-header) + - [Specifying Template Processor callback](#specifying-template-processor-callback) - [Using filters](#using-filters) - [Serve different site files in AP mode](#serve-different-site-files-in-ap-mode) - [Rewrite to different index on AP](#rewrite-to-different-index-on-ap) @@ -411,6 +413,29 @@ response->addHeader("Server","ESP Async Web Server"); request->send(response); ``` +### Respond with content coming from a File containing templates +Internally uses [Chunked Response](#chunked-response). + +Index.htm contents: +``` +%HELLO_FROM_TEMPLATE% +``` + +Somewhere in source files: +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +//Send index.htm with template processor function +request->send(SPIFFS, "/index.htm", String(), false, processor); +``` + ### Respond with content using a callback ```cpp //send 128 bytes as plain text @@ -610,6 +635,23 @@ saveDateModified(date_modified); // Save for next reset handler->setLastModified(date_modified); ``` +### Specifying Template Processor callback +It is possible to specify template processor for static files. For information on template processor see +[Respond with content coming from a File containing templates](#respond-with-content-coming-from-a-file-containing-templates). +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +server.serveStatic("/", SPIFFS, "/www/").setTemplateProcessor(processor); +``` + + ## Using filters Filters can be set to `Rewrite` or `Handler` in order to control when to apply the rewrite and consider the handler. A filter is a callback function that evaluates the request and return a boolean `true` to include the item diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index 82b1584..36441ad 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -116,6 +116,7 @@ class AsyncWebHeader { typedef enum { RCT_NOT_USED = -1, RCT_DEFAULT = 0, RCT_HTTP, RCT_WS, RCT_EVENT, RCT_MAX } RequestedConnectionType; typedef std::function AwsResponseFiller; +typedef std::function AwsTemplateProcessor; class AsyncWebServerRequest { using File = fs::File; @@ -216,23 +217,23 @@ class AsyncWebServerRequest { void send(AsyncWebServerResponse *response); void send(int code, const String& contentType=String(), const String& content=String()); - void send(FS &fs, const String& path, const String& contentType=String(), bool download=false); - void send(File content, const String& path, const String& contentType=String(), bool download=false); - void send(Stream &stream, const String& contentType, size_t len); - void send(const String& contentType, size_t len, AwsResponseFiller callback); - void sendChunked(const String& contentType, AwsResponseFiller callback); - void send_P(int code, const String& contentType, const uint8_t * content, size_t len); - void send_P(int code, const String& contentType, PGM_P content); + void send(FS &fs, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + void send(File content, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + void send(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback=nullptr); + void send(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + void sendChunked(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + void send_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback=nullptr); + void send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback=nullptr); AsyncWebServerResponse *beginResponse(int code, const String& contentType=String(), const String& content=String()); - AsyncWebServerResponse *beginResponse(FS &fs, const String& path, const String& contentType=String(), bool download=false); - AsyncWebServerResponse *beginResponse(File content, const String& path, const String& contentType=String(), bool download=false); - AsyncWebServerResponse *beginResponse(Stream &stream, const String& contentType, size_t len); - AsyncWebServerResponse *beginResponse(const String& contentType, size_t len, AwsResponseFiller callback); - AsyncWebServerResponse *beginChunkedResponse(const String& contentType, AwsResponseFiller callback); + AsyncWebServerResponse *beginResponse(FS &fs, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + AsyncWebServerResponse *beginResponse(File content, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + AsyncWebServerResponse *beginResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback=nullptr); + AsyncWebServerResponse *beginResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + AsyncWebServerResponse *beginChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); AsyncResponseStream *beginResponseStream(const String& contentType, size_t bufferSize=1460); - AsyncWebServerResponse *beginResponse_P(int code, const String& contentType, const uint8_t * content, size_t len); - AsyncWebServerResponse *beginResponse_P(int code, const String& contentType, PGM_P content); + AsyncWebServerResponse *beginResponse_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback=nullptr); + AsyncWebServerResponse *beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback=nullptr); size_t headers() const; // get header count bool hasHeader(const String& name) const; // check if header exists diff --git a/src/WebHandlerImpl.h b/src/WebHandlerImpl.h index 101f5bb..b3c84e3 100644 --- a/src/WebHandlerImpl.h +++ b/src/WebHandlerImpl.h @@ -39,6 +39,7 @@ class AsyncStaticWebHandler: public AsyncWebHandler { String _default_file; String _cache_control; String _last_modified; + AwsTemplateProcessor _callback; bool _isDir; bool _gzipFirst; uint8_t _gzipStats; @@ -55,6 +56,7 @@ class AsyncStaticWebHandler: public AsyncWebHandler { AsyncStaticWebHandler& setLastModified(time_t last_modified); AsyncStaticWebHandler& setLastModified(); //sets to current time. Make sure sntp is runing and time is updated #endif + AsyncStaticWebHandler& setTemplateProcessor(AwsTemplateProcessor newCallback) {_callback = newCallback; return *this;} }; class AsyncCallbackWebHandler: public AsyncWebHandler { diff --git a/src/WebHandlers.cpp b/src/WebHandlers.cpp index 1a528c6..d012920 100644 --- a/src/WebHandlers.cpp +++ b/src/WebHandlers.cpp @@ -22,7 +22,7 @@ #include "WebHandlerImpl.h" AsyncStaticWebHandler::AsyncStaticWebHandler(const char* uri, FS& fs, const char* path, const char* cache_control) - : _fs(fs), _uri(uri), _path(path), _default_file("index.htm"), _cache_control(cache_control), _last_modified("") + : _fs(fs), _uri(uri), _path(path), _default_file("index.htm"), _cache_control(cache_control), _last_modified(""), _callback(nullptr) { // Ensure leading '/' if (_uri.length() == 0 || _uri[0] != '/') _uri = "/" + _uri; @@ -199,7 +199,7 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) response->addHeader("ETag", etag); request->send(response); } else { - AsyncWebServerResponse * response = new AsyncFileResponse(request->_tempFile, filename); + AsyncWebServerResponse * response = new AsyncFileResponse(request->_tempFile, filename, String(), false, _callback); if (_last_modified.length()) response->addHeader("Last-Modified", _last_modified); if (_cache_control.length()){ diff --git a/src/WebRequest.cpp b/src/WebRequest.cpp index c5bfece..a43005d 100644 --- a/src/WebRequest.cpp +++ b/src/WebRequest.cpp @@ -710,78 +710,78 @@ AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(int code, const St return new AsyncBasicResponse(code, contentType, content); } -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(FS &fs, const String& path, const String& contentType, bool download){ +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ if(fs.exists(path) || (!download && fs.exists(path+".gz"))) - return new AsyncFileResponse(fs, path, contentType, download); + return new AsyncFileResponse(fs, path, contentType, download, callback); return NULL; } -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(File content, const String& path, const String& contentType, bool download){ +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ if(content == true) - return new AsyncFileResponse(content, path, contentType, download); + return new AsyncFileResponse(content, path, contentType, download, callback); return NULL; } -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(Stream &stream, const String& contentType, size_t len){ - return new AsyncStreamResponse(stream, contentType, len); +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback){ + return new AsyncStreamResponse(stream, contentType, len, callback); } -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(const String& contentType, size_t len, AwsResponseFiller callback){ - return new AsyncCallbackResponse(contentType, len, callback); +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ + return new AsyncCallbackResponse(contentType, len, callback, templateCallback); } -AsyncWebServerResponse * AsyncWebServerRequest::beginChunkedResponse(const String& contentType, AwsResponseFiller callback){ +AsyncWebServerResponse * AsyncWebServerRequest::beginChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ if(_version) - return new AsyncChunkedResponse(contentType, callback); - return new AsyncCallbackResponse(contentType, 0, callback); + return new AsyncChunkedResponse(contentType, callback, templateCallback); + return new AsyncCallbackResponse(contentType, 0, callback, templateCallback); } AsyncResponseStream * AsyncWebServerRequest::beginResponseStream(const String& contentType, size_t bufferSize){ return new AsyncResponseStream(contentType, bufferSize); } -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse_P(int code, const String& contentType, const uint8_t * content, size_t len){ - return new AsyncProgmemResponse(code, contentType, content, len); +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback){ + return new AsyncProgmemResponse(code, contentType, content, len, callback); } -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse_P(int code, const String& contentType, PGM_P content){ - return beginResponse_P(code, contentType, (const uint8_t *)content, strlen_P(content)); +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback){ + return beginResponse_P(code, contentType, (const uint8_t *)content, strlen_P(content), callback); } void AsyncWebServerRequest::send(int code, const String& contentType, const String& content){ send(beginResponse(code, contentType, content)); } -void AsyncWebServerRequest::send(FS &fs, const String& path, const String& contentType, bool download){ +void AsyncWebServerRequest::send(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ if(fs.exists(path) || (!download && fs.exists(path+".gz"))){ - send(beginResponse(fs, path, contentType, download)); + send(beginResponse(fs, path, contentType, download, callback)); } else send(404); } -void AsyncWebServerRequest::send(File content, const String& path, const String& contentType, bool download){ +void AsyncWebServerRequest::send(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ if(content == true){ - send(beginResponse(content, path, contentType, download)); + send(beginResponse(content, path, contentType, download, callback)); } else send(404); } -void AsyncWebServerRequest::send(Stream &stream, const String& contentType, size_t len){ - send(beginResponse(stream, contentType, len)); +void AsyncWebServerRequest::send(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback){ + send(beginResponse(stream, contentType, len, callback)); } -void AsyncWebServerRequest::send(const String& contentType, size_t len, AwsResponseFiller callback){ - send(beginResponse(contentType, len, callback)); +void AsyncWebServerRequest::send(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ + send(beginResponse(contentType, len, callback, templateCallback)); } -void AsyncWebServerRequest::sendChunked(const String& contentType, AwsResponseFiller callback){ - send(beginChunkedResponse(contentType, callback)); +void AsyncWebServerRequest::sendChunked(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ + send(beginChunkedResponse(contentType, callback, templateCallback)); } -void AsyncWebServerRequest::send_P(int code, const String& contentType, const uint8_t * content, size_t len){ - send(beginResponse_P(code, contentType, content, len)); +void AsyncWebServerRequest::send_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback){ + send(beginResponse_P(code, contentType, content, len, callback)); } -void AsyncWebServerRequest::send_P(int code, const String& contentType, PGM_P content){ - send(beginResponse_P(code, contentType, content)); +void AsyncWebServerRequest::send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback){ + send(beginResponse_P(code, contentType, content, callback)); } void AsyncWebServerRequest::redirect(const String& url){ diff --git a/src/WebResponseImpl.h b/src/WebResponseImpl.h index f234bf8..d7e8c38 100644 --- a/src/WebResponseImpl.h +++ b/src/WebResponseImpl.h @@ -21,6 +21,14 @@ #ifndef ASYNCWEBSERVERRESPONSEIMPL_H_ #define ASYNCWEBSERVERRESPONSEIMPL_H_ +#ifdef Arduino_h +// arduino is not compatible with std::vector +#undef min +#undef max +#endif +#include +// It is possible to restore these defines, but one can use _min and _max instead. Or std::min, std::max. + class AsyncBasicResponse: public AsyncWebServerResponse { private: String _content; @@ -34,13 +42,21 @@ class AsyncBasicResponse: public AsyncWebServerResponse { class AsyncAbstractResponse: public AsyncWebServerResponse { private: String _head; + std::vector _cache; + size_t _readDataFromCacheOrContent(uint8_t* data, const size_t len); + size_t _fillBufferAndProcessTemplates(uint8_t* buf, size_t maxLen); + protected: + AwsTemplateProcessor _callback; public: + AsyncAbstractResponse(AwsTemplateProcessor callback=nullptr); void _respond(AsyncWebServerRequest *request); size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); bool _sourceValid() const { return false; } virtual size_t _fillBuffer(uint8_t *buf __attribute__((unused)), size_t maxLen __attribute__((unused))) { return 0; } }; +#define TEMPLATE_PLACEHOLDER '%' +#define TEMPLATE_PARAM_NAME_LENGTH 32 class AsyncFileResponse: public AsyncAbstractResponse { using File = fs::File; using FS = fs::FS; @@ -49,8 +65,8 @@ class AsyncFileResponse: public AsyncAbstractResponse { String _path; void _setContentType(const String& path); public: - AsyncFileResponse(FS &fs, const String& path, const String& contentType=String(), bool download=false); - AsyncFileResponse(File content, const String& path, const String& contentType=String(), bool download=false); + AsyncFileResponse(FS &fs, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + AsyncFileResponse(File content, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); ~AsyncFileResponse(); bool _sourceValid() const { return !!(_content); } virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override; @@ -60,7 +76,7 @@ class AsyncStreamResponse: public AsyncAbstractResponse { private: Stream *_content; public: - AsyncStreamResponse(Stream &stream, const String& contentType, size_t len); + AsyncStreamResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback=nullptr); bool _sourceValid() const { return !!(_content); } virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override; }; @@ -68,8 +84,9 @@ class AsyncStreamResponse: public AsyncAbstractResponse { class AsyncCallbackResponse: public AsyncAbstractResponse { private: AwsResponseFiller _content; + size_t _filledLength; public: - AsyncCallbackResponse(const String& contentType, size_t len, AwsResponseFiller callback); + AsyncCallbackResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); bool _sourceValid() const { return !!(_content); } virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override; }; @@ -77,8 +94,9 @@ class AsyncCallbackResponse: public AsyncAbstractResponse { class AsyncChunkedResponse: public AsyncAbstractResponse { private: AwsResponseFiller _content; + size_t _filledLength; public: - AsyncChunkedResponse(const String& contentType, AwsResponseFiller callback); + AsyncChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); bool _sourceValid() const { return !!(_content); } virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override; }; @@ -86,8 +104,9 @@ class AsyncChunkedResponse: public AsyncAbstractResponse { class AsyncProgmemResponse: public AsyncAbstractResponse { private: const uint8_t * _content; + size_t _readLength; public: - AsyncProgmemResponse(int code, const String& contentType, const uint8_t * content, size_t len); + AsyncProgmemResponse(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback=nullptr); bool _sourceValid() const { return true; } virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override; }; diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index ae3e24f..423fd25 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -22,6 +22,17 @@ #include "WebResponseImpl.h" #include "cbuf.h" +// Since ESP8266 does not link memchr by default, here's its implementation. +void* memchr(void* ptr, int ch, size_t count) +{ + unsigned char* p = static_cast(ptr); + while(count--) + if(*p++ == static_cast(ch)) + return --p; + return nullptr; +} + + /* * Abstract Response * */ @@ -228,6 +239,16 @@ size_t AsyncBasicResponse::_ack(AsyncWebServerRequest *request, size_t len, uint * Abstract Response * */ +AsyncAbstractResponse::AsyncAbstractResponse(AwsTemplateProcessor callback): _callback(callback) +{ + // In case of template processing, we're unable to determine real response size + if(callback) { + _contentLength = 0; + _sendContentLength = false; + _chunked = true; + } +} + void AsyncAbstractResponse::_respond(AsyncWebServerRequest *request){ addHeader("Connection","close"); _head = _assembleHead(request->version()); @@ -277,6 +298,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u } if(headLen){ + //TODO: memcpy should be faster? sprintf((char*)buf, "%s", _head.c_str()); _head = String(); } @@ -295,7 +317,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u buf[outLen++] = '\r'; buf[outLen++] = '\n'; } else { - outLen = _fillBuffer(buf+headLen, outLen) + headLen; + outLen = _fillBufferAndProcessTemplates(buf+headLen, outLen) + headLen; } if(outLen) @@ -308,7 +330,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u free(buf); - if((_chunked && readLen == 0) || (!_sendContentLength && outLen == 0) || _sentLength == _contentLength){ + if((_chunked && readLen == 0) || (!_sendContentLength && outLen == 0) || (!_chunked && _sentLength == _contentLength)){ _state = RESPONSE_WAIT_ACK; } return outLen; @@ -323,7 +345,118 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u return 0; } +size_t AsyncAbstractResponse::_readDataFromCacheOrContent(uint8_t* data, const size_t len) +{ + // If we have something in cache, copy it to buffer + const size_t readFromCache = std::min(len, _cache.size()); + if(readFromCache) { + memcpy(data, _cache.data(), readFromCache); + _cache.erase(_cache.begin(), _cache.begin() + readFromCache); + } + // If we need to read more... + const size_t needFromFile = len - readFromCache; + const size_t readFromContent = _fillBuffer(data + readFromCache, needFromFile); + return readFromCache + readFromContent; +} +size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t* data, size_t len) +{ + if(!_callback) + return _fillBuffer(data, len); + + const size_t originalLen = len; + len = _readDataFromCacheOrContent(data, len); + // Now we've read 'len' bytes, either from cache or from file + // Search for template placeholders + uint8_t* pTemplateStart = data; + while((pTemplateStart < &data[len]) && (pTemplateStart = (uint8_t*)memchr(pTemplateStart, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart + 1))) { // data[0] ... data[len - 1] + uint8_t* pTemplateEnd = (pTemplateStart < &data[len - 1]) ? (uint8_t*)memchr(pTemplateStart + 1, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart) : nullptr; + // temporary buffer to hold parameter name + uint8_t buf[TEMPLATE_PARAM_NAME_LENGTH + 1]; + String paramName; + // cache position to insert remainder of template parameter value + std::vector::iterator i = _cache.end(); + // If closing placeholder is found: + if(pTemplateEnd) { + // prepare argument to callback + const size_t paramNameLength = std::min(sizeof(buf) - 1, (unsigned int)(pTemplateEnd - pTemplateStart - 1)); + if(paramNameLength) { + memcpy(buf, pTemplateStart + 1, paramNameLength); + buf[paramNameLength] = 0; + paramName = String(reinterpret_cast(buf)); + } else { // double percent sign encountered, this is single percent sign escaped. + // remove the 2nd percent sign + memmove(pTemplateEnd, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1); + len += _readDataFromCacheOrContent(&data[len - 1], 1) - 1; + ++pTemplateStart; + } + } else if(&data[len - 1] - pTemplateStart + 1 < TEMPLATE_PARAM_NAME_LENGTH + 2) { // closing placeholder not found, check if it's in the remaining file data + memcpy(buf, pTemplateStart + 1, &data[len - 1] - pTemplateStart); + const size_t readFromCacheOrContent = _readDataFromCacheOrContent(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PARAM_NAME_LENGTH + 2 - (&data[len - 1] - pTemplateStart + 1)); + if(readFromCacheOrContent) { + pTemplateEnd = (uint8_t*)memchr(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PLACEHOLDER, readFromCacheOrContent); + if(pTemplateEnd) { + // prepare argument to callback + *pTemplateEnd = 0; + paramName = String(reinterpret_cast(buf)); + // Copy remaining read-ahead data into cache (when std::vector::insert returning iterator will be available, these 3 lines can be simplified into 1) + const size_t pos = _cache.size(); + _cache.insert(_cache.end(), pTemplateEnd + 1, buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent); + i = _cache.begin() + pos; + pTemplateEnd = &data[len - 1]; + } + else // closing placeholder not found in file data, store found percent symbol as is and advance to the next position + { + // but first, store read file data in cache + _cache.insert(_cache.end(), buf + (&data[len - 1] - pTemplateStart), buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent); + ++pTemplateStart; + } + } + else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position + ++pTemplateStart; + } + else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position + ++pTemplateStart; + if(paramName.length()) { + // call callback and replace with result. + // Everything in range [pTemplateStart, pTemplateEnd] can be safely replaced with parameter value. + // Data after pTemplateEnd may need to be moved. + // The first byte of data after placeholder is located at pTemplateEnd + 1. + // It should be located at pTemplateStart + numBytesCopied (to begin right after inserted parameter value). + const String paramValue(_callback(paramName)); + const char* pvstr = paramValue.c_str(); + const unsigned int pvlen = paramValue.length(); + const size_t numBytesCopied = std::min(pvlen, static_cast(&data[originalLen - 1] - pTemplateStart + 1)); + // make room for param value + // 1. move extra data to cache if parameter value is longer than placeholder AND if there is no room to store + if((pTemplateEnd + 1 < pTemplateStart + numBytesCopied) && (originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1) < len)) { + size_t pos = i - _cache.begin(); + _cache.insert(i, &data[originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1)], &data[len]); + i = _cache.begin() + pos; + //2. parameter value is longer than placeholder text, push the data after placeholder which not saved into cache further to the end + memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[originalLen] - pTemplateStart - numBytesCopied); + } else if(pTemplateEnd + 1 != pTemplateStart + numBytesCopied) + //2. Either parameter value is shorter than placeholder text OR there is enough free space in buffer to fit. + // Move the entire data after the placeholder + memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1); + // 3. replace placeholder with actual value + memcpy(pTemplateStart, pvstr, numBytesCopied); + // If result is longer than buffer, copy the remainder into cache (this could happen only if placeholder text itself did not fit entirely in buffer) + if(numBytesCopied < pvlen) { + _cache.insert(i, pvstr + numBytesCopied, pvstr + pvlen); + } else if(pTemplateStart + numBytesCopied < pTemplateEnd + 1) { // result is copied fully; if result is shorter than placeholder text... + // there is some free room, fill it from cache + const size_t roomFreed = pTemplateEnd + 1 - pTemplateStart - numBytesCopied; + const size_t totalFreeRoom = originalLen - len + roomFreed; + len += _readDataFromCacheOrContent(&data[len - roomFreed], totalFreeRoom) - roomFreed; + } else { // result is copied fully; it is longer than placeholder text + const size_t roomTaken = pTemplateStart + numBytesCopied - pTemplateEnd - 1; + len = std::min(len + roomTaken, originalLen); + } + } + } // while(pTemplateStart) + return len; +} /* @@ -357,13 +490,16 @@ void AsyncFileResponse::_setContentType(const String& path){ else _contentType = "text/plain"; } -AsyncFileResponse::AsyncFileResponse(FS &fs, const String& path, const String& contentType, bool download){ +AsyncFileResponse::AsyncFileResponse(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback): AsyncAbstractResponse(callback){ _code = 200; _path = path; if(!download && !fs.exists(_path) && fs.exists(_path+".gz")){ _path = _path+".gz"; addHeader("Content-Encoding", "gzip"); + _callback = nullptr; // Unable to process zipped templates + _sendContentLength = true; + _chunked = false; } _content = fs.open(_path, "r"); @@ -386,18 +522,22 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String& path, const String& c snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename); } addHeader("Content-Disposition", buf); - } -AsyncFileResponse::AsyncFileResponse(File content, const String& path, const String& contentType, bool download){ +AsyncFileResponse::AsyncFileResponse(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback): AsyncAbstractResponse(callback){ _code = 200; _path = path; + + if(!download && String(content.name()).endsWith(".gz") && !path.endsWith(".gz")){ + addHeader("Content-Encoding", "gzip"); + _callback = nullptr; // Unable to process gzipped templates + _sendContentLength = true; + _chunked = false; + } + _content = content; _contentLength = _content.size(); - if(!download && String(_content.name()).endsWith(".gz") && !path.endsWith(".gz")) - addHeader("Content-Encoding", "gzip"); - if(contentType == "") _setContentType(path); else @@ -424,7 +564,7 @@ size_t AsyncFileResponse::_fillBuffer(uint8_t *data, size_t len){ * Stream Response * */ -AsyncStreamResponse::AsyncStreamResponse(Stream &stream, const String& contentType, size_t len){ +AsyncStreamResponse::AsyncStreamResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback): AsyncAbstractResponse(callback) { _code = 200; _content = &stream; _contentLength = len; @@ -444,55 +584,64 @@ size_t AsyncStreamResponse::_fillBuffer(uint8_t *data, size_t len){ * Callback Response * */ -AsyncCallbackResponse::AsyncCallbackResponse(const String& contentType, size_t len, AwsResponseFiller callback){ +AsyncCallbackResponse::AsyncCallbackResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback): AsyncAbstractResponse(templateCallback) { _code = 200; _content = callback; _contentLength = len; if(!len) _sendContentLength = false; _contentType = contentType; + _filledLength = 0; } size_t AsyncCallbackResponse::_fillBuffer(uint8_t *data, size_t len){ - return _content(data, len, _sentLength); + size_t ret = _content(data, len, _filledLength); + _filledLength += ret; + return ret; } /* * Chunked Response * */ -AsyncChunkedResponse::AsyncChunkedResponse(const String& contentType, AwsResponseFiller callback){ +AsyncChunkedResponse::AsyncChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor processorCallback): AsyncAbstractResponse(processorCallback) { _code = 200; _content = callback; _contentLength = 0; _contentType = contentType; _sendContentLength = false; _chunked = true; + _filledLength = 0; } size_t AsyncChunkedResponse::_fillBuffer(uint8_t *data, size_t len){ - return _content(data, len, _sentLength); + size_t ret = _content(data, len, _filledLength); + _filledLength += ret; + return ret; } /* * Progmem Response * */ -AsyncProgmemResponse::AsyncProgmemResponse(int code, const String& contentType, const uint8_t * content, size_t len){ +AsyncProgmemResponse::AsyncProgmemResponse(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback): AsyncAbstractResponse(callback) { _code = code; _content = content; _contentType = contentType; _contentLength = len; + _readLength = 0; } size_t AsyncProgmemResponse::_fillBuffer(uint8_t *data, size_t len){ - size_t left = _contentLength - _sentLength; - if (left > len) { - memcpy_P(data, _content + _sentLength, len); - return len; - } - memcpy_P(data, _content + _sentLength, left); - return left; + size_t left = _contentLength - _readLength; + if (left > len) { + memcpy_P(data, _content + _readLength, len); + _readLength += len; + return len; + } + memcpy_P(data, _content + _readLength, left); + _readLength += left; + return left; }