Template Function Code review? - secure_img
Added by Christian Meyer about 1 month ago
Hello!
I created this template function to enable "secure" images...
It should discourage others to try to scrape Images from docroot ...
This also enables showing pictures only to logged in users and keeping this data private.
It works, and I just want to give it to the community =)
If you spot things that might be done better, please let me know.
bool secure_img(Wt::WTemplate *t,
const std::vector<Wt::WString>& args,
std::ostream& result)
{
if(args.size() < 1)
{
Wt::log("info") << "Template Function secure_img needs at least 1 argument";
return false;
}
// find if style classes are given
auto classIndex = std::find_if(args.begin(), args.end(), [](const Wt::WString& ws) {
auto s = ws.toUTF8();
return s.size() >= 6 && s.compare(0, 6, "class=") == 0;
});
// find if alt text is given
auto altIndex = std::find_if(args.begin(), args.end(), [](const Wt::WString& ws) {
auto s = ws.toUTF8();
return s.size() >= 4 && s.compare(0, 4, "alt=") == 0;
});
auto imgFile = t->addChild(std::make_unique<Wt::WFileResource>());
imgFile->setFileName(Wt::WApplication::instance()->appRoot() + args[0].toUTF8());
Wt::WImage wimg{};
wimg.setImageLink(imgFile->generateUrl());
if(altIndex != args.end())
{
// somehow "" is not part of the string, even if it is given in the template
auto altText = (*altIndex).toUTF8().substr(4);
wimg.setAlternateText(altText);
}
if(classIndex != args.end())
{
auto classesString = (*classIndex).toUTF8().substr(6);
wimg.addStyleClass(classesString);
}
wimg.htmlText(result);
return true;
}
Usage: ${secure_img:/path/to/approot/img <alt="Optional Alt Text"> <class="optional style classes">}
First Parameter is Path, Order of 'alt' and 'class' does not matter
Currently I have not tried to use another bound variable to load path from db.
UnTested: ${secure_img:${db_img_path}}
: Should this just work, or is there something to make sure of?
Any comment is welcome =)
Cheers
Christian
Replies (3)
RE: Template Function Code review? - secure_img - Added by Matthias Van Ceulebroeck 29 days ago
Hey Christian,
Thank you for the contribution!
Be mindful that this exposes the filename of the image. If it's guarded by a login, that's fine, though.
Also, do note that for agents detected as bots, this may result in unexpected behavior. Those agent's session will be closed quickly, but they will be served the link to the image.
When they try to load it, the application may detect that as a new session, and serve a new set-up page. Depending on how this is handled, the bot/crawler may encounter a loop.
We encountered something similar a while ago in an application of our own. A simple robots.txt
file solved it for us.
As for the DB substitution. I don't think that will immediately work. The substitution doesn't take place until rendering time. At that point both function resolving and eventual string substitution take place, so they would "collide", and I think the more deeply nested $
would be omitted.
You probably need to look at changing WTemplate::renderTemplateText().
My last claim is unverified, so I may be wrong!
Best,
Matthias
RE: Template Function Code review? - secure_img - Added by Christian Meyer 22 days ago
Hi
Thanks for the response!
I am not quite sure how the Filename is exposed?
At the time of rendering it will be replaced by an img tag with a WFileResource
that creates a temp path?
The ImageFile should also be in appRoot
folder, so even if the filename is exposed it should not be available for an easy grab.
Thank you for the note with the bots, I will keep that in mind.
In another template function I did use bindString for an argument and it seemed to just work.
did not yet had the case where it failed, but will keep my eyes peeled and update when I had the chance to test with db substitutions!
looking into it again I did do a string resolve on the template within the function like so
std::stringstream arg0stream;
t->resolveString(args[0].toUTF8(), std::vector<Wt::WString>(), arg0stream);
I will keep that in mind and update the function when I get to it =)
RE: Template Function Code review? - secure_img - Added by Christian Meyer 19 days ago
Version 2:
taking into account the potential trouble with bound Strings that I seemed to have ran into but probably forgot =D
Now the first argument is treated as source if no attribute src
is found.
If it starts with '/' it is treated as a path.
If it starts with '$' it is treated as a template-variable and the path will be extracted with resolveString()
same if attribute 'src' is used.
An Attribute Map is generated and used to setAlternateText
and addStyleClass
.
the rest is bound to the img with setAttribute
.
The Path for both templateBound and given must be absolute from appRoot
: the WFileResource
will look for appRoot() + <path>
As for the Robots problem:
I added a Custom FileResource that adds the header to the response: "X-Robots-Tag", "noindex, noimageindex"
.
I can not try if that would work, I would love some input on that!
Here is the new Version including a helper function to use in other Template Functions and the Custom Resource Class:
typedef std::map<std::string, std::string> IdentifierMap;
void fillIdentifierMap(const std::vector<Wt::WString>& args, IdentifierMap& identifier_map)
{
std::string vecString;
for (auto wS: args)
{
vecString = wS.toUTF8();
// Find the position of the '=' character in the string
size_t pos = vecString.find('=');
if (pos != std::string::npos)
{
// Extract the identifier and value part (before and after the '=')
std::string identifier = vecString.substr(0, pos);
std::string value = vecString.substr(pos+1);
// Insert the identifier and its value into the map
identifier_map[identifier] = value;
} else {
std::cerr << "Invalid format, '=' not found in: " << wS << std::endl;
}
}
}
class NoRobotsFileResource : public Wt::WFileResource
{
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override
{
response.addHeader("X-Robots-Tag", "noindex, noimageindex");
Wt::WFileResource::handleRequest(request, response);
}
};
bool secure_img(Wt::WTemplate *t,
const std::vector<Wt::WString>& args,
std::ostream& result)
{
if(args.size() < 1)
{
Wt::log("info") << "Template Function secure_img needs at least 1 argument";
return false;
}
IdentifierMap argMap;
fillIdentifierMap(args, argMap);
std::string src;
if(auto src_it = argMap.find("src"); src_it != argMap.end())
{
if(src_it->second.compare(0, 1, "/") == 0)
{
src = src_it->second;
}
else
if(src_it->second.compare(0, 1, "$") == 0)
{
src = t->resolveStringValue(src_it->second).toUTF8();
}
// erase item to later be able to loop over leftover attributes
argMap.erase(src_it);
}
else
{
// Check Argument Vector again for first Item
// fillIdentifierMap only fills with defined identifier=value pairs
if(std::string vecSrc = args[0].toUTF8(); vecSrc.compare(0, 1, "/") == 0)
{
src = vecSrc;
}
else
if(vecSrc.compare(0, 1, "$") == 0)
{
src = t->resolveStringValue(vecSrc).toUTF8();
}
else
{
Wt::log("info") << "Template Function secure_img needs to Start with Path (/) or template Variable ($),\n"
<< "or have identifier src in the arguments:\n";
return false;
}
}
auto imgFile = t->addChild(std::make_unique<NoRobotsFileResource>());
imgFile->setFileName(Wt::WApplication::instance()->appRoot() + src);
Wt::WImage wimg{};
wimg.setImageLink(imgFile->generateUrl());
if(auto alt_it = argMap.find("alt"); alt_it != argMap.end())
{
if(alt_it->second.compare(0, 1, "$") == 0)
{
if(alt_it->second.find(":") != std::string::npos)
{
// might be tr: function
Wt::log("debug") << "TemplateFunction" << "::secure_img: alt has additional Function?";
if(alt_it->second.compare(0, 5, "${tr:") == 0)
{
std::stringstream ssAlt;
if(t->resolveFunction("tr",{alt_it->second.substr(5, alt_it->second.size() -6)}, ssAlt))
wimg.setAlternateText(ssAlt.str());
}
}
else // it's just a bound string
wimg.setAlternateText(t->resolveStringValue(alt_it->second));
}
else // it's just text
wimg.setAlternateText(alt_it->second);
// erase item to later be able to loop over leftover attributes
argMap.erase(alt_it);
}
if(auto class_it = argMap.find("class"); class_it != argMap.end())
{
wimg.addStyleClass(class_it->second);
// erase item to later be able to loop over leftover attributes
argMap.erase(class_it);
}
// Add all left over Attributes
for(auto attr : argMap)
{
wimg.setAttributeValue(attr.first, attr.second);
}
wimg.htmlText(result);
return true;
}
And because I had another Idea while I was writing this up, I added the option to bindString
s to the Alt parameter as well. And because that might not be enough, enabled the tr
function for alt as well =)
So this is also possible:
${secure_img:alt="${tr:xml.var.identifier}" src="${templVar_src}"}
${secure_img:src="/path/to/approot/img.file" alt="${templVar_alt}"}
Good Luck and have fun!
use within the template is as before but as described with the optional setting of the src attribute
${secure_img:/path/to/appRoot/img.file} | ${secure_img:class="img-fluid" src="/path/to/appRoot/img.file"}
${secure_img:${tmplVar} class="sample-class" alt="Sample Text"} | ${secure_img:alt="sample" src="${templateVariable}"}