summaryrefslogtreecommitdiff
path: root/include/json_html_serializer.hpp
diff options
context:
space:
mode:
authorEd Tanous <ed.tanous@intel.com>2019-05-21 23:00:34 +0300
committerEd Tanous <ed@tanous.net>2020-09-24 05:01:21 +0300
commit57fce80e24cfe08e530e0697d6c70bba14076d1c (patch)
tree0b28771a7c7cd609d85008a223d20f66fe376b55 /include/json_html_serializer.hpp
parentbbf1a93eb6935c426deb0ecbcf3d8611b17aeb30 (diff)
downloadbmcweb-57fce80e24cfe08e530e0697d6c70bba14076d1c.tar.xz
Improve JSON->HTML conversion
The existing JSON to html conversion is quite unfortunate, as it runs several very expensive regular expressions on an output to properly invoke the correct behavior, and to escape things like links. This patchset adjusts the behavior to directly dump the tree to HTML, skipping the json step entirely. Most of the code was pulled from the nlohmann::serializer class. Small side node: This also resolves the CSP issue with the inline CSS classes that are currently embedded in the json UI. Note, in terms of user facing behavior, this finally fixes the CSS issue, so the div is now centered as designed. Previously it was left justified. Tested: Ran several redfish schemas and compared to old ones. Output appears the same in the window, and content security policy warnings are gone. Verified several links works as expected, and verified the behavior of all base types, as well as empty arrays and empty objects. All appear to work correctly. Signed-off-by: Ed Tanous <ed@tanous.net> Change-Id: Id9bf6dc33acb1603f009de4cd322e81d83f334be
Diffstat (limited to 'include/json_html_serializer.hpp')
-rw-r--r--include/json_html_serializer.hpp614
1 files changed, 614 insertions, 0 deletions
diff --git a/include/json_html_serializer.hpp b/include/json_html_serializer.hpp
new file mode 100644
index 0000000000..a7d5297b6f
--- /dev/null
+++ b/include/json_html_serializer.hpp
@@ -0,0 +1,614 @@
+#include <nlohmann/json.hpp>
+
+#include <algorithm>
+
+namespace json_html_util
+{
+
+static constexpr uint8_t utf8Accept = 0;
+static constexpr uint8_t utf8Reject = 1;
+
+inline uint8_t decode(uint8_t& state, uint32_t& codePoint,
+ const uint8_t byte) noexcept
+{
+ // clang-format off
+ static const std::array<std::uint8_t, 400> utf8d =
+ {
+ {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1F
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3F
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5F
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7F
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9F
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // A0..BF
+ 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C0..DF
+ 0xA, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // E0..EF
+ 0xB, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // F0..FF
+ 0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2
+ 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4
+ 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6
+ 1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // s7..s8
+ }
+ };
+ // clang-format on
+
+ if (state > 0x8)
+ {
+ return state;
+ }
+
+ const uint8_t type = utf8d[byte];
+
+ codePoint = (state != utf8Accept)
+ ? (byte & 0x3fu) | (codePoint << 6)
+ : static_cast<uint32_t>(0xff >> type) & (byte);
+
+ state = utf8d[256u + state * 16u + type];
+ return state;
+}
+
+inline void dumpEscaped(std::string& out, const std::string& str)
+{
+ std::array<char, 512> stringBuffer{{}};
+ uint32_t codePoint = 0;
+ uint8_t state = utf8Accept;
+ std::size_t bytes = 0; // number of bytes written to string_buffer
+
+ // number of bytes written at the point of the last valid byte
+ std::size_t bytes_after_last_accept = 0;
+ std::size_t undumpedChars = 0;
+
+ for (std::size_t i = 0; i < str.size(); ++i)
+ {
+ const uint8_t byte = static_cast<uint8_t>(str[i]);
+
+ switch (decode(state, codePoint, byte))
+ {
+ case utf8Accept: // decode found a new code point
+ {
+ switch (codePoint)
+ {
+ case 0x08: // backspace
+ {
+ stringBuffer[bytes++] = '\\';
+ stringBuffer[bytes++] = 'b';
+ break;
+ }
+
+ case 0x09: // horizontal tab
+ {
+ stringBuffer[bytes++] = '\\';
+ stringBuffer[bytes++] = 't';
+ break;
+ }
+
+ case 0x0A: // newline
+ {
+ stringBuffer[bytes++] = '\\';
+ stringBuffer[bytes++] = 'n';
+ break;
+ }
+
+ case 0x0C: // formfeed
+ {
+ stringBuffer[bytes++] = '\\';
+ stringBuffer[bytes++] = 'f';
+ break;
+ }
+
+ case 0x0D: // carriage return
+ {
+ stringBuffer[bytes++] = '\\';
+ stringBuffer[bytes++] = 'r';
+ break;
+ }
+
+ case 0x22: // quotation mark
+ {
+ stringBuffer[bytes++] = '&';
+ stringBuffer[bytes++] = 'q';
+ stringBuffer[bytes++] = 'u';
+ stringBuffer[bytes++] = 'o';
+ stringBuffer[bytes++] = 't';
+ stringBuffer[bytes++] = ';';
+ break;
+ }
+
+ case 0x27: // apostrophe
+ {
+ stringBuffer[bytes++] = '&';
+ stringBuffer[bytes++] = 'a';
+ stringBuffer[bytes++] = 'p';
+ stringBuffer[bytes++] = 'o';
+ stringBuffer[bytes++] = 's';
+ stringBuffer[bytes++] = ';';
+ break;
+ }
+
+ case 0x26: // ampersand
+ {
+ stringBuffer[bytes++] = '&';
+ stringBuffer[bytes++] = 'a';
+ stringBuffer[bytes++] = 'm';
+ stringBuffer[bytes++] = 'p';
+ stringBuffer[bytes++] = ';';
+ break;
+ }
+
+ case 0x3C: // less than
+ {
+ stringBuffer[bytes++] = '\\';
+ stringBuffer[bytes++] = 'l';
+ stringBuffer[bytes++] = 't';
+ stringBuffer[bytes++] = ';';
+ break;
+ }
+
+ case 0x3E: // greater than
+ {
+ stringBuffer[bytes++] = '\\';
+ stringBuffer[bytes++] = 'g';
+ stringBuffer[bytes++] = 't';
+ stringBuffer[bytes++] = ';';
+ break;
+ }
+
+ default:
+ {
+ // escape control characters (0x00..0x1F)
+ if ((codePoint <= 0x1F) or (codePoint >= 0x7F))
+ {
+ if (codePoint <= 0xFFFF)
+ {
+ (std::snprintf)(
+ stringBuffer.data() + bytes, 7, "\\u%04x",
+ static_cast<uint16_t>(codePoint));
+ bytes += 6;
+ }
+ else
+ {
+ (std::snprintf)(
+ stringBuffer.data() + bytes, 13,
+ "\\u%04x\\u%04x",
+ static_cast<uint16_t>(0xD7C0 +
+ (codePoint >> 10)),
+ static_cast<uint16_t>(0xDC00 +
+ (codePoint & 0x3FF)));
+ bytes += 12;
+ }
+ }
+ else
+ {
+ // copy byte to buffer (all previous bytes
+ // been copied have in default case above)
+ stringBuffer[bytes++] = str[i];
+ }
+ break;
+ }
+ }
+
+ // write buffer and reset index; there must be 13 bytes
+ // left, as this is the maximal number of bytes to be
+ // written ("\uxxxx\uxxxx\0") for one code point
+ if (stringBuffer.size() - bytes < 13)
+ {
+ out.append(stringBuffer.data(), bytes);
+ bytes = 0;
+ }
+
+ // remember the byte position of this accept
+ bytes_after_last_accept = bytes;
+ undumpedChars = 0;
+ break;
+ }
+
+ case utf8Reject: // decode found invalid UTF-8 byte
+ {
+ // in case we saw this character the first time, we
+ // would like to read it again, because the byte
+ // may be OK for itself, but just not OK for the
+ // previous sequence
+ if (undumpedChars > 0)
+ {
+ --i;
+ }
+
+ // reset length buffer to the last accepted index;
+ // thus removing/ignoring the invalid characters
+ bytes = bytes_after_last_accept;
+
+ stringBuffer[bytes++] = '\\';
+ stringBuffer[bytes++] = 'u';
+ stringBuffer[bytes++] = 'f';
+ stringBuffer[bytes++] = 'f';
+ stringBuffer[bytes++] = 'f';
+ stringBuffer[bytes++] = 'd';
+
+ bytes_after_last_accept = bytes;
+
+ undumpedChars = 0;
+
+ // continue processing the string
+ state = utf8Accept;
+ break;
+
+ break;
+ }
+
+ default: // decode found yet incomplete multi-byte code point
+ {
+ ++undumpedChars;
+ break;
+ }
+ }
+ }
+
+ // we finished processing the string
+ if (state == utf8Accept)
+ {
+ // write buffer
+ if (bytes > 0)
+ {
+ out.append(stringBuffer.data(), bytes);
+ }
+ }
+ else
+ {
+ // write all accepted bytes
+ out.append(stringBuffer.data(), bytes_after_last_accept);
+ out += "\\ufffd";
+ }
+}
+
+inline unsigned int countDigits(uint64_t number) noexcept
+{
+ unsigned int n_digits = 1;
+ for (;;)
+ {
+ if (number < 10)
+ {
+ return n_digits;
+ }
+ if (number < 100)
+ {
+ return n_digits + 1;
+ }
+ if (number < 1000)
+ {
+ return n_digits + 2;
+ }
+ if (number < 10000)
+ {
+ return n_digits + 3;
+ }
+ number = number / 10000u;
+ n_digits += 4;
+ }
+}
+
+template <typename NumberType,
+ std::enable_if_t<std::is_same<NumberType, uint64_t>::value or
+ std::is_same<NumberType, int64_t>::value,
+ int> = 0>
+void dumpInteger(std::string& out, NumberType number)
+{
+ std::array<char, 64> numberbuffer{{}};
+
+ static constexpr std::array<std::array<char, 2>, 100> digits_to_99{{
+ {'0', '0'}, {'0', '1'}, {'0', '2'}, {'0', '3'}, {'0', '4'}, {'0', '5'},
+ {'0', '6'}, {'0', '7'}, {'0', '8'}, {'0', '9'}, {'1', '0'}, {'1', '1'},
+ {'1', '2'}, {'1', '3'}, {'1', '4'}, {'1', '5'}, {'1', '6'}, {'1', '7'},
+ {'1', '8'}, {'1', '9'}, {'2', '0'}, {'2', '1'}, {'2', '2'}, {'2', '3'},
+ {'2', '4'}, {'2', '5'}, {'2', '6'}, {'2', '7'}, {'2', '8'}, {'2', '9'},
+ {'3', '0'}, {'3', '1'}, {'3', '2'}, {'3', '3'}, {'3', '4'}, {'3', '5'},
+ {'3', '6'}, {'3', '7'}, {'3', '8'}, {'3', '9'}, {'4', '0'}, {'4', '1'},
+ {'4', '2'}, {'4', '3'}, {'4', '4'}, {'4', '5'}, {'4', '6'}, {'4', '7'},
+ {'4', '8'}, {'4', '9'}, {'5', '0'}, {'5', '1'}, {'5', '2'}, {'5', '3'},
+ {'5', '4'}, {'5', '5'}, {'5', '6'}, {'5', '7'}, {'5', '8'}, {'5', '9'},
+ {'6', '0'}, {'6', '1'}, {'6', '2'}, {'6', '3'}, {'6', '4'}, {'6', '5'},
+ {'6', '6'}, {'6', '7'}, {'6', '8'}, {'6', '9'}, {'7', '0'}, {'7', '1'},
+ {'7', '2'}, {'7', '3'}, {'7', '4'}, {'7', '5'}, {'7', '6'}, {'7', '7'},
+ {'7', '8'}, {'7', '9'}, {'8', '0'}, {'8', '1'}, {'8', '2'}, {'8', '3'},
+ {'8', '4'}, {'8', '5'}, {'8', '6'}, {'8', '7'}, {'8', '8'}, {'8', '9'},
+ {'9', '0'}, {'9', '1'}, {'9', '2'}, {'9', '3'}, {'9', '4'}, {'9', '5'},
+ {'9', '6'}, {'9', '7'}, {'9', '8'}, {'9', '9'},
+ }};
+
+ // special case for "0"
+ if (number == 0)
+ {
+ out += '0';
+ return;
+ }
+
+ // use a pointer to fill the buffer
+ auto bufferPtr = begin(numberbuffer);
+
+ const bool isNegative = std::is_same<NumberType, int64_t>::value &&
+ !(number >= 0); // see issue #755
+ uint64_t absValue;
+
+ unsigned int nChars;
+
+ if (isNegative)
+ {
+ *bufferPtr = '-';
+ absValue = static_cast<uint64_t>(0 - number);
+
+ // account one more byte for the minus sign
+ nChars = 1 + countDigits(absValue);
+ }
+ else
+ {
+ absValue = static_cast<uint64_t>(number);
+ nChars = countDigits(absValue);
+ }
+
+ // spare 1 byte for '\0'
+ if (nChars >= numberbuffer.size() - 1)
+ {
+ return;
+ }
+
+ // jump to the end to generate the string from backward
+ // so we later avoid reversing the result
+ bufferPtr += nChars;
+
+ // Fast int2ascii implementation inspired by "Fastware" talk by Andrei
+ // Alexandrescu See: https://www.youtube.com/watch?v=o4-CwDo2zpg
+ while (absValue >= 100)
+ {
+ const auto digitsIndex = static_cast<unsigned>((absValue % 100));
+ absValue /= 100;
+ *(--bufferPtr) = digits_to_99[digitsIndex][1];
+ *(--bufferPtr) = digits_to_99[digitsIndex][0];
+ }
+
+ if (absValue >= 10)
+ {
+ const auto digitsIndex = static_cast<unsigned>(absValue);
+ *(--bufferPtr) = digits_to_99[digitsIndex][1];
+ *(--bufferPtr) = digits_to_99[digitsIndex][0];
+ }
+ else
+ {
+ *(--bufferPtr) = static_cast<char>('0' + absValue);
+ }
+
+ out.append(numberbuffer.data(), nChars);
+}
+
+inline void dumpfloat(std::string& out, double number,
+ std::true_type /*isIeeeSingleOrDouble*/)
+{
+ std::array<char, 64> numberbuffer{{}};
+ char* begin = numberbuffer.data();
+ ::nlohmann::detail::to_chars(begin, begin + numberbuffer.size(), number);
+
+ out += begin;
+}
+
+inline void dumpfloat(std::string& out, double number,
+ std::false_type /*isIeeeSingleOrDouble*/)
+{
+ std::array<char, 64> numberbuffer{{}};
+ // get number of digits for a float -> text -> float round-trip
+ static constexpr auto d = std::numeric_limits<double>::max_digits10;
+
+ // the actual conversion
+ std::ptrdiff_t len = (std::snprintf)(
+ numberbuffer.data(), numberbuffer.size(), "%.*g", d, number);
+
+ // negative value indicates an error
+ if (len <= 0)
+ {
+ return;
+ }
+
+ // check if buffer was large enough
+ if (numberbuffer.size() < static_cast<std::size_t>(len))
+ {
+ return;
+ }
+
+ const auto end =
+ std::remove(numberbuffer.begin(), numberbuffer.begin() + len, ',');
+ std::fill(end, numberbuffer.end(), '\0');
+
+ if ((end - numberbuffer.begin()) > len)
+ {
+ return;
+ }
+ len = (end - numberbuffer.begin());
+
+ out.append(numberbuffer.data(), static_cast<std::size_t>(len));
+
+ // determine if need to append ".0"
+ const bool valueIsIntLike =
+ std::none_of(numberbuffer.begin(), numberbuffer.begin() + len + 1,
+ [](char c) { return (c == '.' or c == 'e'); });
+
+ if (valueIsIntLike)
+ {
+ out += ".0";
+ }
+}
+
+inline void dumpfloat(std::string& out, double number)
+{
+ // NaN / inf
+ if (!std::isfinite(number))
+ {
+ out += "null";
+ return;
+ }
+
+ // If float is an IEEE-754 single or double precision number,
+ // use the Grisu2 algorithm to produce short numbers which are
+ // guaranteed to round-trip, using strtof and strtod, resp.
+ //
+ // NB: The test below works if <long double> == <double>.
+ static constexpr bool isIeeeSingleOrDouble =
+ (std::numeric_limits<double>::is_iec559 and
+ std::numeric_limits<double>::digits == 24 and
+ std::numeric_limits<double>::max_exponent == 128) or
+ (std::numeric_limits<double>::is_iec559 and
+ std::numeric_limits<double>::digits == 53 and
+ std::numeric_limits<double>::max_exponent == 1024);
+
+ dumpfloat(out, number,
+ std::integral_constant<bool, isIeeeSingleOrDouble>());
+}
+
+inline void dump(std::string& out, const nlohmann::json& val)
+{
+ switch (val.type())
+ {
+ case nlohmann::json::value_t::object:
+ {
+ if (val.empty())
+ {
+ out += "{}";
+ return;
+ }
+
+ out += "{";
+
+ out += "<div class=tab>";
+ for (auto i = val.begin(); i != val.end();)
+ {
+ out += "&quot";
+ dumpEscaped(out, i.key());
+ out += "&quot: ";
+
+ bool inATag = false;
+ if (i.key() == "@odata.id" || i.key() == "@odata.context" ||
+ i.key() == "Members@odata.nextLink" || i.key() == "Uri")
+ {
+ inATag = true;
+ out += "<a href=\"";
+ dumpEscaped(out, i.value());
+ out += "\">";
+ }
+ dump(out, i.value());
+ if (inATag)
+ {
+ out += "</a>";
+ }
+ i++;
+ if (i != val.end())
+ {
+ out += ",";
+ }
+ out += "<br>";
+ }
+ out += "</div>";
+ out += '}';
+
+ return;
+ }
+
+ case nlohmann::json::value_t::array:
+ {
+ if (val.empty())
+ {
+ out += "[]";
+ return;
+ }
+
+ out += "[";
+
+ out += "<div class=tab>";
+
+ // first n-1 elements
+ for (auto i = val.cbegin(); i != val.cend() - 1; ++i)
+ {
+ dump(out, *i);
+ out += ",<br>";
+ }
+
+ // last element
+ dump(out, val.back());
+
+ out += "</div>";
+ out += ']';
+
+ return;
+ }
+
+ case nlohmann::json::value_t::string:
+ {
+ out += '\"';
+ const std::string* ptr = val.get_ptr<const std::string*>();
+ dumpEscaped(out, *ptr);
+ out += '\"';
+ return;
+ }
+
+ case nlohmann::json::value_t::boolean:
+ {
+ if (*(val.get_ptr<const bool*>()))
+ {
+ out += "true";
+ }
+ else
+ {
+ out += "false";
+ }
+ return;
+ }
+
+ case nlohmann::json::value_t::number_integer:
+ {
+ dumpInteger(out, *(val.get_ptr<const int64_t*>()));
+ return;
+ }
+
+ case nlohmann::json::value_t::number_unsigned:
+ {
+ dumpInteger(out, *(val.get_ptr<const uint64_t*>()));
+ return;
+ }
+
+ case nlohmann::json::value_t::number_float:
+ {
+ dumpfloat(out, *(val.get_ptr<const double*>()));
+ return;
+ }
+
+ case nlohmann::json::value_t::discarded:
+ {
+ out += "<discarded>";
+ return;
+ }
+
+ case nlohmann::json::value_t::null:
+ {
+ out += "null";
+ return;
+ }
+ }
+}
+
+inline void dumpHtml(std::string& out, const nlohmann::json& json)
+{
+ out += "<html>\n"
+ "<head>\n"
+ "<title>Redfish API</title>\n"
+ "<link href=\"/redfish.css\" rel=\"stylesheet\">\n"
+ "</head>\n"
+ "<body>\n"
+ "<div class=\"container\">\n"
+ "<img src=\"/DMTF_Redfish_logo_2017.svg\" alt=\"redfish\" "
+ "height=\"406px\" "
+ "width=\"576px\">\n"
+ "<div class=\"content\">\n";
+ dump(out, json);
+ out += "</div>\n"
+ "</div>\n"
+ "</body>\n"
+ "</html>\n";
+}
+
+} // namespace json_html_util