SwiftUI: detect origin domain when dropped from a website - swift

In a macOS app (target: 12.3), I support dropping images from websites.
I would like to detect the domain of the website that the image was dropped from. I would show this domain in a confirmation dialog: "Are you sure you want to download this file from ?"
This origin is often different than the parsed URL.host.
E.g. if I drag and drop an image from unsplash.com (https://unsplash.com/photos/<photo-id>), the host is images.unsplash.com.
Some other websites don't even use their own subdomain for static files so host can be e.g. an AWS Cloudfront domain.
I checked the docs for NSItemProvider, URL and NSURL, but haven't found a solution yet.
Do you have any idea, e.g. do you know if NSItemProvider contains this info?
import SwiftUI
struct ContentView_Drop: View {
var body: some View {
Color.gray.onDrop(of: [.url], isTargeted: nil) { providers in
guard let provider = providers.first else { return false }
provider.loadObject(ofClass: URL.self) { url, _ in
if let url = url {
print("Dropped a url")
print("URL host: \(url.host)") // E.g. something.cloudfront.net
print("Drop origin: ???") // E.g. example.com
}
}
return true
}
}
}

Related

Keeping the constant part of a URL as a variable available to all view controllers

I am currently expressing JSON data by loading the information from a URL, the URL is from an API that comes in two forms:
test.example.com and example.com
Full links throughout the applications will always end differently after the forward slash:
test.example.com/example1 ... test.example.com/example2
But the beginning will always be one of the 2 forms above
I would like to the ability to easily switch between the two URL by changing it in one place using perhaps an extension that is available for all view controller.
So for example I have:
private func JSON() {
guard let url = URL(string: "https://test.example.com/example"),
let sample = value1.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)
else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = "example1=\(example)".data(using: .utf8)
URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data else { return }
do {
self.JStruct = try JSONDecoder().decode([exampleStruct].self,from:data)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
catch {
print(error)
}
}.resume()
}
What would be the best approach for switching between the two urls?
UPDATE:
Is it wrong to just use:
struct URLVar {
static var url = "https://example1.com/example/"
}
and reference URLVar.url when needed?
What I normally do is maintain an api.plist file (a key value file store) and an environment.plist file. The ending part of the APIs will be stored in the api.plist, while the domain parts example.com will be stored in the environment.plist.
Note that this way, I can have multiple environment.plist files pointing to different places (a production server, a staging server, etc...). I can simply switch in the necessary environment.plist using a build script phase.
To read, I use the normal plist reading mechanisms provided by apple and combine the domainUrl (in environment.plist) with the required resource path (in api.plist). The following link will explore reading options for you:
https://learnappmaking.com/plist-property-list-swift-how-to/
You can also look at the following library that adds some beautiful code generation capabilities for .plist files among others:
https://github.com/SwiftGen/SwiftGen

Security Scoped Bookmark - bookmark resolves but still can't access the file

