From 4eb156b71658bb2ac64ea325f3792c2234006dee Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Sat, 21 May 2022 12:20:43 +0200 Subject: [PATCH] feat: support new PHPhotos access Closes https://github.com/codebytere/node-mac-permissions/issues/44 --- README.md | 10 ++++++- index.js | 13 +++++++-- permissions.mm | 67 +++++++++++++++++++++++++++++++++++---------- test/module.spec.js | 3 +- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 67cba2c..88fa159 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Return Value Descriptions: * `restricted` - The application is not authorized to access `type` data. The user cannot change this application’s status, possibly due to active restrictions such as parental controls being in place. * `denied` - The user explicitly denied access to `type` data for the application. * `authorized` - The application is authorized to access `type` data. +* `limited` - The application is authorized for limited access to `type` data. Currently only applicable to the `photos` type. **Notes:** * Access to `bluetooth` will always return a status of `authorized` prior to macOS 10.15, as the underlying API was not introduced until that version. @@ -330,7 +331,9 @@ askForMusicLibraryAccess().then(status => { }) ``` -### `permissions.askForPhotosAccess()` +### `permissions.askForPhotosAccess([accessLevel])` + +* `accessLevel` String (optional) - The access level being requested of Photos. Can be either `add-only` or `read-write`. Only available on macOS 11 or higher. Returns `Promise` - Current permission status; can be `authorized`, `denied`, or `restricted`. @@ -347,6 +350,11 @@ Your app must provide an explanation for its use of the photo library using the Your reason for wanting to access Photos ``` +**Note:** + +You should add the `PHPhotoLibraryPreventAutomaticLimitedAccessAlert` key with a Boolean value of `YES` to your app’s `Info.plist` file to prevent the system from automatically presenting the limited library selection prompt. See [`PHAuthorizationStatusLimited`](https://developer.apple.com/documentation/photokit/phauthorizationstatus/phauthorizationstatuslimited?language=objc) for more information. + + Example: ```js diff --git a/index.js b/index.js index 86bb7b1..ccf6ae3 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,8 @@ function getAuthStatus(type) { 'location', 'microphone', 'music-library', - 'photos', + 'photos-add-only', + 'photos-read-write', 'reminders', 'speech-recognition', 'screen', @@ -35,6 +36,14 @@ function askForFoldersAccess(folder) { return permissions.askForFoldersAccess.call(this, folder) } +function askForPhotosAccess(accessLevel = 'add-only') { + if (!['add-only', 'read-write'].includes(accessLevel)) { + throw new TypeError(`${accessLevel} must be one of either 'add-only' or 'read-write'`) + } + + return permissions.askForPhotosAccess.call(this, accessLevel) +} + module.exports = { askForAccessibilityAccess: permissions.askForAccessibilityAccess, askForCalendarAccess: permissions.askForCalendarAccess, @@ -46,7 +55,7 @@ module.exports = { askForRemindersAccess: permissions.askForRemindersAccess, askForMicrophoneAccess: permissions.askForMicrophoneAccess, askForMusicLibraryAccess: permissions.askForMusicLibraryAccess, - askForPhotosAccess: permissions.askForPhotosAccess, + askForPhotosAccess, askForSpeechRecognitionAccess: permissions.askForSpeechRecognitionAccess, askForScreenCaptureAccess: permissions.askForScreenCaptureAccess, getAuthStatus, diff --git a/permissions.mm b/permissions.mm index 4796f61..6e17960 100644 --- a/permissions.mm +++ b/permissions.mm @@ -21,6 +21,12 @@ const std::string kDenied{"denied"}; const std::string kRestricted{"restricted"}; const std::string kNotDetermined{"not determined"}; +const std::string kLimited{"limited"}; + +PHAccessLevel GetPHAccessLevel(const std::string &type) + API_AVAILABLE(macosx(11.0)) { + return type == "read-write" ? PHAccessLevelReadWrite : PHAccessLevelAddOnly; +} NSURL *URLForDirectory(NSSearchPathDirectory directory) { NSFileManager *fm = [NSFileManager defaultManager]; @@ -367,8 +373,16 @@ bool HasOpenSystemPreferencesDialog() { // Returns a status indicating whether or not the user has authorized Photos // access. -std::string PhotosAuthStatus() { - PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; +std::string PhotosAuthStatus(const std::string &access_level) { + PHAuthorizationStatus status = PHAuthorizationStatusNotDetermined; + + if (@available(macOS 11, *)) { + PHAccessLevel level = GetPHAccessLevel(access_level); + status = [PHPhotoLibrary authorizationStatusForAccessLevel:level]; + } else { + status = [PHPhotoLibrary authorizationStatus]; + } + return StringFromPhotosStatus(status); } @@ -390,8 +404,10 @@ bool HasOpenSystemPreferencesDialog() { auth_status = FDAAuthStatus(); } else if (type == "microphone") { auth_status = MediaAuthStatus("microphone"); - } else if (type == "photos") { - auth_status = PhotosAuthStatus(); + } else if (type == "photos-add-only") { + auth_status = PhotosAuthStatus("add-only"); + } else if (type == "photos-read-write") { + auth_status = PhotosAuthStatus("read-write"); } else if (type == "speech-recognition") { auth_status = SpeechRecognitionAuthStatus(); } else if (type == "camera") { @@ -603,18 +619,40 @@ void AskForFullDiskAccess(const Napi::CallbackInfo &info) { Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New( env, Napi::Function::New(env, NoOp), "photosCallback", 0, 1); - std::string auth_status = PhotosAuthStatus(); + std::string access_level = info[0].As().Utf8Value(); + std::string auth_status = PhotosAuthStatus(access_level); + if (auth_status == kNotDetermined) { __block Napi::ThreadSafeFunction tsfn = ts_fn; - [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { - auto callback = [=](Napi::Env env, Napi::Function js_cb, - const char *granted) { - deferred.Resolve(Napi::String::New(env, granted)); - }; - std::string auth_result = StringFromPhotosStatus(status); - tsfn.BlockingCall(auth_result.c_str(), callback); - tsfn.Release(); - }]; + if (@available(macOS 11, *)) { + [PHPhotoLibrary + requestAuthorizationForAccessLevel:GetPHAccessLevel(access_level) + handler:^(PHAuthorizationStatus status) { + auto callback = + [=](Napi::Env env, + Napi::Function js_cb, + const char *granted) { + deferred.Resolve(Napi::String::New( + env, granted)); + }; + std::string auth_result = + StringFromPhotosStatus(status); + tsfn.BlockingCall(auth_result.c_str(), + callback); + tsfn.Release(); + }]; + } else { + __block Napi::ThreadSafeFunction tsfn = ts_fn; + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { + auto callback = [=](Napi::Env env, Napi::Function js_cb, + const char *granted) { + deferred.Resolve(Napi::String::New(env, granted)); + }; + std::string auth_result = StringFromPhotosStatus(status); + tsfn.BlockingCall(auth_result.c_str(), callback); + tsfn.Release(); + }]; + } } else if (auth_status == kDenied) { OpenPrefPane("Privacy_Photos"); @@ -624,7 +662,6 @@ void AskForFullDiskAccess(const Napi::CallbackInfo &info) { ts_fn.Release(); deferred.Resolve(Napi::String::New(env, auth_status)); } - return deferred.Promise(); } diff --git a/test/module.spec.js b/test/module.spec.js index 9569d2b..f7646df 100644 --- a/test/module.spec.js +++ b/test/module.spec.js @@ -21,7 +21,8 @@ describe('node-mac-permissions', () => { 'location', 'microphone', 'music-library', - 'photos', + 'photos-add-only', + 'photos-read-write', 'reminders', 'speech-recognition', 'screen',