/*
* HtmlUtils.cpp
*
* Copyright (C) 2022 by Posit Software, PBC
*
* Unless you have received this program directly from Posit Software pursuant
* to the terms of a commercial license agreement with Posit Software, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace boost::placeholders;
namespace rstudio {
namespace core {
namespace html_utils {
HTML::HTML(const std::string& text, bool isHTML)
{
if (!isHTML)
text_ = string_utils::htmlEscape(text);
else
text_ = text;
}
std::string defaultTitle(const std::string& htmlContent)
{
boost::regex re("<[Hh]([1-6]).*?>(.*?)[Hh]\\1>");
boost::smatch match;
if (regex_utils::search(htmlContent, match, re))
return match[2];
else
return "";
}
Base64ImageFilter::Base64ImageFilter(const FilePath& basePath)
: boost::iostreams::regex_filter(
boost::regex(
R"((<\s*[Ii][Mm][Gg] [^\>]*[Ss][Rr][Cc]\s*=\s*)(["'])(.*?)(\2))"),
boost::bind(&Base64ImageFilter::toBase64Image, this, _1)),
basePath_(basePath)
{
}
std::string Base64ImageFilter::toBase64Image(const boost::cmatch& match)
{
// extract image reference
std::string imgRef = match[3];
// url decode it
imgRef = http::util::urlDecode(imgRef);
// see if this is an image within the base directory. if it is then
// base64 encode it
FilePath imagePath = basePath_.completeChildPath(imgRef);
if (imagePath.exists() &&
boost::algorithm::starts_with(imagePath.getMimeContentType(), "image/"))
{
std::string imageBase64;
Error error = core::base64::encode(imagePath, &imageBase64);
if (!error)
{
imgRef = "data:" + imagePath.getMimeContentType() + ";base64,";
imgRef.append(imageBase64);
}
else
{
LOG_ERROR(error);
}
}
// return the filtered result
return match[1] + match[2] + imgRef + match[4];
}
// convert fonts to base64
CssUrlFilter::CssUrlFilter(const FilePath& basePath)
: boost::iostreams::regex_filter(
boost::regex("url\\('([^'']+)'\\)"),
boost::bind(&CssUrlFilter::toBase64Url, this, _1)),
basePath_(basePath)
{
}
std::string CssUrlFilter::toBase64Url(const boost::cmatch& match)
{
// is this a local file?
std::string urlRef = match[1];
FilePath urlPath = basePath_.completeChildPath(urlRef);
std::string ext = urlPath.getExtensionLowerCase();
if (urlPath.exists() && (ext == ".ttf" || ext == ".otf"))
{
std::string fontBase64;
Error error = core::base64::encode(urlPath, &fontBase64);
if (!error)
{
// return base64 encoded font
std::string type = (ext == ".ttf") ? "truetype" : "opentype";
boost::format fmt("url(data:font/%1%;base64,%2%)");
return boost::str(fmt % type % fontBase64);
}
else
{
LOG_ERROR(error);
return match[0];
}
}
else
{
return match[0];
}
}
TextRange findClosestRange(std::string::const_iterator pos,
const std::vector& ranges)
{
TextRange closestRange = ranges.front();
for (const auto& range : ranges)
{
if (std::abs(range.begin - pos) < std::abs(closestRange.begin - pos))
closestRange = range;
}
return closestRange;
}
void HtmlPreserver::preserve(std::string* pInput)
{
// begin and end regexes
boost::regex beginPreserve("");
boost::regex endPreserve("");
// discover ranges we need to preserve
std::vector ranges;
std::string::const_iterator pos = pInput->begin();
std::string::const_iterator inputEnd = pInput->end();
while (pos != inputEnd)
{
// look for begin marker
boost::smatch m;
if (regex_utils::search(pos, inputEnd, m, beginPreserve))
{
// set begin iterator
std::string::const_iterator begin = m[0].first;
std::string::const_iterator end = m[0].second;
// look for end marker
if (regex_utils::search(end, inputEnd, m, endPreserve))
{
// update end to be the end of the match
end = m[0].second;
}
else
{
// didn't find a matching end pattern so set the end to the
// end of the document -- this will cause us to exclude the
// rest of the document from processing
end = inputEnd;
}
// mark everything before the match as requiring processing
ranges.push_back(TextRange(true, pos, begin));
// add the matched range to our list
ranges.push_back(TextRange(false, begin, end));
// update the position
pos = end + 1;
}
else
{
// no more preserve regions, set termination condition
ranges.push_back(TextRange(true, pos, pInput->end()));
pos = inputEnd;
}
}
// substitute guids for all of the matched ranges
std::string modifiedInput;
for (auto& range : ranges)
{
if (range.process)
{
modifiedInput += std::string(range.begin, range.end);
}
else
{
std::string guid = core::system::generateUuid();
std::string html = std::string(range.begin, range.end);
preserved_[guid] = html;
modifiedInput += guid;
}
}
// return the modified input
*pInput = modifiedInput;
}
void HtmlPreserver::restore(std::string* pOutput)
{
for (const auto& preserve : preserved_)
{
boost::algorithm::replace_first(*pOutput,
preserve.first,
preserve.second);
}
}
} // namespace html_utils
} // namespace core
} // namespace rstudio