EDIT: Additional information added at the bottom
I have a sandboxed, document based application that loads a user selected quicktime movie into an AVPlayer, and everything was working perfectly.
Now I am upgrading the code so that it will use Security Scoped bookmarks to get the URL rather than just storing a URL string so that the persistent store will allow the movie to be loaded upon relaunch of the application. When the bookmark is created it is stored in a Data variable of a managed object.
For some reason, this has broken the AVPlayer. While I have created a bookmark from the user selected URL, and can resolving the URL from the bookmark when the application is relaunched, the movie is not getting loaded into the AVPlayer correctly and I can't figure out why... I have confirmed that the URL being resolved from the bookmark does point to the movie file.
I have also added the appropriate entitlements to the project.
Here is my code:
Function Where User Selects a Movie To Load and Bookmark is Created
#IBAction func loadMovie(_ sender: Any) {
let openPanel = NSOpenPanel()
openPanel.title = "Select Video File To Import"
openPanel.allowedFileTypes = ["mov", "avi", "mp4"]
openPanel.begin { (result: NSApplication.ModalResponse) -> Void in
if result == NSApplication.ModalResponse.OK {
self.movieURL = openPanel.url
self.player = AVPlayer.init(url: self.movieURL!)
self.setupMovie()
if self.loadedMovieDatabase.count > 0 {
print("Movie Object Exists. Adding URL String")
self.loadedMovieDatabase[0].urlString = String(describing: self.movieURL!)
} else {
print("No Movie Object Exists Yet. Creating one and adding URL String")
let document = NSDocumentController.shared.currentDocument as! NSPersistentDocument
let myManagedObjectContext = document.managedObjectContext!
let newMovie = NSEntityDescription.insertNewObject(forEntityName: "Movie", into: myManagedObjectContext) as! MovieMO
self.loadedMovieDatabase.append(newMovie)
self.loadedMovieDatabase[0].urlString = String(describing: self.movieURL!)
}
// create Security-Scoped bookmark - Added 2/1/18
do {
try self.loadedMovieDatabase[0].bookmark = (self.movieURL?.bookmarkData(options: NSURL.BookmarkCreationOptions.withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil))!
} catch {
print("Can't create security bookmark!")
}
}
}
}
Function where Bookmark is Resolved into URL and Movie is Loaded
// initialize AVPlayer with URL stored in coreData movie object if it exists and is a valid path
if loadedMovieDatabase.count > 0 {
// initialize with saved movie path if it is valid (from security bookmark data)
// let myURL = URL(string: loadedMovieDatabase[0].urlString!) <- replaced with new code below
print("Loading URL from Bookmark")
var urlResult = false
var myURL : URL
do {
try myURL = URL.init(resolvingBookmarkData: loadedMovieDatabase[0].bookmark, bookmarkDataIsStale: &urlResult)!
print("URL Loaded from Bookmark")
print("URL is", myURL)
let isSecuredURL = myURL.startAccessingSecurityScopedResource()
print("IsSecured = ", isSecuredURL)
player = AVPlayer.init(url: myURL)
print("Setting Up Movie")
setupMovie()
} catch {
// No Data in bookmark so load default ColorBars movie instead
print("No Security Bookmark Available. Reverting to Default Color Bars")
let myURL = URL(string: initialMoviePath)
player = AVPlayer.init(url: myURL!)
setupMovie()
}
} else {
// load default ColorBars movie instead
print("Nothing was loaded so just set up a new document.")
let myURL = URL(string: initialMoviePath)
player = AVPlayer.init(url: myURL!)
setupMovie()
}
I am new to Security-Scoped Bookmarks, so I'm hoping that this may be obvious to anyone who has worked with them before.
I'm wondering if it's a problem with:
let isSecuredURL = myURL.startAccessingSecurityScopedResource()
Perhaps I'm calling this incorrectly? Sometimes I find Apple's documentation to be vague and confusing... Any insight would be appreciated!
EDIT:
I believe I know why this is happening, but I'm not sure how to fix it...
myURL.startAccessingSecurityScopedResource()
always returns FALSE... per the documentation that would mean that it's not working. Additionally, while the movie file is located on my Desktop, the Resolved URL comes up as the following (this may be normal, I don't know.):
file:///Users/me/Library/Containers/myapp/Data/Desktop/sample_on_desktop.mov
The apple docs make reference to the fact that a Document Scope can not use files in the system (aka "/Library"), but my entitlements are setup to use application-scope bookmarks, and my bookmark was created using the nil flag for relativeURL: so this shouldn't be an issue.
I just stumbled upon the answer accidentally...
For starters, when I was resolving the URL, I was not using the method which allows you to include OPTIONS, so my URL was resolved WITHOUT the security-scope. My original code to resolve was:
try myURL = URL.init(resolvingBookmarkData: loadedMovieDatabase[0].bookmark, bookmarkDataIsStable: &urlResult)!
When I should have been using the version with options here:
try myURL = URL.init(resolvingBookmarkData: loadedMovieDatabase[0].bookmark, Options: URL.bookmarkResolutionOptions.withSecurityScope, relativeTo: nil, bookmarkDataIsStable: &urlResult)!
Basically, I used the first init option Xcode presented in the predictive list with the words "resolvingBookmarkData:" when I should have looked further down the list. (This is how I found my error.)
NOTE also that it's important to use...
URL.bookmarkResolutionOptions.withSecurityScope
and not
URL.bookmarkCreationOptions.withSecurityScope
...when you're resolving your URL or it doesn't appear to work correctly.
Thus ends my frustration with this problem :) I hope this explanation might help others facing this problem!

Sandbox and Finder alias

I'm trying to create a security scoped URL for a user provide file which happens to be an alias using this methods which uses a resolvedFinderAlias() method:
func storeBookmark(url: URL) -> Bool
{
// Resolve alias before storing bookmark
let origURL = (url as NSURL).resolvedFinderAlias()
// Peek to see if we've seen this key before
if let data = bookmarks[url] {
if self.fetchBookmark(key: url, value: data) {
Swift.print ("= \(url.absoluteString)")
return true
}
}
do
{
let options:URL.BookmarkCreationOptions = [.withSecurityScope,.securityScopeAllowOnlyReadAccess]
let data = try url.bookmarkData(options: options, includingResourceValuesForKeys: nil, relativeTo: origURL)
bookmarks[url] = data
return self.fetchBookmark(key: url, value: data)
}
catch let error
{
NSApp.presentError(error)
Swift.print ("Error storing bookmark: \(url)")
return false
}
}
which throws an error in attempt to use the resolved URL to as the relative URL; I had originally just swapped the passed in URL to the origURL which ddin't work either.
The only solution is to not do this, or to previously be passed in the original URL. It's almost as if you cannot swap URLs you must be supplied that from either an open dialog or a pasteboard drop.
Are URLs which are aliases not suitable for sandbox work ?

How to get active tab in a Safari App Extension?

