Skip to content

Commit

Permalink
Merge pull request #28 from lucka-me/fix-macos-extension-permission
Browse files Browse the repository at this point in the history
Fix permission issue for Safari extension on macOS
  • Loading branch information
lucka-me authored Jul 27, 2024
2 parents 487df4f + c2a5a2a commit 0798ed3
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 46 deletions.
4 changes: 2 additions & 2 deletions IntelStack.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.5.0;
MARKETING_VERSION = 1.5.1;
PRODUCT_BUNDLE_IDENTIFIER = dev.lucka.IntelStack;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
Expand Down Expand Up @@ -953,7 +953,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.5.0;
MARKETING_VERSION = 1.5.1;
PRODUCT_BUNDLE_IDENTIFIER = dev.lucka.IntelStack;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
Expand Down
124 changes: 94 additions & 30 deletions Shared/Extensions/UserDefaults+Bookmark.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,56 +8,54 @@
import Foundation

extension UserDefaults {
// A solution from quoid/userscripts/xcode/Shared/Preferences.swift
// For macOS, the security-scoped bookmark (created with .withSecurityScope option) is resolvable only in the main
// app, but not in the app extension, but a non-security-scoped bookmark (or called implicity security scope) is
// resolvable and accessable in the app extension (really weird).
#if os(macOS)
fileprivate static let implicitySecurityScopeBookmarkKeySuffix = ".ImplicitySecurityScope"
#endif

func bookmark(forKey defaultName: String) -> URL? {
let bookmarkData: Data?
let securityScopedBookmark: Data?
#if os(macOS)
if Bundle.main.isAppExtension {
bookmarkData = data(forKey: defaultName + Self.implicitySecurityScopeBookmarkKeySuffix)
let isAppExtension = Bundle.main.isAppExtension
if isAppExtension {
securityScopedBookmark = syncBookmark(forKey: defaultName)
} else {
bookmarkData = data(forKey: defaultName)
securityScopedBookmark = data(forKey: defaultName)
}
#else
bookmarkData = data(forKey: defaultName)
securityScopedBookmark = data(forKey: defaultName)
#endif
guard let bookmarkData else { return nil }
guard let securityScopedBookmark else { return nil }

var bookmarkDataIsStale = false
let url: URL?
#if os(macOS)
if Bundle.main.isAppExtension {
url = try? .init(resolvingBookmarkData: bookmarkData, bookmarkDataIsStale: &bookmarkDataIsStale)
// Prevent updating bookmark in app extension
bookmarkDataIsStale = false
} else {
url = try? .init(
resolvingSecurityScopedBookmarkData: bookmarkData, bookmarkDataIsStale: &bookmarkDataIsStale
)
}
#else
url = try? .init(
resolvingSecurityScopedBookmarkData: bookmarkData, bookmarkDataIsStale: &bookmarkDataIsStale
let url = try? URL(
resolvingSecurityScopedBookmarkData: securityScopedBookmark,
bookmarkDataIsStale: &bookmarkDataIsStale
)
#endif
if bookmarkDataIsStale, let url, url.startAccessingSecurityScopedResource() {
#if os(macOS)
if isAppExtension {
// Update the "extension-al" security-scoped bookmark only
if let updatedBookmark = try? url.bookmarkData(options: .withSecurityScope) {
set(updatedBookmark, forKey: Self.extensionDataKey(forKey: defaultName))
}
} else {
setBookmark(url, forKey: defaultName)
}
#else
setBookmark(url, forKey: defaultName)
#endif
url.stopAccessingSecurityScopedResource()
}

return url
}

func setBookmark(_ url: URL?, forKey defaultName: String) {
#if os(macOS)
let appSyncKey = Self.appSyncKey(forKey: defaultName)
#endif
guard let url else {
let value: Data? = nil
set(value, forKey: defaultName)
#if os(macOS)
set(value, forKey: appSyncKey)
#endif
return
}
#if os(macOS)
Expand All @@ -67,11 +65,77 @@ extension UserDefaults {
else {
return
}
set(implicitySecurityScopeBookmark, forKey: defaultName + Self.implicitySecurityScopeBookmarkKeySuffix)
set(implicitySecurityScopeBookmark, forKey: appSyncKey)
#else
guard let bookmark = try? url.bookmarkData() else { return }
#endif
set(bookmark, forKey: defaultName)
return
}
}

#if os(macOS)
fileprivate extension UserDefaults {
// A solution from quoid/userscripts/xcode/Shared/Preferences.swift
// On macOS, a security-scoped bookmark (created with .withSecurityScope option) is only resolvable in the bundle
// in which it was created, but not in other bundles. A non-security-scoped (or called implicity security scope)
// bookmark is resolvable in other bundles UNTIL reboot.
// So we sync the URL from app to extension(s) by:
// 1. In the App, when storing the bookmark, we also set a "global" non-security-scoped bookmark
// 2. In every extension, we hold an "extension-al" non-security-scoped bookmark and an "extension-al" security-
// scoped bookmark
// 3. In extension, when we need the URL, we get both the non-security-scoped bookmarks first
// 3.1. If they are equal, the URL is not changed, we use the "extension-al" security-scoped bookmark directly
// 3.2. If not, the URL was changed, we resolve the "global" one and update the "extension-al" security-scoped bookmark
static let implicitySecurityScopeBookmarkKeySuffix = "ImplicitySecurityScope"
static let bundleIdentifier = Bundle.main.bundleIdentifier!

// Generate key for "global" non-security-scoped bookmark
static func appSyncKey(forKey defaultName: String) -> String {
"\(defaultName).\(Self.implicitySecurityScopeBookmarkKeySuffix)"
}

// Generate key for "extension-al" security-scoped bookmark
static func extensionDataKey(forKey defaultName: String) -> String {
"\(defaultName).\(Self.bundleIdentifier)"
}

// Generate key for "extension-al" non-security-scoped bookmark
static func extensionSyncKey(forKey defaultName: String) -> String {
"\(defaultName).\(Self.bundleIdentifier).\(Self.implicitySecurityScopeBookmarkKeySuffix)"
}

func syncBookmark(forKey defaultName: String) -> Data? {
let extensionDataKey = Self.extensionDataKey(forKey: defaultName)
let extensionSyncKey = Self.extensionSyncKey(forKey: defaultName)
guard
let appSyncData = data(forKey: Self.appSyncKey(forKey: defaultName))
else {
let value: Data? = nil
set(value, forKey: extensionSyncKey)
set(value, forKey: extensionDataKey)
return value
}
let extensionSyncData = data(forKey: extensionSyncKey)
guard extensionSyncData != appSyncData else {
// Already synced
return data(forKey: extensionDataKey)
}
var bookmarkDataIsStale = false
guard
let url = try? URL(resolvingBookmarkData: appSyncData, bookmarkDataIsStale: &bookmarkDataIsStale),
url.startAccessingSecurityScopedResource(),
let syncedData = try? url.bookmarkData(options: .withSecurityScope)
else {
let value: Data? = nil
set(value, forKey: extensionSyncKey)
set(value, forKey: extensionDataKey)
return nil
}
url.stopAccessingSecurityScopedResource()
set(appSyncData, forKey: extensionSyncKey)
set(syncedData, forKey: extensionDataKey)
return syncedData
}
}
#endif
52 changes: 38 additions & 14 deletions Shared/Localization/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,13 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "The location to save external plugins is unavailable"
"value" : "The folder to save external plugins is unavailable"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "存储外部插件的位置不可用"
"value" : "存储外部插件的文件夹不可用"
}
}
}
Expand All @@ -246,13 +246,13 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Please ensure the location is selected in Settings and existing."
"value" : "Please ensure the folder is selected in Settings and existing."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "请确保在设置页面设置了位置,并且这个位置存在。"
"value" : "请确保在设置页面设置了文件夹,并且这个文件夹存在。"
}
}
}
Expand Down Expand Up @@ -1206,29 +1206,53 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Change Location"
"value" : "Change Folder"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "更换位置"
"value" : "更改文件夹"
}
}
}
},
"SettingsView.ExternalScripts.Footer" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Intel Stack will load user scripts from the location."
"variations" : {
"device" : {
"mac" : {
"stringUnit" : {
"state" : "translated",
"value" : "Intel Stack will load user scripts from the folder.\nAfter selecting or changing the folder, please enable IITC and open Ingress Intel webpage once before the next reboot."
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "Intel Stack will load user scripts from the folder."
}
}
}
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "Intel Stack会从选择的位置载入用户脚本。"
"variations" : {
"device" : {
"mac" : {
"stringUnit" : {
"state" : "translated",
"value" : "Intel Stack会从选择的文件夹载入用户脚本。\n如果您选择或更改了文件夹,请在下次重新启动此设备之前开启IITC并于Safari浏览器中访问一次Intel网页。"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "Intel Stack会从选择的文件夹载入用户脚本。"
}
}
}
}
}
}
Expand All @@ -1238,13 +1262,13 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Select Location"
"value" : "Select Folder"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "选择位置"
"value" : "选择文件夹"
}
}
}
Expand Down

0 comments on commit 0798ed3

Please sign in to comment.