blob: 0bc17ffffa7a48ec7aba490cf3af23b2b5f6b8d1 [file] [log] [blame]
/*
* Copyright (C) 2020-2021 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "MediaPermissionUtilities.h"
#import "SandboxUtilities.h"
#import "WKWebViewInternal.h"
#import "WebPageProxy.h"
#import <WebCore/LocalizedStrings.h>
#import <WebCore/SecurityOriginData.h>
#import <mutex>
#import <wtf/BlockPtr.h>
#import <wtf/URLHelpers.h>
#import <wtf/cocoa/TypeCastsCocoa.h>
#import <wtf/spi/cf/CFBundleSPI.h>
#import <wtf/spi/darwin/SandboxSPI.h>
#import "TCCSoftLink.h"
#import <pal/cocoa/AVFoundationSoftLink.h>
#import <pal/cocoa/SpeechSoftLink.h>
namespace WebKit {
bool checkSandboxRequirementForType(MediaPermissionType type)
{
#if PLATFORM(MAC)
static std::once_flag audioFlag;
static std::once_flag videoFlag;
static bool isAudioEntitled = true;
static bool isVideoEntitled = true;
auto checkFunction = [](const char* operation, bool* entitled) {
if (!currentProcessIsSandboxed())
return;
int result = sandbox_check(getpid(), operation, static_cast<enum sandbox_filter_type>(SANDBOX_CHECK_NO_REPORT | SANDBOX_FILTER_NONE));
if (result == -1)
WTFLogAlways("Error checking '%s' sandbox access, errno=%ld", operation, (long)errno);
*entitled = !result;
};
switch (type) {
case MediaPermissionType::Audio:
std::call_once(audioFlag, checkFunction, "device-microphone", &isAudioEntitled);
return isAudioEntitled;
case MediaPermissionType::Video:
std::call_once(videoFlag, checkFunction, "device-camera", &isVideoEntitled);
return isVideoEntitled;
}
#endif
return true;
}
bool checkUsageDescriptionStringForType(MediaPermissionType type)
{
static std::once_flag audioDescriptionFlag;
static std::once_flag videoDescriptionFlag;
static bool hasMicrophoneDescriptionString = false;
static bool hasCameraDescriptionString = false;
switch (type) {
case MediaPermissionType::Audio:
static TCCAccessPreflightResult audioAccess = TCCAccessPreflight(get_TCC_kTCCServiceMicrophone(), NULL);
if (audioAccess == kTCCAccessPreflightGranted)
return true;
std::call_once(audioDescriptionFlag, [] {
hasMicrophoneDescriptionString = dynamic_objc_cast<NSString>(NSBundle.mainBundle.infoDictionary[@"NSMicrophoneUsageDescription"]).length > 0;
});
return hasMicrophoneDescriptionString;
case MediaPermissionType::Video:
static TCCAccessPreflightResult videoAccess = TCCAccessPreflight(get_TCC_kTCCServiceCamera(), NULL);
if (videoAccess == kTCCAccessPreflightGranted)
return true;
std::call_once(videoDescriptionFlag, [] {
hasCameraDescriptionString = dynamic_objc_cast<NSString>(NSBundle.mainBundle.infoDictionary[@"NSCameraUsageDescription"]).length > 0;
});
return hasCameraDescriptionString;
}
}
bool checkUsageDescriptionStringForSpeechRecognition()
{
return dynamic_objc_cast<NSString>(NSBundle.mainBundle.infoDictionary[@"NSSpeechRecognitionUsageDescription"]).length > 0;
}
static NSString* visibleDomain(const String& host)
{
auto domain = WTF::URLHelpers::userVisibleURL(host.utf8());
return startsWithLettersIgnoringASCIICase(domain, "www.") ? domain.substring(4) : domain;
}
NSString *applicationVisibleNameFromOrigin(const WebCore::SecurityOriginData& origin)
{
if (origin.protocol != "http" && origin.protocol != "https")
return nil;
return visibleDomain(origin.host);
}
NSString *applicationVisibleName()
{
NSBundle *appBundle = [NSBundle mainBundle];
NSString *displayName = appBundle.infoDictionary[(__bridge NSString *)_kCFBundleDisplayNameKey];
NSString *readableName = appBundle.infoDictionary[(__bridge NSString *)kCFBundleNameKey];
return displayName ?: readableName;
}
static NSString *alertMessageText(MediaPermissionReason reason, const WebCore::SecurityOriginData& origin)
{
NSString *visibleOrigin = applicationVisibleNameFromOrigin(origin);
if (!visibleOrigin)
visibleOrigin = applicationVisibleName();
switch (reason) {
case MediaPermissionReason::Camera:
return [NSString stringWithFormat:WEB_UI_NSSTRING(@"Allow “%@” to use your camera?", @"Message for user camera access prompt"), visibleOrigin];
case MediaPermissionReason::CameraAndMicrophone:
return [NSString stringWithFormat:WEB_UI_NSSTRING(@"Allow “%@” to use your camera and microphone?", @"Message for user media prompt"), visibleOrigin];
case MediaPermissionReason::Microphone:
return [NSString stringWithFormat:WEB_UI_NSSTRING(@"Allow “%@” to use your microphone?", @"Message for user microphone access prompt"), visibleOrigin];
case MediaPermissionReason::ScreenCapture:
return [NSString stringWithFormat:WEB_UI_NSSTRING(@"Allow “%@” to observe your screen?", @"Message for screen sharing prompt"), visibleOrigin];
case MediaPermissionReason::DeviceOrientation:
return [NSString stringWithFormat:WEB_UI_NSSTRING(@"“%@” Would Like to Access Motion and Orientation", @"Message for requesting access to the device motion and orientation"), visibleOrigin];
case MediaPermissionReason::Geolocation:
return [NSString stringWithFormat:WEB_UI_NSSTRING(@"Allow “%@” to use your current location?", @"Message for geolocation prompt"), visibleOrigin];
case MediaPermissionReason::SpeechRecognition:
return [NSString stringWithFormat:WEB_UI_NSSTRING(@"Allow “%@” to capture your audio and use it for speech recognition?", @"Message for spechrecognition prompt"), visibleDomain(origin.host)];
}
}
static NSString *allowButtonText(MediaPermissionReason reason)
{
switch (reason) {
case MediaPermissionReason::Camera:
case MediaPermissionReason::CameraAndMicrophone:
case MediaPermissionReason::Microphone:
return WEB_UI_STRING_KEY(@"Allow", "Allow (usermedia)", @"Allow button title in user media prompt");
case MediaPermissionReason::ScreenCapture:
return WEB_UI_STRING_KEY(@"Allow", "Allow (screensharing)", @"Allow button title in screen sharing prompt");
case MediaPermissionReason::DeviceOrientation:
return WEB_UI_STRING_KEY(@"Allow", "Allow (device motion and orientation access)", @"Button title in Device Orientation Permission API prompt");
case MediaPermissionReason::Geolocation:
return WEB_UI_STRING_KEY(@"Allow", "Allow (geolocation)", @"Allow button title in geolocation prompt");
case MediaPermissionReason::SpeechRecognition:
return WEB_UI_STRING_KEY(@"Allow", "Allow (speechrecognition)", @"Allow button title in speech recognition prompt");
}
}
static NSString *doNotAllowButtonText(MediaPermissionReason reason)
{
switch (reason) {
case MediaPermissionReason::Camera:
case MediaPermissionReason::CameraAndMicrophone:
case MediaPermissionReason::Microphone:
return WEB_UI_STRING_KEY(@"Don’t Allow", "Don’t Allow (usermedia)", @"Disallow button title in user media prompt");
case MediaPermissionReason::ScreenCapture:
return WEB_UI_STRING_KEY(@"Don’t Allow", "Don’t Allow (screensharing)", @"Disallow button title in screen sharing prompt");
case MediaPermissionReason::DeviceOrientation:
return WEB_UI_STRING_KEY(@"Cancel", "Cancel (device motion and orientation access)", @"Button title in Device Orientation Permission API prompt");
case MediaPermissionReason::Geolocation:
return WEB_UI_STRING_KEY(@"Don’t Allow", "Don’t Allow (geolocation)", @"Disallow button title in geolocation prompt");
case MediaPermissionReason::SpeechRecognition:
return WEB_UI_STRING_KEY(@"Don’t Allow", "Don’t Allow (speechrecognition)", @"Disallow button title in speech recognition prompt");
}
}
void alertForPermission(WebPageProxy& page, MediaPermissionReason reason, const WebCore::SecurityOriginData& origin, CompletionHandler<void(bool)>&& completionHandler)
{
#if PLATFORM(IOS_FAMILY)
if (reason == MediaPermissionReason::DeviceOrientation) {
if (auto& userPermissionHandler = page.deviceOrientationUserPermissionHandlerForTesting())
return completionHandler(userPermissionHandler());
}
#endif
auto webView = page.cocoaView();
if (!webView) {
completionHandler(false);
return;
}
auto *alertTitle = alertMessageText(reason, origin);
if (!alertTitle) {
completionHandler(false);
return;
}
auto *allowButtonString = allowButtonText(reason);
auto *doNotAllowButtonString = doNotAllowButtonText(reason);
auto completionBlock = makeBlockPtr(WTFMove(completionHandler));
#if PLATFORM(MAC)
auto alert = adoptNS([NSAlert new]);
[alert setMessageText:alertTitle];
NSButton *button = [alert addButtonWithTitle:allowButtonString];
button.keyEquivalent = @"";
button = [alert addButtonWithTitle:doNotAllowButtonString];
button.keyEquivalent = @"\E";
[alert beginSheetModalForWindow:[webView window] completionHandler:[completionBlock](NSModalResponse returnCode) {
auto shouldAllow = returnCode == NSAlertFirstButtonReturn;
completionBlock(shouldAllow);
}];
#else
UIAlertController* alert = [UIAlertController alertControllerWithTitle:alertTitle message:nil preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* allowAction = [UIAlertAction actionWithTitle:allowButtonString style:UIAlertActionStyleDefault handler:[completionBlock](UIAlertAction *action) {
completionBlock(true);
}];
UIAlertAction* doNotAllowAction = [UIAlertAction actionWithTitle:doNotAllowButtonString style:UIAlertActionStyleCancel handler:[completionBlock](UIAlertAction *action) {
completionBlock(false);
}];
[alert addAction:doNotAllowAction];
[alert addAction:allowAction];
[[UIViewController _viewControllerForFullScreenPresentationFromView:webView.get()] presentViewController:alert animated:YES completion:nil];
#endif
}
#if HAVE(AVCAPTUREDEVICE)
void requestAVCaptureAccessForType(MediaPermissionType type, CompletionHandler<void(bool authorized)>&& completionHandler)
{
ASSERT(isMainRunLoop());
AVMediaType mediaType = type == MediaPermissionType::Audio ? AVMediaTypeAudio : AVMediaTypeVideo;
auto decisionHandler = makeBlockPtr([completionHandler = WTFMove(completionHandler)](BOOL authorized) mutable {
callOnMainRunLoop([completionHandler = WTFMove(completionHandler), authorized]() mutable {
completionHandler(authorized);
});
});
[PAL::getAVCaptureDeviceClass() requestAccessForMediaType:mediaType completionHandler:decisionHandler.get()];
}
MediaPermissionResult checkAVCaptureAccessForType(MediaPermissionType type)
{
AVMediaType mediaType = type == MediaPermissionType::Audio ? AVMediaTypeAudio : AVMediaTypeVideo;
auto authorizationStatus = [PAL::getAVCaptureDeviceClass() authorizationStatusForMediaType:mediaType];
if (authorizationStatus == AVAuthorizationStatusDenied || authorizationStatus == AVAuthorizationStatusRestricted)
return MediaPermissionResult::Denied;
if (authorizationStatus == AVAuthorizationStatusNotDetermined)
return MediaPermissionResult::Unknown;
return MediaPermissionResult::Granted;
}
#endif // HAVE(AVCAPTUREDEVICE)
#if HAVE(SPEECHRECOGNIZER)
void requestSpeechRecognitionAccess(CompletionHandler<void(bool authorized)>&& completionHandler)
{
ASSERT(isMainRunLoop());
auto decisionHandler = makeBlockPtr([completionHandler = WTFMove(completionHandler)](SFSpeechRecognizerAuthorizationStatus status) mutable {
bool authorized = status == SFSpeechRecognizerAuthorizationStatusAuthorized;
callOnMainRunLoop([completionHandler = WTFMove(completionHandler), authorized]() mutable {
completionHandler(authorized);
});
});
[PAL::getSFSpeechRecognizerClass() requestAuthorization:decisionHandler.get()];
}
MediaPermissionResult checkSpeechRecognitionServiceAccess()
{
auto authorizationStatus = [PAL::getSFSpeechRecognizerClass() authorizationStatus];
if (authorizationStatus == SFSpeechRecognizerAuthorizationStatusDenied || authorizationStatus == SFSpeechRecognizerAuthorizationStatusRestricted)
return MediaPermissionResult::Denied;
if (authorizationStatus == SFSpeechRecognizerAuthorizationStatusAuthorized)
return MediaPermissionResult::Granted;
return MediaPermissionResult::Unknown;
}
bool checkSpeechRecognitionServiceAvailability(const String& localeIdentifier)
{
auto recognizer = localeIdentifier.isEmpty() ? adoptNS([PAL::allocSFSpeechRecognizerInstance() init]) : adoptNS([PAL::allocSFSpeechRecognizerInstance() initWithLocale:[NSLocale localeWithLocaleIdentifier:localeIdentifier]]);
return recognizer && [recognizer isAvailable];
}
#endif // HAVE(SPEECHRECOGNIZER)
} // namespace WebKit