When I follow the Apple example I always get a nil activeTab:
override func toolbarItemClicked(in window: SFSafariWindow) {
// This method will be called when your toolbar item is clicked.
window.getActiveTab(completionHandler: { (activeTab) in
print(activeTab)
activeTab?.getActivePage(completionHandler: { (activePage) in
print(activePage)
activePage?.getPropertiesWithCompletionHandler( { (properties) in
print(properties)
if properties?.url != nil {
let urlString = properties!.url.absoluteString
print("URL!", urlString)
let videoURL = self.youtubeDl(urlString: urlString)
if videoURL != nil {
window.openTab(with: videoURL!, makeActiveIfPossible: true, completionHandler: nil)
}
}
})
})
})
}
Yes there is a getActiveTab:
SFSafariApplication.getActiveWindow { (window) in
window?.getActiveTab { (tab) in
tab?.getActivePage(completionHandler: { (page) in
}
}
}
Your Safari App Extension can only access the tabs of sites your extension is configured to access.
In your Info.plist, verify that the SFSafariWebsiteAccess dictionary:
Has a Level equal to "Some" or "All".
Has all required domains listed in the Allowed Domains array (if Level="Some").
Be aware that your extension's website access permissions are displayed in Safari > Preferences > Extensions. You are encouraged to limit your website access to "Some" (and explicit domains) unless you really do require access to every website.
You should go to info.plist, NSExtension->SFSafariWebsiteAccess and set something ( for example * - for all sites) to Allowed domains. Without this option, I always got nil from the window. I think your extension doesn't work if the site is not allowed.

How to get e-mail subject from message:// URL in OSX Swift

I have a desktop app that receives e-mail URLs ("message://" scheme) from the drag&drop pasteboard and I want to get the Subject from the relevant message. The only clue I have, so far, is that the QuickLook library might give me an information object where I can retrieve this info from.
Since the QuickLook API seems to be rather in flux at the moment and most examples show how to use it in iOS, I simply cannot find a way to set up my "Preview" object using a URL and get the information from there.
I would like to avoid setting up my project as a QuickLook plugin, or setting up the whole preview pane / view scaffolding; at the moment I just want to get out what QuickLook loads before it starts displaying, but I can't comprehend what paradigm Apple wants me to implement here.
XCode 7.3.1.
It turns out I misinterpreted the contents of draggingInfo.draggingPasteboard().types as a hierarchical list containing only one type of info (URL in this case).
Had to subscribe to dragged event type kUTTypeMessage as String and retrieve the e-mail subject from the pasteboard with stringForType("public.url-name")
EDIT: Note that the current Mail.app will sometimes create a stack of mails when you drag an e-mail thread. Although the method above still works to get the subject of the stack, there is no URL in the dragging info then and since there's no list of Message-IDs available either, I had to resort to scraping the user's mbox directory:
// See if we can resolve e-mail message meta data
if let mboxPath = pboard.stringForType("com.apple.mail.PasteboardTypeMessageTransfer") {
if let automatorPlist = pboard.propertyListForType("com.apple.mail.PasteboardTypeAutomator") {
// Get the latest e-mail in the thread
if let maxID = (automatorPlist.allObjects.flatMap({ $0["id"]! }) as AnyObject).valueForKeyPath("#max.self") as? Int {
// Read its meta data in the background
let emailItem = draggingEmailItem
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
// Find the e-mail file
if let path = Util.findEmlById(searchPath: mboxPath, id: maxID) {
// Read its contents
emailItem.properties = Util.metaDataFromEml(path)
dispatch_async(dispatch_get_main_queue(), {
// Update UI
});
}
}
}
}
}
Util funcs:
/* Searches the given path for <id>.eml[x] and returns its URL if found
*/
static func findEmlById(searchPath searchPath: String, id: Int)-> NSURL? {
let enumerator = NSFileManager.defaultManager().enumeratorAtPath(searchPath)
while let element = enumerator?.nextObject() as? NSString {
switch (element.lastPathComponent, element.pathExtension) {
case (let lpc, "emlx") where lpc.hasPrefix("\(id)"):
return NSURL(fileURLWithPath: searchPath).URLByAppendingPathComponent(element as String)!
case (let lpc, "eml") where lpc.hasPrefix("\(id)"):
return NSURL(fileURLWithPath: searchPath).URLByAppendingPathComponent(element as String)!
default: ()
}
}
return nil
}
/* Reads an eml[x] file and parses it, looking for e-mail meta data
*/
static func metaDataFromEml(path: NSURL)-> Dictionary<String, AnyObject> {
// TODO Support more fields
var properties: Dictionary<String, AnyObject> = [:]
do {
let emlxContent = try String(contentsOfURL: path, encoding: NSUTF8StringEncoding)
// Parse message ID from "...\nMessage-ID: <...>"
let messageIdStrMatches = emlxContent.regexMatches("[\\n\\r].*Message-ID:\\s*<([^\n\r]*)>")
if !messageIdStrMatches.isEmpty {
properties["messageId"] = messageIdStrMatches[0] as String
}
}
catch {
print("ERROR: Failed to open emlx file")
}
return properties
}
Note: If your app is sandboxed you will need the com.apple.security.temporary-exception.files.home-relative-path.read-only entitlement set to an array with one string in it: /Library/