Created
May 26, 2018 22:00
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/**************************************************************************\ | |
|* *| | |
|* 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