MIDIPermissionRequest.cpp (7237B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim:set ts=2 sw=2 sts=2 et cindent: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "mozilla/dom/MIDIPermissionRequest.h" 8 9 #include "mozilla/BasePrincipal.h" 10 #include "mozilla/Preferences.h" 11 #include "mozilla/RandomNum.h" 12 #include "mozilla/StaticPrefs_dom.h" 13 #include "mozilla/dom/Document.h" 14 #include "mozilla/dom/MIDIAccessManager.h" 15 #include "mozilla/dom/MIDIOptionsBinding.h" 16 #include "mozilla/ipc/BackgroundChild.h" 17 #include "mozilla/ipc/PBackgroundChild.h" 18 #include "nsContentUtils.h" 19 #include "nsIGlobalObject.h" 20 21 //------------------------------------------------- 22 // MIDI Permission Requests 23 //------------------------------------------------- 24 25 using namespace mozilla::dom; 26 27 NS_IMPL_CYCLE_COLLECTION_INHERITED(MIDIPermissionRequest, 28 ContentPermissionRequestBase, mPromise) 29 30 NS_IMPL_QUERY_INTERFACE_CYCLE_COLLECTION_INHERITED(MIDIPermissionRequest, 31 ContentPermissionRequestBase, 32 nsIRunnable) 33 34 NS_IMPL_ADDREF_INHERITED(MIDIPermissionRequest, ContentPermissionRequestBase) 35 NS_IMPL_RELEASE_INHERITED(MIDIPermissionRequest, ContentPermissionRequestBase) 36 37 MIDIPermissionRequest::MIDIPermissionRequest(nsPIDOMWindowInner* aWindow, 38 Promise* aPromise, 39 const MIDIOptions& aOptions) 40 : ContentPermissionRequestBase( 41 aWindow->GetDoc()->NodePrincipal(), aWindow, 42 ""_ns, // We check prefs in a custom way here 43 "midi"_ns), 44 mPromise(aPromise), 45 mNeedsSysex(aOptions.mSysex) { 46 MOZ_ASSERT(aWindow); 47 MOZ_ASSERT(aPromise, "aPromise should not be null!"); 48 MOZ_ASSERT(aWindow->GetDoc()); 49 mPrincipal = aWindow->GetDoc()->NodePrincipal(); 50 MOZ_ASSERT(mPrincipal); 51 } 52 53 NS_IMETHODIMP 54 MIDIPermissionRequest::GetTypes(nsIArray** aTypes) { 55 NS_ENSURE_ARG_POINTER(aTypes); 56 nsTArray<nsString> options; 57 58 // The previous implementation made no differences between midi and 59 // midi-sysex. The check on the SitePermsAddonProvider pref should be removed 60 // at the same time as the old implementation. 61 if (mNeedsSysex || !StaticPrefs::dom_sitepermsaddon_provider_enabled()) { 62 options.AppendElement(u"sysex"_ns); 63 } 64 return nsContentPermissionUtils::CreatePermissionArray(mType, options, 65 aTypes); 66 } 67 68 NS_IMETHODIMP 69 MIDIPermissionRequest::Cancel() { 70 mCancelTimer = nullptr; 71 mPromise->MaybeRejectWithSecurityError( 72 "WebMIDI requires a site permission add-on to activate"); 73 return NS_OK; 74 } 75 76 NS_IMETHODIMP 77 MIDIPermissionRequest::Allow(JS::Handle<JS::Value> aChoices) { 78 MOZ_ASSERT(aChoices.isUndefined()); 79 MIDIAccessManager* mgr = MIDIAccessManager::Get(); 80 mgr->CreateMIDIAccess(mWindow, mNeedsSysex, mPromise); 81 return NS_OK; 82 } 83 84 NS_IMETHODIMP 85 MIDIPermissionRequest::Run() { 86 // If the testing flag is true, skip dialog 87 if (Preferences::GetBool("midi.prompt.testing", false)) { 88 bool allow = 89 Preferences::GetBool("media.navigator.permission.disabled", false); 90 if (allow) { 91 Allow(JS::UndefinedHandleValue); 92 } else { 93 Cancel(); 94 } 95 return NS_OK; 96 } 97 98 nsCString permName = "midi"_ns; 99 // The previous implementation made no differences between midi and 100 // midi-sysex. The check on the SitePermsAddonProvider pref should be removed 101 // at the same time as the old implementation. 102 if (mNeedsSysex || !StaticPrefs::dom_sitepermsaddon_provider_enabled()) { 103 permName.Append("-sysex"); 104 } 105 106 // First, check for an explicit allow/deny. Note that we want to support 107 // granting a permission on the base domain and then using it on a subdomain, 108 // which is why we use the non-"Exact" variants of these APIs. See bug 109 // 1757218. 110 if (nsContentUtils::IsSitePermAllow(mPrincipal, permName)) { 111 Allow(JS::UndefinedHandleValue); 112 return NS_OK; 113 } 114 115 if (nsContentUtils::IsSitePermDeny(mPrincipal, permName)) { 116 CancelWithRandomizedDelay(); 117 return NS_OK; 118 } 119 120 // If the add-on is not installed, and sitepermsaddon provider not enabled, 121 // auto-deny (except for localhost). 122 if (StaticPrefs::dom_webmidi_gated() && 123 !StaticPrefs::dom_sitepermsaddon_provider_enabled() && 124 !nsContentUtils::HasSitePerm(mPrincipal, permName) && 125 !mPrincipal->GetIsLoopbackHost()) { 126 CancelWithRandomizedDelay(); 127 return NS_OK; 128 } 129 130 // If sitepermsaddon provider is enabled and user denied install, 131 // auto-deny (except for localhost, where we use a regular permission flow). 132 if (StaticPrefs::dom_sitepermsaddon_provider_enabled() && 133 nsContentUtils::IsSitePermDeny(mPrincipal, "install"_ns) && 134 !mPrincipal->GetIsLoopbackHost()) { 135 CancelWithRandomizedDelay(); 136 return NS_OK; 137 } 138 139 // Before we bother the user with a prompt, see if they have any devices. If 140 // they don't, just report denial. 141 MOZ_ASSERT(NS_IsMainThread()); 142 mozilla::ipc::PBackgroundChild* actor = 143 mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); 144 if (NS_WARN_IF(!actor)) { 145 return NS_ERROR_FAILURE; 146 } 147 RefPtr<MIDIPermissionRequest> self = this; 148 actor->SendHasMIDIDevice( 149 [=](bool aHasDevices) { 150 MOZ_ASSERT(NS_IsMainThread()); 151 152 if (aHasDevices) { 153 self->DoPrompt(); 154 } else { 155 nsContentUtils::ReportToConsoleNonLocalized( 156 u"Silently denying site request for MIDI access because no devices were detected. You may need to restart your browser after connecting a new device."_ns, 157 nsIScriptError::infoFlag, "WebMIDI"_ns, mWindow->GetDoc()); 158 self->CancelWithRandomizedDelay(); 159 } 160 }, 161 [=](auto) { self->CancelWithRandomizedDelay(); }); 162 163 return NS_OK; 164 } 165 166 // If the user has no MIDI devices, we automatically deny the request. To 167 // prevent sites from using timing attack to discern the existence of MIDI 168 // devices, we instrument silent denials with a randomized delay between 3 169 // and 13 seconds, which is intended to model the time the user might spend 170 // considering a prompt before denying it. 171 // 172 // Note that we set the random component of the delay to zero in automation 173 // to avoid unnecessarily increasing test end-to-end time. 174 void MIDIPermissionRequest::CancelWithRandomizedDelay() { 175 MOZ_ASSERT(NS_IsMainThread()); 176 uint32_t baseDelayMS = 3 * 1000; 177 uint32_t randomDelayMS = 178 xpc::IsInAutomation() ? 0 : RandomUint64OrDie() % (10 * 1000); 179 auto delay = TimeDuration::FromMilliseconds(baseDelayMS + randomDelayMS); 180 RefPtr<MIDIPermissionRequest> self = this; 181 NS_NewTimerWithCallback( 182 getter_AddRefs(mCancelTimer), [=](auto) { self->Cancel(); }, delay, 183 nsITimer::TYPE_ONE_SHOT, 184 "MIDIPermissionRequest::CancelWithRandomizedDelay"_ns); 185 } 186 187 nsresult MIDIPermissionRequest::DoPrompt() { 188 if (NS_FAILED(nsContentPermissionUtils::AskPermission(this, mWindow))) { 189 Cancel(); 190 return NS_ERROR_FAILURE; 191 } 192 return NS_OK; 193 }