From 0dcf25b8c77266490502626bf64097bc95274123 Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Wed, 17 Feb 2021 16:10:30 +0100 Subject: [PATCH 1/3] Add a test for #8542 --- .../unit/OutputReportTabular.unit.cc | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/tst/EnergyPlus/unit/OutputReportTabular.unit.cc b/tst/EnergyPlus/unit/OutputReportTabular.unit.cc index 8832a3a0a5e..96ebc2c143f 100644 --- a/tst/EnergyPlus/unit/OutputReportTabular.unit.cc +++ b/tst/EnergyPlus/unit/OutputReportTabular.unit.cc @@ -73,6 +73,7 @@ #include #include #include +#include #include #include #include @@ -8423,3 +8424,98 @@ TEST_F(EnergyPlusFixture, OutputReportTabularMonthly_8317_ValidateOutputTableMon compare_err_stream(expected_error); } + + +TEST_F(SQLiteFixture, OutputReportTabularTest_EscapeHTML) +{ + // Test for #8542 - Ensures strings are escaped before going to HTML + EnergyPlus::sqlite->sqliteBegin(); + EnergyPlus::sqlite->createSQLiteSimulationsRecord(1, "EnergyPlus Version", "Current Time"); + + auto &ort(state->dataOutRptTab); + ort->numStyles = 1; + ort->TableStyle(1) = OutputReportTabular::iTableStyle::HTML; + ort->del(1) = DataStringGlobals::CharSpace; // space - this is not used much for HTML output + + ort->WriteTabularFiles = true; + + SetupUnitConversions(*state); + ort->unitsStyle = OutputReportTabular::iUnitsStyle::JtoKWH; + + SetPredefinedTables(*state); + std::string CompName = "My Coil "; + + PreDefTableEntry(*state, state->dataOutRptPredefined->pdchDXCoolCoilType, CompName, "Coil:Cooling:DX:SingleSpeed"); + // This would normally be called with numerics such as CompName, 0.006, 8, but I don't really care + PreDefTableEntry(*state, state->dataOutRptPredefined->pdch2CoilLvgHumRatIdealPeak, CompName, "My Design Day where it's >= 8\u00B0"); + PreDefTableEntry(*state, state->dataOutRptPredefined->pdst2CoilSummaryCoilSelection, CompName, "My Design Day where it's >= 8\u00B0"); // this is >= 8 degree sign + + // We enable the reports we care about, making sure we have the right ones + EXPECT_EQ("HVACSizingSummary", state->dataOutRptPredefined->reportName(6).name); + state->dataOutRptPredefined->reportName(6).show = true; + + OutputReportTabular::OpenOutputTabularFile(*state); + + WritePredefinedTables(*state); + + OutputReportTabular::CloseOutputTabularFile(*state); + + std::vector lines = read_lines_in_file(DataStringGlobals::outputTblHtmFileName); + + // Lambda helper to locate a line in the html file, and compare that line with the expected html after trimming + auto compare_html_output = [this, &lines](const std::string& lookup, const std::string& expectedHTMLString) { + std::string found_cell; + for (const auto& line: lines) { + if (line.find(lookup) != std::string::npos) { + found_cell = line; + break; + } + } + EXPECT_FALSE(found_cell.empty()) + << "Did not find the lookup string '" << lookup + << "' string in the html output at '" << DataStringGlobals::outputTblHtmFileName + << "'..." << '\n' << delimited_string(lines); + + // Trim leading and trailing spaces + found_cell.erase(0, found_cell.find_first_not_of(' ')); // ltrim + found_cell.erase(found_cell.find_last_not_of(' ') + 1); // rtrim + + EXPECT_EQ(expectedHTMLString, found_cell) << found_cell; + }; + + compare_html_output("My Coil", "My Coil <coil is DX>"); + + // Note that I DO NOT expect `'` to be escaped by `'` like it would in xml. Technically HTML4 doesn't support that, though most browsers + // would anyways. Also, escaping single and double quotes is only needed inside attributes + compare_html_output("My Design Day", "My Design Day where it's >= 8°"); + + + // We ensure that SQL doesn't have the same escape + for (const std::string reportName: {"HVACSizingSummary"}) { + + + auto result = queryResult("SELECT RowName, Value From TabularDataWithStrings " + "WHERE ReportName = \"" + reportName + "\"" + " AND ColumnName = \"Coil Leaving Air Humidity Ratio at Ideal Loads Peak\"", + "TabularDataWithStrings"); + + EnergyPlus::sqlite->sqliteCommit(); + + EXPECT_EQ(1u, result.size()); + // Because the table has 8 cols + EXPECT_EQ(8u, result[0].size()); + + // 0.006 is a ratio, so unitconv = 1 + std::string s = result[0][0]; + // Trim the string, it has leading spaces + //s.erase(std::remove_if(s.begin(), s.end(), ::isspace), s.end()); + + EXPECT_EQ("My Coil ", s); + + EXPECT_EQ("My Design Day where it's >= 8\u00B0", result[0][1]); + } + + // Clean up + FileSystem::removeFile(DataStringGlobals::outputTblHtmFileName); + +} From 166feaf8dea9f7692384171fc3e7cdf6c51b01da Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Wed, 17 Feb 2021 16:12:10 +0100 Subject: [PATCH 2/3] Fix #8542 - Escape HTML characters. Added an optional boolean argument to ConvertToEscape to not convert " and ' like it would in the original routine which was meant for XML. This is to support HTML4 mostly, but might as well be correct. --- src/EnergyPlus/OutputReportTabular.cc | 21 ++++++++++++--------- src/EnergyPlus/OutputReportTabular.hh | 3 ++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/EnergyPlus/OutputReportTabular.cc b/src/EnergyPlus/OutputReportTabular.cc index d9c4d2ba2cb..8a41bb0edf6 100644 --- a/src/EnergyPlus/OutputReportTabular.cc +++ b/src/EnergyPlus/OutputReportTabular.cc @@ -14188,7 +14188,7 @@ namespace EnergyPlus::OutputReportTabular { for (iCol = 1; iCol <= colsColumnLabels; ++iCol) { outputLine = " "; for (jRow = 1; jRow <= maxNumColLabelRows; ++jRow) { - outputLine += colLabelMulti(iCol, jRow); + outputLine += ConvertToEscaped(colLabelMulti(iCol, jRow), false); if (jRow < maxNumColLabelRows) { outputLine += "
"; } @@ -14200,13 +14200,13 @@ namespace EnergyPlus::OutputReportTabular { for (jRow = 1; jRow <= rowsBody; ++jRow) { tbl_stream << " \n"; if (rowLabels(jRow) != "") { - tbl_stream << " " << InsertCurrencySymbol(state, rowLabels(jRow), true) << "\n"; + tbl_stream << " " << ConvertToEscaped(InsertCurrencySymbol(state, rowLabels(jRow), true), false) << "\n"; } else { tbl_stream << "  \n"; } for (iCol = 1; iCol <= colsBody; ++iCol) { if (body(iCol, jRow) != "") { - tbl_stream << " " << InsertCurrencySymbol(state, body(iCol, jRow), true) << "\n"; + tbl_stream << " " << ConvertToEscaped(InsertCurrencySymbol(state, body(iCol, jRow), true), false) << "\n"; } else { tbl_stream << "  \n"; } @@ -14541,7 +14541,7 @@ namespace EnergyPlus::OutputReportTabular { return s; } - std::string ConvertToEscaped(std::string const &inString) // Input String + std::string ConvertToEscaped(std::string const &inString, bool isXML) // Input String { // SUBROUTINE INFORMATION: // AUTHOR Jason Glazer @@ -14552,7 +14552,10 @@ namespace EnergyPlus::OutputReportTabular { // PURPOSE OF THIS SUBROUTINE: // Convert to XML safe escaped character string // so it excludes: - // " ' < > & + // " ' < > & degree-sign + // If isXML=false, it does an HTML conversion, so only ` < > & ` and degree sign + // Technically HTML4 doesn't support ", though most browsers would anyways. + // Also, escaping single and double quotes is only needed inside attributes if (inString.empty()) return ""; @@ -14565,11 +14568,11 @@ namespace EnergyPlus::OutputReportTabular { while (true) { if (index == inputSize) break; c = inString[index++]; - if (c == '\"') { + if ((c == '\"') && isXML) { s += """; } else if (c == '&') { s += "&"; - } else if (c == '\'') { + } else if ((c == '\'') && isXML) { s += "'"; } else if (c == '<') { s += "<"; @@ -14594,9 +14597,9 @@ namespace EnergyPlus::OutputReportTabular { } else if (c == '\\') { if (index == inputSize) break; c = inString[index++]; - if (c == '"') { + if ((c == '"') && isXML) { s += """; - } else if (c == '\'') { + } else if ((c == '\'') && isXML) { s += "'"; } else if (c == 'u' || c == 'x') { int remainingLen = inputSize - index; diff --git a/src/EnergyPlus/OutputReportTabular.hh b/src/EnergyPlus/OutputReportTabular.hh index 38fc60ffd49..3156a67724c 100644 --- a/src/EnergyPlus/OutputReportTabular.hh +++ b/src/EnergyPlus/OutputReportTabular.hh @@ -716,7 +716,8 @@ namespace OutputReportTabular { std::string ConvertUnicodeToUTF8(unsigned long const codepoint); - std::string ConvertToEscaped(std::string const &inString); // Input String + std::string ConvertToEscaped(std::string const &inString, // Input String + bool isXML=true); // isXML if false assumes HTML and will not convert quotes and apostrophes, for HTML4 void DetermineBuildingFloorArea(EnergyPlusData &state); From 7ac466d70289144c95ebe492400ecd3857e33002 Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Wed, 17 Feb 2021 16:45:15 +0100 Subject: [PATCH 3/3] Couple of extra things atop the file (like a "Header") that could need escaping as well --- src/EnergyPlus/OutputReportTabular.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EnergyPlus/OutputReportTabular.cc b/src/EnergyPlus/OutputReportTabular.cc index 8a41bb0edf6..516a72874e0 100644 --- a/src/EnergyPlus/OutputReportTabular.cc +++ b/src/EnergyPlus/OutputReportTabular.cc @@ -3074,11 +3074,11 @@ namespace EnergyPlus::OutputReportTabular { tbl_stream << "\n"; tbl_stream << "

Program Version:" << VerString << "

\n"; tbl_stream << "

Tabular Output Report in Format: HTML

\n"; - tbl_stream << "

Building: " << BuildingName << "

\n"; + tbl_stream << "

Building: " << ConvertToEscaped(BuildingName, false) << "

\n"; if (state.dataEnvrn->EnvironmentName == state.dataEnvrn->WeatherFileLocationTitle) { - tbl_stream << "

Environment: " << state.dataEnvrn->EnvironmentName << "

\n"; + tbl_stream << "

Environment: " << ConvertToEscaped(state.dataEnvrn->EnvironmentName, false) << "

\n"; } else { - tbl_stream << "

Environment: " << state.dataEnvrn->EnvironmentName << " ** " << state.dataEnvrn->WeatherFileLocationTitle << "

\n"; + tbl_stream << "

Environment: " << ConvertToEscaped(state.dataEnvrn->EnvironmentName, false) << " ** " << ConvertToEscaped(state.dataEnvrn->WeatherFileLocationTitle, false) << "

\n"; } tbl_stream << "

Simulation Timestamp: " << std::setw(4) << ort->td(1) << '-' << std::setfill('0') << std::setw(2) << ort->td(2) << '-' << std::setw(2) << ort->td(3) << '\n';