diff --git a/installer/MSIX/appxmanifest.xml b/installer/MSIX/appxmanifest.xml index 7dc1e1be9e..a57983a327 100644 --- a/installer/MSIX/appxmanifest.xml +++ b/installer/MSIX/appxmanifest.xml @@ -31,6 +31,12 @@ + + + images\logo.png + Powertoys custom protocol + + diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 5c1c4c82e8..5f090eda69 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -236,8 +236,20 @@ + + + + + + + + + + + + diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index d0506c5825..eecc209872 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -29,6 +29,9 @@ common + + + StaticLibrary true @@ -164,7 +167,16 @@ + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + \ No newline at end of file diff --git a/src/common/notifications.cpp b/src/common/notifications.cpp index cf500bd7cc..e7051e423c 100644 --- a/src/common/notifications.cpp +++ b/src/common/notifications.cpp @@ -145,10 +145,10 @@ void notifications::register_background_toast_handler() } } -void notifications::show_toast(std::wstring_view message) +void notifications::show_toast(std::wstring message, toast_params params) { // The toast won't be actually activated in the background, since it doesn't have any buttons - show_toast_with_activations(message, {}, {}); + show_toast_with_activations(std::move(message), {}, {}, std::move(params)); } inline void xml_escape(std::wstring data) @@ -182,13 +182,13 @@ inline void xml_escape(std::wstring data) data.swap(buffer); } -void notifications::show_toast_with_activations(std::wstring_view message, std::wstring_view background_handler_id, std::vector buttons) +void notifications::show_toast_with_activations(std::wstring message, std::wstring_view background_handler_id, std::vector actions, toast_params params) { // DO NOT LOCALIZE any string in this function, because they're XML tags and a subject to // https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/toast-xml-schema std::wstring toast_xml; - toast_xml.reserve(1024); + toast_xml.reserve(2048); std::wstring title{ L"PowerToys" }; if (winstore::running_as_packaged()) { @@ -200,28 +200,78 @@ void notifications::show_toast_with_activations(std::wstring_view message, std:: toast_xml += L""; toast_xml += message; toast_xml += L""; + for (size_t i = 0; i < size(actions); ++i) + { + std::visit(overloaded{ + [&](const snooze_button& b) { + const bool has_durations = !b.durations.empty() && size(b.durations) <= 5; + std::wstring selection_id = L"snoozeTime"; + selection_id += static_cast(L'0' + i); + if (has_durations) + { + toast_xml += LR"()"; + for (const auto& duration : b.durations) + { + toast_xml += LR"()"; + } + toast_xml += LR"()"; + } + }, + [](const auto&) {} }, + actions[i]); + } - for (size_t i = 0; i < size(buttons); ++i) + for (size_t i = 0; i < size(actions); ++i) { std::visit(overloaded{ [&](const link_button& b) { - toast_xml += LR"()"; + toast_xml += LR"(" />)"; }, [&](const background_activated_button& b) { - toast_xml += LR"()"; + toast_xml += LR"(" />)"; }, - }, - buttons[i]); + [&](const snooze_button& b) { + const bool has_durations = !b.durations.empty() && size(b.durations) <= 5; + std::wstring selection_id = L"snoozeTime"; + selection_id += static_cast(L'0' + i); + toast_xml += LR"()"; + } }, + actions[i]); } toast_xml += L""; @@ -232,5 +282,22 @@ void notifications::show_toast_with_activations(std::wstring_view message, std:: const auto notifier = winstore::running_as_packaged() ? ToastNotificationManager::ToastNotificationManager::CreateToastNotifier() : ToastNotificationManager::ToastNotificationManager::CreateToastNotifier(WIN32_AUMID); + + // Set a tag-related params if it has a valid length + if (params.tag.has_value() && params.tag->length() < 64) + { + notification.Tag(*params.tag); + if (!params.resend_if_scheduled) + { + for (const auto& scheduled_toast : notifier.GetScheduledToastNotifications()) + { + if (scheduled_toast.Tag() == *params.tag) + { + return; + } + } + } + } + notifier.Show(notification); } diff --git a/src/common/notifications.h b/src/common/notifications.h index c4c8e9ebce..65be031ed3 100644 --- a/src/common/notifications.h +++ b/src/common/notifications.h @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include +#include namespace notifications { @@ -12,19 +14,38 @@ namespace notifications void run_desktop_app_activator_loop(); + struct snooze_duration + { + std::wstring label; + int minutes; + }; + + struct snooze_button + { + std::vector durations; + }; + struct link_button { - std::wstring_view label; - std::wstring_view url; + std::wstring label; + std::wstring url; + bool context_menu = false; }; struct background_activated_button { - std::wstring_view label; + std::wstring label; + bool context_menu = false; }; - using button_t = std::variant; + struct toast_params + { + std::optional tag; + bool resend_if_scheduled = true; + }; - void show_toast(std::wstring_view plaintext_message); - void show_toast_with_activations(std::wstring_view plaintext_message, std::wstring_view background_handler_id, std::vector buttons); + using action_t = std::variant; + + void show_toast(std::wstring plaintext_message, toast_params params = {}); + void show_toast_with_activations(std::wstring plaintext_message, std::wstring_view background_handler_id, std::vector actions, toast_params params = {}); } diff --git a/src/common/notifications/fancyzones_notifications.h b/src/common/notifications/fancyzones_notifications.h new file mode 100644 index 0000000000..0e79e15427 --- /dev/null +++ b/src/common/notifications/fancyzones_notifications.h @@ -0,0 +1,71 @@ +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include + +#include "../timeutil.h" +namespace +{ + const inline wchar_t CANT_DRAG_ELEVATED_DONT_SHOW_AGAIN_REGISTRY_PATH[] = LR"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\DontShowMeThisDialogAgain\{e16ea82f-6d94-4f30-bb02-d6d911588afd})"; + const inline int64_t disable_interval_in_days = 30; +} + +inline bool disable_cant_drag_elevated_warning() +{ + HKEY key{}; + if (RegCreateKeyExW(HKEY_CURRENT_USER, + CANT_DRAG_ELEVATED_DONT_SHOW_AGAIN_REGISTRY_PATH, + 0, + nullptr, + REG_OPTION_NON_VOLATILE, + KEY_ALL_ACCESS, + nullptr, + &key, + nullptr) != ERROR_SUCCESS) + { + return false; + } + const auto now = timeutil::now(); + const size_t buf_size = sizeof(now); + if (RegSetValueExW(key, nullptr, 0, REG_QWORD, reinterpret_cast(&now), sizeof(now)) != ERROR_SUCCESS) + { + RegCloseKey(key); + return false; + } + RegCloseKey(key); + return true; +} + +inline bool is_cant_drag_elevated_warning_disabled() +{ + HKEY key{}; + if (RegOpenKeyExW(HKEY_CURRENT_USER, + CANT_DRAG_ELEVATED_DONT_SHOW_AGAIN_REGISTRY_PATH, + 0, + KEY_READ, + &key) != ERROR_SUCCESS) + { + return false; + } + std::wstring buffer(std::numeric_limits::digits10 + 2, L'\0'); + time_t last_disabled_time{}; + DWORD time_size = static_cast(sizeof(last_disabled_time)); + if (RegGetValueW( + key, + nullptr, + nullptr, + RRF_RT_REG_QWORD, + nullptr, + &last_disabled_time, + &time_size) != ERROR_SUCCESS) + { + RegCloseKey(key); + return false; + } + RegCloseKey(key); + return timeutil::diff::in_days(timeutil::now(), last_disabled_time) < disable_interval_in_days; + return false; +} \ No newline at end of file diff --git a/src/common/packages.config b/src/common/packages.config new file mode 100644 index 0000000000..09cb116327 --- /dev/null +++ b/src/common/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/common/window_helpers.cpp b/src/common/window_helpers.cpp index 35b935072d..925e6f48f7 100644 --- a/src/common/window_helpers.cpp +++ b/src/common/window_helpers.cpp @@ -1,5 +1,7 @@ #include "window_helpers.h" #include "pch.h" +#include + HWND CreateMsgWindow(_In_ HINSTANCE hInst, _In_ WNDPROC pfnWndProc, _In_ void* p) { @@ -27,4 +29,32 @@ HWND CreateMsgWindow(_In_ HINSTANCE hInst, _In_ WNDPROC pfnWndProc, _In_ void* p } return hwnd; -} \ No newline at end of file +} + +bool IsProcessOfWindowElevated(HWND window) +{ + DWORD pid = 0; + GetWindowThreadProcessId(window, &pid); + if (!pid) + { + return false; + } + + wil::unique_handle hProcess{ OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, + FALSE, + pid) }; + + wil::unique_handle token; + bool elevated = false; + + if (OpenProcessToken(hProcess.get(), TOKEN_QUERY, &token)) + { + TOKEN_ELEVATION elevation; + DWORD size; + if (GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size)) + { + return elevation.TokenIsElevated != 0; + } + } + return false; +} diff --git a/src/common/window_helpers.h b/src/common/window_helpers.h index 67167f116c..2494b8125c 100644 --- a/src/common/window_helpers.h +++ b/src/common/window_helpers.h @@ -1,4 +1,7 @@ #pragma once #include "common.h" -HWND CreateMsgWindow(_In_ HINSTANCE hInst, _In_ WNDPROC pfnWndProc, _In_ void* p); \ No newline at end of file +HWND CreateMsgWindow(_In_ HINSTANCE hInst, _In_ WNDPROC pfnWndProc, _In_ void* p); + +// If HWND is already dead, we assume it wasn't elevated +bool IsProcessOfWindowElevated(HWND window); \ No newline at end of file diff --git a/src/modules/fancyzones/lib/FancyZones.cpp b/src/modules/fancyzones/lib/FancyZones.cpp index f65acf979c..4d93d85f66 100644 --- a/src/modules/fancyzones/lib/FancyZones.cpp +++ b/src/modules/fancyzones/lib/FancyZones.cpp @@ -12,9 +12,13 @@ #include #include -#include +#include +#include +#include #include +#include + enum class DisplayChangeType { WorkArea, @@ -24,6 +28,8 @@ enum class DisplayChangeType Initialization }; +extern "C" IMAGE_DOS_HEADER __ImageBase; + namespace std { template<> @@ -176,7 +182,7 @@ private: bool IsInterestingWindow(HWND window) noexcept; void UpdateZoneWindows() noexcept; void MoveWindowsOnDisplayChange() noexcept; - void UpdateDragState(require_write_lock) noexcept; + void UpdateDragState(HWND window, require_write_lock) noexcept; void CycleActiveZoneSet(DWORD vkCode) noexcept; bool OnSnapHotkey(DWORD vkCode) noexcept; void MoveSizeStartInternal(HWND window, HMONITOR monitor, POINT const& ptScreen, require_write_lock) noexcept; @@ -807,7 +813,7 @@ void FancyZones::MoveWindowsOnDisplayChange() noexcept EnumWindows(callback, reinterpret_cast(this)); } -void FancyZones::UpdateDragState(require_write_lock) noexcept +void FancyZones::UpdateDragState(HWND window, require_write_lock) noexcept { const bool shift = GetAsyncKeyState(VK_SHIFT) & 0x8000; const bool mouseL = GetAsyncKeyState(VK_LBUTTON) & 0x8000; @@ -836,6 +842,23 @@ void FancyZones::UpdateDragState(require_write_lock) noexcept { m_dragEnabled = !(shift | mouse); } + + const bool windowElevated = IsProcessOfWindowElevated(window); + static const bool meElevated = is_process_elevated(); + static bool warning_shown = false; + if (windowElevated && !meElevated) + { + m_dragEnabled = false; + if (!warning_shown && !is_cant_drag_elevated_warning_disabled()) + { + std::vector actions = { + notifications::link_button{ GET_RESOURCE_STRING(IDS_CANT_DRAG_ELEVATED_LEARN_MORE), L"https://aka.ms/powertoysDetectedElevatedHelp" }, + notifications::link_button{ GET_RESOURCE_STRING(IDS_CANT_DRAG_ELEVATED_DIALOG_DONT_SHOW_AGAIN), L"powertoys://cant_drag_elevated_disable/" } + }; + notifications::show_toast_with_activations(GET_RESOURCE_STRING(IDS_CANT_DRAG_ELEVATED), {}, std::move(actions)); + warning_shown = true; + } + } } void FancyZones::CycleActiveZoneSet(DWORD vkCode) noexcept @@ -936,7 +959,7 @@ void FancyZones::MoveSizeStartInternal(HWND window, HMONITOR monitor, POINT cons m_windowMoveSize = window; // This updates m_dragEnabled depending on if the shift key is being held down. - UpdateDragState(writeLock); + UpdateDragState(window, writeLock); if (m_dragEnabled) { @@ -1019,7 +1042,7 @@ void FancyZones::MoveSizeUpdateInternal(HMONITOR monitor, POINT const& ptScreen, if (m_inMoveSize) { // This updates m_dragEnabled depending on if the shift key is being held down. - UpdateDragState(writeLock); + UpdateDragState(m_windowMoveSize, writeLock); if (m_zoneWindowMoveSize) { @@ -1226,4 +1249,4 @@ winrt::com_ptr MakeFancyZones(HINSTANCE hinstance, const winrt::com } return winrt::make_self(hinstance, settings); -} \ No newline at end of file +} diff --git a/src/modules/fancyzones/lib/fancyzones.rc b/src/modules/fancyzones/lib/fancyzones.rc index 961ad67597..5a0bb1bb13 100644 --- a/src/modules/fancyzones/lib/fancyzones.rc +++ b/src/modules/fancyzones/lib/fancyzones.rc @@ -26,6 +26,10 @@ BEGIN IDS_SETTING_EXCLCUDED_APPS_DESCRIPTION "To exclude an application from snapping to zones add its name here (one per line). Excluded apps will react to the Windows Snap regardless of all other settings." IDS_SETTINGS_HIGHLIGHT_OPACITY "Zone opacity (%)" IDS_FANCYZONES L"FancyZones" + IDS_CANT_DRAG_ELEVATED L"We've detected an application running with administrator privileges. This blocks some functionality in PowerToys. Visit our wiki page to learn more." + IDS_CANT_DRAG_ELEVATED_LEARN_MORE L"Learn more" + IDS_CANT_DRAG_ELEVATED_DIALOG_DONT_SHOW_AGAIN L"Don't show again" + END 1 VERSIONINFO diff --git a/src/modules/fancyzones/lib/resource.h b/src/modules/fancyzones/lib/resource.h index 4cdcf6f748..0b860517bb 100644 --- a/src/modules/fancyzones/lib/resource.h +++ b/src/modules/fancyzones/lib/resource.h @@ -19,3 +19,6 @@ #define IDS_SETTING_EXCLCUDED_APPS_DESCRIPTION 119 #define IDS_SETTINGS_HIGHLIGHT_OPACITY 120 #define IDS_FANCYZONES 121 +#define IDS_CANT_DRAG_ELEVATED 122 +#define IDS_CANT_DRAG_ELEVATED_LEARN_MORE 123 +#define IDS_CANT_DRAG_ELEVATED_DIALOG_DONT_SHOW_AGAIN 124 diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 35291e8719..73422b175a 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -25,6 +25,7 @@ #if _DEBUG && _WIN64 #include "unhandled_exception_handler.h" #endif +#include extern "C" IMAGE_DOS_HEADER __ImageBase; @@ -39,6 +40,8 @@ namespace { const wchar_t MSI_VERSION_MUTEX_NAME[] = L"Local\\PowerToyRunMutex"; const wchar_t MSIX_VERSION_MUTEX_NAME[] = L"Local\\PowerToyMSIXRunMutex"; + + const wchar_t PT_URI_PROTOCOL_SCHEME[] = L"powertoys://"; } void chdir_current_executable() @@ -116,7 +119,7 @@ std::future check_github_updates() std::wstring contents = GITHUB_NEW_VERSION_AVAILABLE_OFFER_VISIT; contents += new_version->version_string; contents += L'.'; - notifications::show_toast_with_activations(contents, {}, { notifications::link_button{ GITHUB_NEW_VERSION_AGREE, new_version->release_page_uri.ToString() } }); + notifications::show_toast_with_activations(std::move(contents), {}, { notifications::link_button{ GITHUB_NEW_VERSION_AGREE, new_version->release_page_uri.ToString().c_str() } }); } void github_update_checking_worker() @@ -228,17 +231,22 @@ int runner(bool isProcessElevated) enum class SpecialMode { None, - Win32ToastNotificationCOMServer + Win32ToastNotificationCOMServer, + ToastNotificationHandler }; -SpecialMode should_run_in_special_mode() +SpecialMode should_run_in_special_mode(const int n_cmd_args, LPWSTR* cmd_arg_list) { - int nArgs; - LPWSTR* szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs); - for (size_t i = 1; i < nArgs; ++i) + for (size_t i = 1; i < n_cmd_args; ++i) { - if (!wcscmp(notifications::TOAST_ACTIVATED_LAUNCH_ARG, szArglist[i])) + if (!wcscmp(notifications::TOAST_ACTIVATED_LAUNCH_ARG, cmd_arg_list[i])) + { return SpecialMode::Win32ToastNotificationCOMServer; + } + else if (n_cmd_args == 2 && !wcsncmp(PT_URI_PROTOCOL_SCHEME, cmd_arg_list[i], wcslen(PT_URI_PROTOCOL_SCHEME))) + { + return SpecialMode::ToastNotificationHandler; + } } return SpecialMode::None; @@ -250,14 +258,42 @@ int win32_toast_notification_COM_server_mode() return 0; } +enum class toast_notification_handler_result +{ + exit_success, + exit_error +}; + +toast_notification_handler_result toast_notification_handler(const std::wstring_view param) +{ + if (param == L"cant_drag_elevated_disable/") + { + return disable_cant_drag_elevated_warning() ? toast_notification_handler_result::exit_success : toast_notification_handler_result::exit_error; + } + else + { + return toast_notification_handler_result::exit_error; + } +} + int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { winrt::init_apartment(); - switch (should_run_in_special_mode()) + int n_cmd_args = 0; + LPWSTR* cmd_arg_list = CommandLineToArgvW(GetCommandLineW(), &n_cmd_args); + switch (should_run_in_special_mode(n_cmd_args, cmd_arg_list)) { case SpecialMode::Win32ToastNotificationCOMServer: return win32_toast_notification_COM_server_mode(); + case SpecialMode::ToastNotificationHandler: + switch (toast_notification_handler(cmd_arg_list[1] + wcslen(PT_URI_PROTOCOL_SCHEME))) + { + case toast_notification_handler_result::exit_error: + return 1; + case toast_notification_handler_result::exit_success: + return 0; + } case SpecialMode::None: // continue as usual break; @@ -339,12 +375,12 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine auto general_settings = load_general_settings(); int rvalue = 0; - bool isProcessElevated = is_process_elevated(); - if (isProcessElevated || - general_settings.GetNamedBoolean(L"run_elevated", false) == false || - strcmp(lpCmdLine, "--dont-elevate") == 0) + const bool elevated = is_process_elevated(); + if ((elevated || + general_settings.GetNamedBoolean(L"run_elevated", false) == false || + strcmp(lpCmdLine, "--dont-elevate") == 0)) { - result = runner(isProcessElevated); + result = runner(elevated); } else {