/*
* This source file is part of RmlUi, the HTML/CSS Interface Middleware
*
* For the latest information, see http://github.com/mikke89/RmlUi
*
* Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
* Copyright (c) 2019-2023 The RmlUi Team, and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#include "../../Include/RmlUi/Core/URL.h"
#include "../../Include/RmlUi/Core/Log.h"
#include "../../Include/RmlUi/Core/StringUtilities.h"
#include
#include
namespace Rml {
const char* DEFAULT_PROTOCOL = "file";
URL::URL()
{
port = 0;
url_dirty = false;
}
URL::URL(const String& _url)
{
port = 0;
RMLUI_VERIFY(SetURL(_url));
}
URL::URL(const char* _url)
{
port = 0;
RMLUI_VERIFY(SetURL(_url));
}
URL::~URL() {}
bool URL::SetURL(const String& _url)
{
url_dirty = false;
url = _url;
// Make sure an Empty URL is completely Empty.
if (url.empty())
{
protocol.clear();
login.clear();
password.clear();
host.clear();
port = 0;
path.clear();
file_name.clear();
extension.clear();
return true;
}
// Find the protocol. This consists of the string appearing before the
// '://' token (ie, file://, http://).
const char* host_begin = strchr(_url.c_str(), ':');
if (nullptr != host_begin)
{
protocol = String(_url.c_str(), host_begin);
if (0 != strncmp(host_begin, "://", 3))
{
char malformed_terminator[4] = {0, 0, 0, 0};
strncpy(malformed_terminator, host_begin, 3);
Log::Message(Log::LT_ERROR, "Malformed protocol identifier found in URL %s; expected %s://, found %s%s.\n", _url.c_str(),
protocol.c_str(), protocol.c_str(), malformed_terminator);
return false;
}
host_begin += 3;
}
else
{
protocol = DEFAULT_PROTOCOL;
host_begin = _url.c_str();
}
// We only want to look for a host if a protocol was specified.
const char* path_begin;
if (host_begin != _url.c_str())
{
// Find the host. This is the string appearing after the protocol or after
// the username:password combination, and terminated either with a colon,
// if a port is specified, or a forward slash if there is no port.
// Check for a login pair
const char* at_symbol = strchr(host_begin, '@');
if (at_symbol)
{
String login_password;
login_password = String(host_begin, at_symbol);
host_begin = at_symbol + 1;
const char* password_ptr = strchr(login_password.c_str(), ':');
if (password_ptr)
{
login = String(login_password.c_str(), password_ptr);
password = String(password_ptr + 1);
}
else
{
login = login_password;
}
}
// Get the host portion
path_begin = strchr(host_begin, '/');
// Search for the colon in the host name, which will indicate a port.
const char* port_begin = strchr(host_begin, ':');
if (nullptr != port_begin && (nullptr == path_begin || port_begin < path_begin))
{
if (1 != sscanf(port_begin, ":%d", &port))
{
Log::Message(Log::LT_ERROR, "Malformed port number found in URL %s.\n", _url.c_str());
return false;
}
host = String(host_begin, port_begin);
// Don't continue if there is no path.
if (nullptr == path_begin)
{
return true;
}
// Increment the path string past the trailing slash.
++path_begin;
}
else
{
port = -1;
if (nullptr == path_begin)
{
host = host_begin;
return true;
}
else
{
// Assign the host name, then increment the path string past the
// trailing slash.
host = String(host_begin, path_begin);
++path_begin;
}
}
}
else
{
path_begin = _url.c_str();
}
// Check for parameters
String path_segment;
const char* parameters = strchr(path_begin, '?');
if (parameters)
{
// Pull the path segment out, so further processing doesn't read the parameters
path_segment = String(path_begin, parameters);
path_begin = path_segment.c_str();
// Loop through all parameters, loading them
StringList parameter_list;
StringUtilities::ExpandString(parameter_list, parameters + 1, '&');
for (size_t i = 0; i < parameter_list.size(); i++)
{
// Split into key and value
StringList key_value;
StringUtilities::ExpandString(key_value, parameter_list[i], '=');
key_value[0] = UrlDecode(key_value[0]);
if (key_value.size() == 2)
this->parameters[key_value[0]] = UrlDecode(key_value[1]);
else
this->parameters[key_value[0]] = "";
}
}
// Find the path. This is the string appearing after the host, terminated
// by the last forward slash.
const char* file_name_begin = strrchr(path_begin, '/');
if (nullptr == file_name_begin)
{
// No path!
file_name_begin = path_begin;
path = "";
}
else
{
// Copy the path including the trailing slash.
path = String(path_begin, ++file_name_begin);
// Normalise the path, stripping any ../'s from it
size_t parent_dir_pos = String::npos;
while ((parent_dir_pos = path.find("/../")) != String::npos && parent_dir_pos != 0)
{
// Find the start of the parent directory.
size_t parent_dir_start_pos = path.rfind('/', parent_dir_pos - 1);
if (parent_dir_start_pos == String::npos)
parent_dir_start_pos = 0;
else
parent_dir_start_pos += 1;
// Strip out the parent dir and the /..
path.erase(parent_dir_start_pos, parent_dir_pos - parent_dir_start_pos + 4);
// We've altered the URL, mark it dirty
url_dirty = true;
}
}
// Find the file name. This is the string after the trailing slash of the
// path, and just before the extension.
const char* extension_begin = strrchr(file_name_begin, '.');
if (nullptr == extension_begin)
{
file_name = file_name_begin;
extension = "";
}
else
{
file_name = String(file_name_begin, extension_begin);
extension = extension_begin + 1;
}
return true;
}
const String& URL::GetURL() const
{
if (url_dirty)
ConstructURL();
return url;
}
bool URL::SetProtocol(const String& _protocol)
{
protocol = _protocol;
url_dirty = true;
return true;
}
const String& URL::GetProtocol() const
{
return protocol;
}
bool URL::SetLogin(const String& _login)
{
login = _login;
url_dirty = true;
return true;
}
const String& URL::GetLogin() const
{
return login;
}
bool URL::SetPassword(const String& _password)
{
password = _password;
url_dirty = true;
return true;
}
const String& URL::GetPassword() const
{
return password;
}
bool URL::SetHost(const String& _host)
{
host = _host;
url_dirty = true;
return true;
}
const String& URL::GetHost() const
{
return host;
}
bool URL::SetPort(int _port)
{
port = _port;
url_dirty = true;
return true;
}
int URL::GetPort() const
{
return port;
}
bool URL::SetPath(const String& _path)
{
path = _path;
url_dirty = true;
return true;
}
bool URL::PrefixPath(const String& prefix)
{
// If there's no trailing slash on the end of the prefix, add one.
if (!prefix.empty() && prefix[prefix.size() - 1] != '/')
path = prefix + "/" + path;
else
path = prefix + path;
url_dirty = true;
return true;
}
const String& URL::GetPath() const
{
return path;
}
bool URL::SetFileName(const String& _file_name)
{
file_name = _file_name;
url_dirty = true;
return true;
}
const String& URL::GetFileName() const
{
return file_name;
}
bool URL::SetExtension(const String& _extension)
{
extension = _extension;
url_dirty = true;
return true;
}
const String& URL::GetExtension() const
{
return extension;
}
const URL::Parameters& URL::GetParameters() const
{
return parameters;
}
void URL::SetParameter(const String& key, const String& value)
{
parameters[key] = value;
url_dirty = true;
}
void URL::SetParameters(const Parameters& _parameters)
{
parameters = _parameters;
url_dirty = true;
}
void URL::ClearParameters()
{
parameters.clear();
}
String URL::GetPathedFileName() const
{
String pathed_file_name = path;
// Append the file name.
pathed_file_name += file_name;
// Append the extension.
if (!extension.empty())
{
pathed_file_name += ".";
pathed_file_name += extension;
}
return pathed_file_name;
}
String URL::GetQueryString() const
{
String query_string;
int count = 0;
for (Parameters::const_iterator itr = parameters.begin(); itr != parameters.end(); ++itr)
{
query_string += (count == 0) ? "" : "&";
query_string += UrlEncode((*itr).first);
query_string += "=";
query_string += UrlEncode((*itr).second);
count++;
}
return query_string;
}
bool URL::operator<(const URL& rhs) const
{
if (url_dirty)
ConstructURL();
if (rhs.url_dirty)
rhs.ConstructURL();
return url < rhs.url;
}
void URL::ConstructURL() const
{
url = "";
// Append the protocol.
if (!protocol.empty() && !host.empty())
{
url = protocol;
url += "://";
}
// Append login and password
if (!login.empty())
{
url += login;
if (!password.empty())
{
url += ":";
url += password;
}
url += "@";
}
RMLUI_ASSERTMSG(password.empty() || (!password.empty() && !login.empty()), "Can't have a password without a login!");
// Append the host.
url += host;
// Only check ports if there is some host/protocol part
if (!url.empty())
{
if (port > 0)
{
RMLUI_ASSERTMSG(!host.empty(), "Can't have a port without a host!");
constexpr size_t port_buffer_size = 16;
char port_string[port_buffer_size];
snprintf(port_string, port_buffer_size, ":%d/", port);
url += port_string;
}
else
{
url += "/";
}
}
// Append the path.
if (!path.empty())
{
url += path;
}
// Append the file name.
url += file_name;
// Append the extension.
if (!extension.empty())
{
url += ".";
url += extension;
}
// Append parameters
if (!parameters.empty())
{
url += "?";
url += GetQueryString();
}
url_dirty = false;
}
String URL::UrlEncode(const String& value)
{
String encoded;
constexpr size_t hex_buffer_size = 4;
char hex[hex_buffer_size] = {0, 0, 0, 0};
encoded.clear();
const char* value_c = value.c_str();
for (String::size_type i = 0; value_c[i]; i++)
{
char c = value_c[i];
if (IsUnreservedChar(c))
encoded += c;
else
{
snprintf(hex, hex_buffer_size, "%%%02X", c);
encoded += hex;
}
}
return encoded;
}
String URL::UrlDecode(const String& value)
{
String decoded;
decoded.clear();
const char* value_c = value.c_str();
String::size_type value_len = value.size();
for (String::size_type i = 0; i < value_len; i++)
{
char c = value_c[i];
if (c == '+')
{
decoded += ' ';
}
else if (c == '%')
{
char* endp;
String t = value.substr(i + 1, 2);
int ch = strtol(t.c_str(), &endp, 16);
if (*endp == '\0')
decoded += char(ch);
else
decoded += t;
i += 2;
}
else
{
decoded += c;
}
}
return decoded;
}
bool URL::IsUnreservedChar(const char in)
{
switch (in)
{
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
case 'g':
case 'h':
case 'i':
case 'j':
case 'k':
case 'l':
case 'm':
case 'n':
case 'o':
case 'p':
case 'q':
case 'r':
case 's':
case 't':
case 'u':
case 'v':
case 'w':
case 'x':
case 'y':
case 'z':
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
case 'G':
case 'H':
case 'I':
case 'J':
case 'K':
case 'L':
case 'M':
case 'N':
case 'O':
case 'P':
case 'Q':
case 'R':
case 'S':
case 'T':
case 'U':
case 'V':
case 'W':
case 'X':
case 'Y':
case 'Z':
case '-':
case '.':
case '_':
case '~': return true;
default: break;
}
return false;
}
} // namespace Rml