Skip to content

Instantly share code, notes, and snippets.

@GeeLaw
Created May 26, 2018 22:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GeeLaw/782d1ff3cf4f990c61ba1cc20ddf4a60 to your computer and use it in GitHub Desktop.
Save GeeLaw/782d1ff3cf4f990c61ba1cc20ddf4a60 to your computer and use it in GitHub Desktop.
URI protocol handler that chooses a class-verb combination based on the URI
/**************************************************************************\
|* *|
|* The MIT License (MIT) *|
|* *|
|* Copyright © 2018 by Gee Law *|
|* *|
|* Permission is hereby granted, free of charge, to any person obtaining *|
|* a copy of this software and associated documentation files (the “Soft- *|
|* ware”), 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 includ- *|
|* ed 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. *|
|* *|
|**************************************************************************|
|* Compile with the following command: *|
|* cl /EHsc handler.cc *|
|* /link /SUBSYSTEM:WINDOWS shell32.lib ole32.lib user32.lib *|
|* *|
|* Register this application as HTTP and HTTPS protocol handler, and *|
|* set the command for "open" verb to be the following: *|
|* handler ? %1 *|
|* *|
|* Edit your preference at %USERPROFILE%\web-browser-handler.config, *|
|* which will be populated with default values the first time you *|
|* launch this application. Alternatively, launch the application *|
|* without any command-line parameters to open the configuration *|
|* file. It is recommended that you use a stable way to produce the *|
|* replacement URL and class name. *|
|* *|
|* - Internet Explorer uses IE.HTTP and IE.HTTPS as the class name, and *|
|* accepts the raw URL. *|
|* - Microsoft Edge uses microsoft-edge as the class name, and accepts *|
|* microsoft-edge:<URL> as the URL. Note that although you might be *|
|* able to launch https://geelaw.blog/ as microsoft-edge protocol, *|
|* and that Edge might open correctly, this behaviour is not guaranteed *|
|* to persist in the future. *|
|* - Per-user installed Chrome use ChromeHTML.<slug> as the class name, *|
|* and I do not know the case for machine-wide installed Chrome. *|
|* *|
\**************************************************************************/
#define UNICODE
#include<windows.h>
#include<winuser.h>
#include<objbase.h>
#include<shlobj.h>
#include<shellapi.h>
#include<cstdio>
#include<string>
#include<regex>
#include<utility>
#define BUFFER_SIZE (MAX_PATH + 128)
wchar_t const * const configFileName = L"\\web-browser-handler.config";
wchar_t configFilePath[BUFFER_SIZE];
bool GetConfigFilePath()
{
auto gspf = ::SHGetSpecialFolderPath(nullptr, configFilePath, CSIDL_PROFILE, TRUE);
if (gspf == FALSE)
return false;
wchar_t *i = configFilePath;
for (i = configFilePath; *i; ++i)
;
wchar_t const *j = configFileName;
if (i[-1] == L'\\')
++j;
for (; *j; *(i++) = *(j++))
;
*i = L'\0';
return true;
}
bool CreateConfigFileOnDemand()
{
FILE *f = _wfopen(configFilePath, L"r");
if (f)
{
fclose(f);
return true;
}
f = _wfopen(configFilePath, L"w");
if (!f)
return false;
fputws(
L"# Syntax:\n"
L"# Lines that start with # (without leading space) are comments\n"
L"# Regular expression to match URI from its beginning\n"
L"# Replacement string\n"
L"# %% for a percent sign\n"
L"# %u for original URL\n"
L"# %q for quoted URL\n"
L"# Class\n"
L"# Verb\n\n"
L"http:.*\n"
L"microsoft-edge:%u\n"
L"microsoft-edge\n"
L"open\n\n"
L"https:.*\n"
L"microsoft-edge:%u\n"
L"microsoft-edge\n"
L"open\n\n"
L"# http:.*\n"
L"# %u\n"
L"# IE.HTTP\n"
L"# open\n\n"
L"# https:.*\n"
L"# %u\n"
L"# IE.HTTPS\n"
L"# open\n\n",
f);
fclose(f);
return true;
}
void OpenConfigFile(int nCmdShow)
{
ShellExecute(nullptr, nullptr, configFilePath, nullptr, nullptr, nCmdShow);
}
struct ConfigIterator
{
FILE *fp;
ConfigIterator()
{
fp = _wfopen(configFilePath, L"r");
}
~ConfigIterator()
{
if (fp)
{
fclose(fp);
}
}
bool GetNextPattern(std::wstring &pattern,
std::wstring &replacement,
std::wstring &className,
std::wstring &verb)
{
std::wstring *target[] = { &pattern, &replacement, &className, &verb, nullptr };
for (int i = 0; target[i]; ++i)
target[i]->clear();
int which = 0;
enum { Initial, WaitingNewLine, Reading } status = Initial;
wint_t gwc;
while (target[which] && (gwc = _fgetwc_nolock(fp)) != WEOF)
{
switch (status)
{
case Initial:
if (gwc == L'\r' || gwc == L'\n')
{
break;
}
if (gwc == '#')
{
status = WaitingNewLine;
}
else
{
status = Reading;
target[which]->push_back(gwc);
}
break;
case WaitingNewLine:
if (gwc == L'\r' || gwc == L'\n')
{
status = Initial;
}
break;
case Reading:
if (gwc == L'\r' || gwc == L'\n')
{
status = Initial;
++which;
}
else
{
target[which]->push_back(gwc);
}
break;
}
}
return target[which] ? false : true;
}
};
struct ComInit
{
bool wasSuccessful;
ComInit()
{
wasSuccessful = (CoInitializeEx(nullptr,
COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) == S_OK);
}
~ComInit()
{
if (wasSuccessful)
{
CoUninitialize();
}
}
};
std::wstring ReplaceString(wchar_t const *replacement, wchar_t const *url)
{
std::wstring replaced, quoted;
bool createdQuoted = false;
enum { Normal, Escaping } status = Normal;
for (; *replacement; ++replacement)
{
if (status == Normal)
{
if (*replacement == L'%')
{
status = Escaping;
}
else
{
replaced.push_back(*replacement);
}
}
else
{
if (*replacement == L'u')
{
replaced += url;
}
else if (*replacement == L'q')
{
if (!createdQuoted)
{
createdQuoted = true;
quoted.push_back('"');
for (auto i = url; *i; ++i)
{
if (*i == L'"' || *i == L'\\')
quoted.push_back(L'\\');
quoted.push_back(*i);
}
quoted.push_back('"');
}
replaced += quoted;
}
else if (*replacement == L'%')
{
replaced.push_back('%');
}
else
{
replaced.push_back(L'%');
replaced.push_back(*replacement);
}
status = Normal;
}
}
if (status == Escaping)
{
replaced.push_back(L'%');
}
return std::move(replaced);
}
int MyMain(wchar_t const *cmdline, int nCmdShow);
/* Scheme:
* executable ? URL-to-handle
*/
int CALLBACK wWinMain(HINSTANCE hInst, HINSTANCE hPrev,
wchar_t *cmdline, int nCmdShow)
{
return MyMain(cmdline, nCmdShow);
}
wchar_t const *GetUrlFromCmdline(wchar_t const *cmdline)
{
auto url = cmdline;
for (; *url && *url != L'?'; ++url)
;
if (*(url++) == L'?')
return *url == L' ' ? url + 1 : url;
return nullptr;
}
int MyMain(wchar_t const *cmdline, int nCmdShow)
{
std::wstring msgbox = L"Error opening URL: ";
wchar_t const *url = GetUrlFromCmdline(cmdline);
if (url)
{
msgbox += url;
}
else
{
msgbox += cmdline;
}
msgbox += L"\r\n";
ComInit comInit;
auto msgboxStyle = MB_ICONERROR | MB_OK | MB_SETFOREGROUND;
if (!comInit.wasSuccessful)
{
msgbox += L"Could not initialize Component Object Model.";
MessageBox(nullptr, msgbox.c_str(), L"Error opening URL", msgboxStyle);
return -1;
}
if (!GetConfigFilePath())
{
msgbox += L"Could not get configuration file path %USERPROFILE%\\web-browser-handler.config";
MessageBox(nullptr, msgbox.c_str(), L"Error opening URL", msgboxStyle);
return -1;
}
if (!CreateConfigFileOnDemand())
{
msgbox += L"Could not access configuration file: ";
msgbox += configFilePath;
MessageBox(nullptr, msgbox.c_str(), L"Error opening URL", msgboxStyle);
return -1;
}
if (!url)
{
OpenConfigFile(nCmdShow);
return -1;
}
ConfigIterator it;
std::wstring pattern, replacement, className, verb;
while (it.GetNextPattern(pattern, replacement, className, verb))
{
if (std::regex_match(url, (std::wregex)pattern,
std::regex_constants::match_any | std::regex_constants::match_continuous))
{
std::wstring replaced = ReplaceString(replacement.c_str(), url);
SHELLEXECUTEINFO sei = { };
sei.cbSize = sizeof sei;
sei.fMask = SEE_MASK_CLASSNAME | SEE_MASK_NOASYNC;
sei.lpVerb = verb.c_str();
sei.lpFile = replaced.c_str();
sei.nShow = nCmdShow;
sei.lpClass = className.c_str();
auto see = ShellExecuteEx(&sei);
if (see == FALSE)
{
msgbox += L"ShellExecuteEx failed.";
MessageBox(nullptr, msgbox.c_str(), L"Error opening URL", msgboxStyle);
return 1;
}
return 0;
}
}
msgbox += L"Did not find a matching handler.";
MessageBox(nullptr, msgbox.c_str(), L"Error opening URL", msgboxStyle);
return 2;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment