312 lines
11 KiB
C++
312 lines
11 KiB
C++
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "NotificationCallback.h"
|
|
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "mozilla/CmdLineAndEnvUtils.h"
|
|
#include "mozilla/ToastNotificationHeaderOnlyUtils.h"
|
|
|
|
using namespace mozilla::widget::toastnotification;
|
|
|
|
HRESULT STDMETHODCALLTYPE
|
|
NotificationCallback::QueryInterface(REFIID riid, void** ppvObject) {
|
|
if (!ppvObject) {
|
|
return E_POINTER;
|
|
}
|
|
|
|
*ppvObject = nullptr;
|
|
|
|
if (!(riid == guid || riid == __uuidof(INotificationActivationCallback) ||
|
|
riid == __uuidof(IUnknown))) {
|
|
return E_NOINTERFACE;
|
|
}
|
|
|
|
AddRef();
|
|
*ppvObject = reinterpret_cast<void*>(this);
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT STDMETHODCALLTYPE NotificationCallback::Activate(
|
|
LPCWSTR appUserModelId, LPCWSTR invokedArgs,
|
|
const NOTIFICATION_USER_INPUT_DATA* data, ULONG dataCount) {
|
|
HandleActivation(invokedArgs);
|
|
|
|
// Windows 8 style callbacks are not called and notifications are not removed
|
|
// from the Action Center unless we return `S_OK`, so always do so even if
|
|
// we're unable to handle the notification properly.
|
|
return S_OK;
|
|
}
|
|
|
|
void NotificationCallback::HandleActivation(LPCWSTR invokedArgs) {
|
|
if (invokedArgs) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Info,
|
|
(L"Invoked with arguments: '%s'", invokedArgs));
|
|
}
|
|
auto maybeArgs = ParseToastArguments(invokedArgs);
|
|
if (maybeArgs.isSome() && maybeArgs.ref().skipNotificationServer) {
|
|
// Nothing to do for the Windows 8 style callbacks case.
|
|
return;
|
|
}
|
|
|
|
auto [programPath, cmdLine] = BuildRunCommand(maybeArgs);
|
|
|
|
// This pipe object will let Firefox notify us when it has handled the
|
|
// notification. Create this before interacting with the application so the
|
|
// application can rely on it existing.
|
|
auto maybePipe = maybeArgs.isSome() ? CreatePipe(maybeArgs.value().windowsTag)
|
|
: mozilla::Nothing();
|
|
|
|
// Run the application.
|
|
STARTUPINFOW si = {};
|
|
si.cb = sizeof(STARTUPINFOW);
|
|
PROCESS_INFORMATION pi = {};
|
|
|
|
// Runs `{program path} [--profile {profile path}] [--notification-windowsTag
|
|
// {tag}]`.
|
|
if (!CreateProcessW(programPath.c_str(), cmdLine.get(), nullptr, nullptr,
|
|
false, DETACHED_PROCESS | NORMAL_PRIORITY_CLASS, nullptr,
|
|
nullptr, &si, &pi)) {
|
|
MOZ_WIN_EVENT_LOG_ERROR_MESSAGE(MOZ_NOTIFICATION_SERVER_NAME,
|
|
L"NotificationCallback::HandleActivation: "
|
|
L"Failed to exec program. Error was %d",
|
|
GetLastError());
|
|
return;
|
|
} else {
|
|
NOTIFY_LOG(mozilla::LogLevel::Info, (L"Invoked %s", cmdLine.get()));
|
|
}
|
|
|
|
// Transfer `SetForegroundWindow` permission to the launched application.
|
|
|
|
maybePipe.apply([](const auto& pipe) {
|
|
if (ConnectPipeWithTimeout(pipe)) {
|
|
HandlePipeMessages(pipe);
|
|
}
|
|
});
|
|
// Close handles when we're done with them
|
|
if (pi.hThread) {
|
|
CloseHandle(pi.hThread);
|
|
}
|
|
if (pi.hProcess) {
|
|
CloseHandle(pi.hProcess);
|
|
}
|
|
}
|
|
|
|
mozilla::Maybe<ToastArgs> NotificationCallback::ParseToastArguments(
|
|
LPCWSTR invokedArgs) {
|
|
if (!invokedArgs) {
|
|
// Don't try to parse a null string.
|
|
MOZ_WIN_EVENT_LOG_INFO_MESSAGE(MOZ_NOTIFICATION_SERVER_NAME,
|
|
L"Was invoked with null args");
|
|
return mozilla::Nothing();
|
|
}
|
|
ToastArgs parsedArgs;
|
|
parsedArgs.skipNotificationServer = false;
|
|
|
|
std::wistringstream args(invokedArgs);
|
|
bool hasMozillaArgs = false; // Do the args look like they came from us?
|
|
|
|
for (std::wstring key, value;
|
|
std::getline(args, key) && std::getline(args, value);) {
|
|
if (key == kLaunchArgProgram) {
|
|
hasMozillaArgs = true;
|
|
} else if (key == kLaunchArgProfile) {
|
|
parsedArgs.profile = value;
|
|
} else if (key == kLaunchArgTag) {
|
|
parsedArgs.windowsTag = value;
|
|
} else if (key == kLaunchArgLogging) {
|
|
gVerbose = value == L"verbose";
|
|
} else if (key == kLaunchArgAction) {
|
|
parsedArgs.action = value;
|
|
} else if (key == kSkipNotificationKey) {
|
|
// This is a special case where it looks like the notification was created
|
|
// by a Mozilla app that has the pref
|
|
// "alerts.useSystemBackend.windows.notificationserver.enabled" set to
|
|
// false. In this case, we want this callback to exit successfully without
|
|
// launching the app. See ToastNotificationHandler::GetLaunchArgument
|
|
// comments for more info on this behavior.
|
|
parsedArgs.skipNotificationServer = true;
|
|
hasMozillaArgs = true;
|
|
}
|
|
}
|
|
|
|
if (!hasMozillaArgs) {
|
|
MOZ_WIN_EVENT_LOG_INFO_MESSAGE(MOZ_NOTIFICATION_SERVER_NAME,
|
|
L"Was invoked with args: %s", invokedArgs);
|
|
return mozilla::Nothing();
|
|
}
|
|
|
|
return mozilla::Some(parsedArgs);
|
|
}
|
|
|
|
std::tuple<path, mozilla::UniquePtr<wchar_t[]>>
|
|
NotificationCallback::BuildRunCommand(
|
|
const mozilla::Maybe<ToastArgs>& maybeArgs) {
|
|
path programPath = installDir / L"" MOZ_APP_NAME;
|
|
programPath += L".exe";
|
|
|
|
std::vector<const wchar_t*> childArgv;
|
|
childArgv.push_back(programPath.c_str());
|
|
|
|
if (maybeArgs.isSome()) {
|
|
const ToastArgs& args = maybeArgs.ref();
|
|
if (!args.profile.empty()) {
|
|
childArgv.push_back(L"--profile");
|
|
childArgv.push_back(args.profile.c_str());
|
|
} else {
|
|
NOTIFY_LOG(mozilla::LogLevel::Warning,
|
|
(L"No profile; invocation will choose default profile"));
|
|
}
|
|
|
|
if (!args.windowsTag.empty()) {
|
|
childArgv.push_back(L"--notification-windowsTag");
|
|
childArgv.push_back(args.windowsTag.c_str());
|
|
} else {
|
|
NOTIFY_LOG(mozilla::LogLevel::Warning,
|
|
(L"No windowsTag; invoking anyway"));
|
|
}
|
|
|
|
if (!args.action.empty()) {
|
|
childArgv.push_back(L"--notification-windowsAction");
|
|
childArgv.push_back(args.action.c_str());
|
|
} else {
|
|
NOTIFY_LOG(mozilla::LogLevel::Warning, (L"No action; invoking anyway"));
|
|
}
|
|
}
|
|
return {programPath,
|
|
mozilla::MakeCommandLine(childArgv.size(), childArgv.data())};
|
|
}
|
|
|
|
mozilla::Maybe<nsAutoHandle> NotificationCallback::CreatePipe(
|
|
const std::wstring& tag) {
|
|
if (tag.empty()) {
|
|
return mozilla::Nothing();
|
|
}
|
|
|
|
// Prefix required by pipe API.
|
|
std::wstring pipeName = GetNotificationPipeName(tag.c_str());
|
|
|
|
nsAutoHandle pipe(CreateNamedPipeW(
|
|
pipeName.c_str(), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
|
|
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT |
|
|
PIPE_REJECT_REMOTE_CLIENTS,
|
|
1, sizeof(ToastNotificationPermissionMessage),
|
|
sizeof(ToastNotificationPidMessage), 0, nullptr));
|
|
if (pipe.get() == INVALID_HANDLE_VALUE) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Error, (L"Error creating pipe %s, error %lu",
|
|
pipeName.c_str(), GetLastError()));
|
|
return mozilla::Nothing();
|
|
}
|
|
|
|
return mozilla::Some(pipe.out());
|
|
}
|
|
|
|
bool NotificationCallback::ConnectPipeWithTimeout(const nsAutoHandle& pipe) {
|
|
nsAutoHandle overlappedEvent(CreateEventW(nullptr, TRUE, FALSE, nullptr));
|
|
if (!overlappedEvent) {
|
|
NOTIFY_LOG(
|
|
mozilla::LogLevel::Error,
|
|
(L"Error creating pipe connect event, error %lu", GetLastError()));
|
|
return false;
|
|
}
|
|
|
|
OVERLAPPED overlappedConnect{};
|
|
overlappedConnect.hEvent = overlappedEvent.get();
|
|
|
|
BOOL result = ConnectNamedPipe(pipe.get(), &overlappedConnect);
|
|
DWORD lastError = GetLastError();
|
|
if (lastError == ERROR_IO_PENDING) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Info, (L"Waiting on pipe connection"));
|
|
|
|
if (!WaitEventWithTimeout(overlappedEvent)) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Warning,
|
|
(L"Pipe connect wait failed, cancelling (connection may still "
|
|
L"succeed)"));
|
|
|
|
CancelIo(pipe.get());
|
|
DWORD undefined;
|
|
BOOL overlappedResult =
|
|
GetOverlappedResult(pipe.get(), &overlappedConnect, &undefined, TRUE);
|
|
if (!overlappedResult || GetLastError() != ERROR_PIPE_CONNECTED) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Error,
|
|
(L"Pipe connect failed, error %lu", GetLastError()));
|
|
return false;
|
|
}
|
|
|
|
// Pipe connected before cancellation, fall through.
|
|
}
|
|
} else if (result) {
|
|
// Overlapped `ConnectNamedPipe` should return 0.
|
|
NOTIFY_LOG(mozilla::LogLevel::Error,
|
|
(L"Error connecting pipe, error %lu", lastError));
|
|
return false;
|
|
} else if (lastError != ERROR_PIPE_CONNECTED) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Error,
|
|
(L"Error connecting pipe, error %lu", lastError));
|
|
return false;
|
|
}
|
|
|
|
NOTIFY_LOG(mozilla::LogLevel::Info, (L"Pipe connected!"));
|
|
return true;
|
|
}
|
|
|
|
void NotificationCallback::HandlePipeMessages(const nsAutoHandle& pipe) {
|
|
ToastNotificationPidMessage in{};
|
|
auto read = [&](OVERLAPPED& overlapped) {
|
|
return ReadFile(pipe.get(), &in, sizeof(in), nullptr, &overlapped);
|
|
};
|
|
if (!SyncDoOverlappedIOWithTimeout(pipe, sizeof(in), read)) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Error, (L"Pipe read failed"));
|
|
return;
|
|
}
|
|
|
|
ToastNotificationPermissionMessage out{};
|
|
out.setForegroundPermissionGranted = TransferForegroundPermission(in.pid);
|
|
auto write = [&](OVERLAPPED& overlapped) {
|
|
return WriteFile(pipe.get(), &out, sizeof(out), nullptr, &overlapped);
|
|
};
|
|
if (!SyncDoOverlappedIOWithTimeout(pipe, sizeof(out), write)) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Error, (L"Pipe write failed"));
|
|
return;
|
|
}
|
|
|
|
NOTIFY_LOG(mozilla::LogLevel::Info, (L"Pipe write succeeded!"));
|
|
}
|
|
|
|
DWORD NotificationCallback::TransferForegroundPermission(DWORD pid) {
|
|
// When the instance of Firefox is still running we need to grant it
|
|
// foreground permission to bring itself to the foreground. We're able to do
|
|
// this even though the COM server is not the foreground process likely due to
|
|
// Windows granting permission to the COM object via
|
|
// `CoAllowSetForegroundWindow`.
|
|
//
|
|
// Note that issues surrounding `SetForegroundWindow` permissions are obscured
|
|
// when builds are run with a debugger, whereupon Windows grants
|
|
// `SetForegroundWindow` permission in all instances.
|
|
//
|
|
// We can not rely on granting this permission to the process created above
|
|
// because remote server clients do not meet the criteria to receive
|
|
// `SetForegroundWindow` permissions without unsupported hacks.
|
|
if (!pid) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Warning,
|
|
(L"`pid` received from pipe was 0, no process to grant "
|
|
L"`SetForegroundWindow` permission to"));
|
|
return FALSE;
|
|
}
|
|
// When this call succeeds, the COM process loses the `SetForegroundWindow`
|
|
// permission.
|
|
if (!AllowSetForegroundWindow(pid)) {
|
|
NOTIFY_LOG(mozilla::LogLevel::Error,
|
|
(L"Failed to grant `SetForegroundWindow` permission, error %lu",
|
|
GetLastError()));
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|