Facing issues with ContentBlockerRequestHandler of Safari extension - swift

I am currently working on a safari app extension that blocks content. I want the user to configure the rule (turning a rule on and off). Since I can’t overwrite the bundled JSON files and we can’t write to the documents folder, as it’s not accessible to the extension I decided to use App Groups. My approach looks like this:
Within the ContentBlockerRequestHandler I want to save the blockerList.json into the app group (Only when launched for the first time)
When this is done I want that the handler reads from the app group by taking the url of my json which is within the app group instead of taking the default json in the extension
Since I can not debug the handler I don't know if I am on the right path. The following shows my code:
class ContentBlockerRequestHandler: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
guard let rulesUrl = loadRules() else {
let clonedRules = cloneBlockerList()
save(rules: clonedRules)
return
}
guard let attachment = NSItemProvider(contentsOf: rulesUrl) else { return }
let item = NSExtensionItem()
item.attachments = [attachment]
context.completeRequest(returningItems: [item], completionHandler: nil)
}
private func cloneBlockerList() -> [Rule] {
var rules: [Rule] = []
if let url = Bundle.main.url(forResource: "blockerList", withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let jsonData = try decoder.decode(ResponseData.self, from: data)
rules = jsonData.rules
} catch {
print("error:(error)")
}
}
return rules
}
private func save(rules: [Rule]) {
let documentsDirectory = FileManager().containerURL(forSecurityApplicationGroupIdentifier: "my group identifier")
let archiveURL = documentsDirectory?.appendingPathComponent("rules.json")
let encoder = JSONEncoder()
if let dataToSave = try? encoder.encode(rules) {
do {
try dataToSave.write(to: archiveURL!)
} catch {
// TODO: ("Error: Can't save Counters")
return;
}
}
}
private func loadRules() -> URL? {
let documentFolder = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "my group identifier")
guard let jsonURL = documentFolder?.appendingPathComponent("rules.json") else {
return nil
}
return jsonURL
}
}
Thankful for any help

Related

Swift after json parsing variables are assigned to their initial values

I'm new to swift i am sorry if this is a stupid question
I am trying to expand my knowledge in macOS development and i am trying out new things
i am parsing a json file from an url
it works fine in the do{}catch{} brackets however, i want to use what i get from the json data in other parts of the program.
i created some variables to store the values.
However, they go back to their initial value once the do{}catch{} execution is done
how can i store the values I got
#IBAction func buttonPressed(_ sender: Any) {
var summonerNameGlobal: String = ""
var summonerIdGlobal: String = ""
var summonerPuuidGlobal: String = ""
var summonerAccountIdGlobal: String = ""
let jsonString = "https://na1.api.riotgames.com/lol/summoner/v4/summoners/by-name/john?api_key=\(apiKey)"
guard let url = URL(string: jsonString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {return}
DispatchQueue.main.async {
do {
let summoner = try JSONDecoder().decode(SummonerInfo.self, from: data)
self.summonerIdLabel.stringValue = summoner.id
summonerNameGlobal = summoner.name
summonerIdGlobal = summoner.id
summonerAccountIdGlobal = summoner.accountId
summonerPuuidGlobal = summoner.puuid
} catch {
print(error)
}
}
}.resume()
print(summonerNameGlobal)
print(summonerPuuidGlobal)
print(summonerIdGlobal)
print(summonerAccountIdGlobal)
}
They are not going to default again but you are checking them before they are being set ... because async function take some time to get response from server but your print statements run immediately
What you can do is to check values once they are set
func callApi(completion: #escaping (SummonerInfo?)->Void){
let jsonString = "https://na1.api.riotgames.com/lol/summoner/v4/summoners/by-name/john?api_key=\(apiKey)"
guard let url = URL(string: jsonString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {return}
DispatchQueue.main.async {
do {
let summoner = try JSONDecoder().decode(SummonerInfo.self, from: data)
completion(summoner)
} catch {
completion(nil)
print(error)
}
}
}.resume()
}
#IBAction func buttonPressed(_ sender: Any) {
callApi { [weak self] info in
if let getInfo = info {
print(getInfo.name)
print(getInfo.id)
print(getInfo.accountId)
print(getInfo.puuid)
} else {
print("data is nil")
}
}
}

How to add or delete rule of safari content block list at run time

I am using in my project safari content blocker extension. when i set the rule in blockerList.json file statically and run the project every thing is working fine. Now i want to set my rule dynamically using the technic as it describes in below.
Guys please help me out to set the rule dynamically at run time.
I try this but i am getting an error when
load from viewcontroller class
fileprivate func saveRuleFile() {
let ruleList = [["trigger":["url-filter": ".*"],"action":["type": "block"]]]
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let encoded = try? encoder.encode(ruleList) {
let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.****.***")
print("sharedContainerURL = \(String(describing: sharedContainerURL))")
if let json = String(data: encoded, encoding: .utf8) {
print(json)
}
if let destinationURL = sharedContainerURL?.appendingPathComponent("Rules.json") {
do {
try encoded.write(to: destinationURL)
} catch {
print (error)
}
}
}
}
And write this in ContentBlockerRequestHandler class
func beginRequest(with context: NSExtensionContext) {
let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.****.***")
let sourceURL = sharedContainerURL?.appendingPathComponent("Rules.json")
let ruleAttachment = NSItemProvider(contentsOf: sourceURL)
let item = NSExtensionItem()
item.attachments = ([ruleAttachment] as! [NSItemProvider])
context.completeRequest(returningItems: [item], completionHandler: nil)
}
i try to load using
SFContentBlockerManager.reloadContentBlocker(withIdentifier: "com.app.*****", completionHandler: {(error) in
if error != nil{
print("error: \(error.debugDescription)")
}
})
when try to execute 3rd number block at run time i'm getting an error. But i go to the file path and checked the json is absolutely fine, its a valid json there.
Error Domain=WKErrorDomain Code=2 "(null)" UserInfo={NSHelpAnchor=Rule list compilation failed: Failed to parse the JSON String.}
Try to use JSONSerialization. It work great for me :)
fileprivate func saveRuleFile() {
let ruleList = [["trigger":["url-filter": ".*"],"action":["type": "block"]]]
let jsonAsData = try! JSONSerialization.data(withJSONObject: ruleList)
let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.****.***")
print("sharedContainerURL = \(String(describing: sharedContainerURL))")
if let destinationURL = sharedContainerURL?.appendingPathComponent("Rules.json") {
do {
try jsonAsData.write(to: destinationURL)
} catch {
print (error)
}
}
}

View pdf documents

I have a table with the names of pdf documents. Previously, there were 3 documents and each one has its own ViewController. How can I make it so that with hundreds of documents, I would select one from the table and show it on the View, if I select another document, then on the same View show another document.
while I have such a function, where I substituted the name of the documents in each class and showed it in different representations. But now I need to display everything on one ViewController when selecting any document
import UIKit
import PDFKit
class pdfViewClass {
class func filePDfFunc(nameFile: String, formatFile:String,
nameView:PDFView)
{
if let path = Bundle.main.path(forResource: nameFile,
ofType:formatFile) {
if let pdfDocument = PDFDocument(url: URL(fileURLWithPath:
path)) {
nameView.autoScales = true
nameView.displayDirection = .vertical
nameView.document = pdfDocument
}
}
}
}
You can use Native Apple UIDocumentInteractionController for viewing PDF file.
Create a function like below for View PDF
func viewPdf(urlPath: String, screenTitle: String) {
// open pdf for booking id
guard let url = urlPath.toUrl else {
print("Please pass valid url")
return
}
self.downloadPdf(fileURL: url, screenTitle: screenTitle) { localPdf in
if let url = localPdf {
DispatchQueue.main.sync {
self.openDocument(atURL: url, screenTitle: screenTitle)
}
}
}
}
Function for download PDF
// method for download pdf file
func downloadPdf(fileURL: URL, screenTitle: String, complition: #escaping ((URL?) -> Void)) {
// Create destination URL
if let documentsUrl: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let destinationFileUrl = documentsUrl.appendingPathComponent("\(screenTitle).pdf")
if FileManager.default.fileExists(atPath: destinationFileUrl.path) {
try? FileManager.default.removeItem(at: destinationFileUrl)
}
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url: fileURL)
let task = session.downloadTask(with: request) { tempLocalUrl, response, error in
if let tempLocalUrl = tempLocalUrl, error == nil {
// Success
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("Successfully downloaded. Status code: \(statusCode)")
}
do {
try FileManager.default.copyItem(at: tempLocalUrl, to: destinationFileUrl)
complition(destinationFileUrl)
} catch let writeError {
print("Error creating a file \(destinationFileUrl) : \(writeError)")
}
} else {
print("Error took place while downloading a file. Error description: \(error?.localizedDescription ?? "N/A")")
}
}
task.resume()
} else {
complition(nil)
}
}
Function for open documents
func openDocument(atURL url: URL, screenTitle: String) {
self.documentInteractionController.url = url
self.documentInteractionController.name = screenTitle
self.documentInteractionController.delegate = self
self.documentInteractionController.presentPreview(animated: true)
}
On tap of tableView pass the specific index URL
viewPdf(urlPath: "http://www.africau.edu/images/default/sample.pdf", screenTitle: "Tesing Document")
You can do it using WKWebView easily. Use WKWebView to load your pdf doc.

Data tasks outside ViewController

I'm gonna start with I'm currently learning swift + iOS so I'm by no means an experienced developer or one for that matter.
My goal is to separate any network calls that are currently done in my view controller to a dedicated class outside of it.
In this view controller i have a IBAction with the following code inside of it:
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: loginRequest) {
(data, response, error) in
guard let _ = response, let data = data else {return}
do {
let apiData = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
let token = apiData.data?.token
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
} catch let decodeError as NSError {
print("Decoder error: \(decodeError.localizedDescription)\n")
return
}
}.resume()
What I want, or I think I want to achieve is something like this:
let apiData = "somehow get it from outside"
Then when apiData has info stored in it, execute this next bit of code:
let token = apiData.data?.token
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
How would I achieve this? Thank you.
You can try
class API {
static func userLoginWith(email:String,password:String,completion:#escaping(_ token:String?) -> ()) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: loginRequest) {
(data, response, error) in
guard let _ = response, let data = data else { completion(nil) ; return }
do {
let apiData = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
completion(apiData.data?.token)
} catch {
print("Decoder error: ",error")
completion(nil)
}
}.resume()
}
}
Inside the VC
API.userLoginWith(email:<##>,password:<##>) { (token) in
if let token = token {
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
}
}

Read and write permission for user selected folder in Mac OS app?

I am developing MAC OS app which have functionality to create file on the behalf of your. First user select folder for storing file (One time at start of app) and then user can select type and name of the file user want to create on selected folder (Folder selected on start of the app) using apple script. I am able to create file when i add below temporary-exception in entitlement file but its not able to app apple review team but works in sandboxing.
Guideline 2.4.5(i) - Performance
We've determined that one or more temporary entitlement exceptions requested for this app are not appropriate and will not be granted:
com.apple.security.temporary-exception.files.home-relative-path.read-write
/FolderName/
I found :
Enabling App Sandbox - Allows apps to write executable files.
And
Enabling User-Selected File Access - Xcode provides a pop-up menu, in the Summary tab of the target editor, with choices to enable read-only or read/write access to files and folders that the user explicitly selects. When you enable user-selected file access, you gain programmatic access to files and folders that the user opens using an NSOpenPanel object, and files the user saves using an NSSavePanel object.
Using below code for creating file :
let str = "Super long string here"
let filename = getDocumentsDirectory().appendingPathComponent("/xyz/output.txt")
do {
try str.write(to: filename, atomically: true, encoding: String.Encoding.utf8)
} catch {
// failed to write file – bad permissions, bad filename, missing permissions, or more likely it can't be converted to the encoding
}
Also tried adding com.apple.security.files.user-selected.read-write in entitlement file for an NSOpenPanel object :
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
Is there any way to get pass apple review team to approve Mac App with read and write permission to user selected folder ?
Here is my Answer
How to do implement and persist Read and write permission of user selected folder in Mac OS app?
GitHub Example Project link
First :
Add user-selected and bookmarks.app permissions in entitlement file :
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
Then i created class for all bookmark related function required for storeing, loading ... etc bookmarks app.
import Foundation
import Cocoa
var bookmarks = [URL: Data]()
func openFolderSelection() -> URL?
{
let openPanel = NSOpenPanel()
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canCreateDirectories = true
openPanel.canChooseFiles = false
openPanel.begin
{ (result) -> Void in
if result.rawValue == NSApplication.ModalResponse.OK.rawValue
{
let url = openPanel.url
storeFolderInBookmark(url: url!)
}
}
return openPanel.url
}
func saveBookmarksData()
{
let path = getBookmarkPath()
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: path)
}
func storeFolderInBookmark(url: URL)
{
do
{
let data = try url.bookmarkData(options: NSURL.BookmarkCreationOptions.withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
bookmarks[url] = data
}
catch
{
Swift.print ("Error storing bookmarks")
}
}
func getBookmarkPath() -> String
{
var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as URL
url = url.appendingPathComponent("Bookmarks.dict")
return url.path
}
func loadBookmarks()
{
let path = getBookmarkPath()
bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: path) as! [URL: Data]
for bookmark in bookmarks
{
restoreBookmark(bookmark)
}
}
func restoreBookmark(_ bookmark: (key: URL, value: Data))
{
let restoredUrl: URL?
var isStale = false
Swift.print ("Restoring \(bookmark.key)")
do
{
restoredUrl = try URL.init(resolvingBookmarkData: bookmark.value, options: NSURL.BookmarkResolutionOptions.withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
}
catch
{
Swift.print ("Error restoring bookmarks")
restoredUrl = nil
}
if let url = restoredUrl
{
if isStale
{
Swift.print ("URL is stale")
}
else
{
if !url.startAccessingSecurityScopedResource()
{
Swift.print ("Couldn't access: \(url.path)")
}
}
}
}
Then open folder selection using NSOpenPanel so the user can select which folders to give you access to. The NSOpenPanel must be stored as a bookmark and saved to disk. Then your app will have the same level of access as it did when the user selected the folder.
To open NSOpenPanel :
let selectedURL = openFolderSelection()
saveBookmarksData()
and to load existing bookmark after app close :
loadBookmarks()
Thats it.
I Hope it will help someone.
Add user-selected and bookmarks.app permissions in entitlement file :
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
Then open folder selection using NSOpenPanel so the user can select which folders to give you access to. The NSOpenPanel must be stored as a bookmark and saved to disk. Then your app will have the same level of access as it did when the user selected the folder.
Since 'unarchiveObject(withFile:)' was deprecated in macOS 10.14, created a new answer in case someone has a similar question.
So after setting this in plist,
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
Create a BookMark class like below:
import Foundation
#objcMembers final class BookMarks: NSObject, NSSecureCoding {
struct Keys {
static let data = "data"
}
var data: [URL:Data] = [URL: Data]()
static var supportsSecureCoding: Bool = true
required init?(coder: NSCoder) {
self.data = coder.decodeObject(of: [NSDictionary.self, NSData.self, NSURL.self], forKey: Keys.data) as? [URL: Data] ?? [:]
}
required init(data: [URL: Data]) {
self.data = data
}
func encode(with coder: NSCoder) {
coder.encode(data, forKey: Keys.data)
}
func store(url: URL) {
do {
let bookmark = try url.bookmarkData(options: NSURL.BookmarkCreationOptions.withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
data[url] = bookmark
} catch {
print("Error storing bookmarks")
}
}
func dump() {
let path = Self.path()
do {
try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true).write(to: path)
} catch {
print("Error dumping bookmarks")
}
}
static func path() -> URL {
var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as URL
url = url.appendingPathComponent("Bookmarks.dict")
return url
}
static func restore() -> BookMarks? {
let path = Self.path()
let nsdata = NSData(contentsOf: path)
guard nsdata != nil else { return nil }
do {
let bookmarks = try NSKeyedUnarchiver.unarchivedObject(ofClass: Self.self, from: nsdata! as Data)
for bookmark in bookmarks?.data ?? [:] {
Self.restore(bookmark)
}
return bookmarks
} catch {
// print(error.localizedDescription)
print("Error loading bookmarks")
return nil
}
}
static func restore(_ bookmark: (key: URL, value: Data)) {
let restoredUrl: URL?
var isStale = false
print("Restoring \(bookmark.key)")
do {
restoredUrl = try URL.init(resolvingBookmarkData: bookmark.value, options: NSURL.BookmarkResolutionOptions.withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
} catch {
print("Error restoring bookmarks")
restoredUrl = nil
}
if let url = restoredUrl {
if isStale {
print("URL is stale")
} else {
if !url.startAccessingSecurityScopedResource() {
print("Couldn't access: \(url.path)")
}
}
}
}
}
Then using it:
loading
let bookmarks = BookMarks.restore() ?? BookMarks(data: [:])
adding
bookmarks.store(url: someUrl)
saving
bookmarks.dump()
Swift 5 with Xcode 14.2 - Jan-2023 :- below code works fine in my macOS app:
Keep below code in a class and follow instructions given after the code:
private static let BOOKMARK_KEY = "bookmark"
// Check permission is granted or not
public static func isPermissionGranted() -> Bool {
if let data = UserDefaults.standard.data(forKey: BOOKMARK_KEY) {
var bookmarkDataIsStale: ObjCBool = false
do {
let url = try (NSURL(resolvingBookmarkData: data, options: [.withoutUI, .withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &bookmarkDataIsStale) as URL)
if bookmarkDataIsStale.boolValue {
NSLog("WARNING stale security bookmark")
return false
}
return url.startAccessingSecurityScopedResource()
} catch {
print(error.localizedDescription)
return false
}
}
return false
} // isPermissionGranted
static func selectFolder(folderPicked: () -> Void) {
let folderChooserPoint = CGPoint(x: 0, y: 0)
let folderChooserSize = CGSize(width: 450, height: 400)
let folderChooserRectangle = CGRect(origin: folderChooserPoint, size: folderChooserSize)
let folderPicker = NSOpenPanel(contentRect: folderChooserRectangle, styleMask: .utilityWindow, backing: .buffered, defer: true)
let homePath = "/Users/\(NSUserName())"
folderPicker.directoryURL = NSURL.fileURL(withPath: homePath, isDirectory: true)
folderPicker.canChooseDirectories = true
folderPicker.canChooseFiles = false
folderPicker.allowsMultipleSelection = false
folderPicker.canDownloadUbiquitousContents = false
folderPicker.canResolveUbiquitousConflicts = false
folderPicker.begin { response in
if response == .OK {
let url = folderPicker.urls
NSLog("\(url)")
// Save Url Bookmark
if let mUrl = folderPicker.url {
storeFolderInBookmark(url: mUrl)
}
}
}
}
private static func storeFolderInBookmark(url: URL) { // mark 1
do {
let data = try url.bookmarkData(options: NSURL.BookmarkCreationOptions.withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
UserDefaults.standard.set(data, forKey: BOOKMARK_KEY)
} catch {
NSLog("Error storing bookmarks")
}
}
How to Use:
isPermissionGranted() - this function is to check user has granted directory permission or not. If it returns true then use directory/file operation read/write. If it returns false then use call selectFolder() function
selectFolder() - if isPermissionGranted() returns false then call this function to take permission from user. user will just need to click on as home directory will choose automatically.
storeFolderInBookmark() - Just keep it in the code you don't need to modify it, it will save url as bookmark for future use
Hope this will help & save lots of time. Thanks.
I found the best and working answer here - reusing security scoped bookmark
Super simple, easy to understand and does the job pretty well.
The solution was :-
var userDefault = NSUserDefaults.standardUserDefaults()
var folderPath: NSURL? {
didSet {
do {
let bookmark = try folderPath?.bookmarkDataWithOptions(.SecurityScopeAllowOnlyReadAccess, includingResourceValuesForKeys: nil, relativeToURL: nil)
userDefault.setObject(bookmark, forKey: "bookmark")
} catch let error as NSError {
print("Set Bookmark Fails: \(error.description)")
}
}
}
func applicationDidFinishLaunching(aNotification: NSNotification) {
if let bookmarkData = userDefault.objectForKey("bookmark") as? NSData {
do {
let url = try NSURL.init(byResolvingBookmarkData: bookmarkData, options: .WithoutUI, relativeToURL: nil, bookmarkDataIsStale: nil)
url.startAccessingSecurityScopedResource()
} catch let error as NSError {
print("Bookmark Access Fails: \(error.description)")
}
}
}
Updated to Swift 5 (Thanks Jay!)
var folderPath: URL? {
didSet {
do {
let bookmark = try folderPath?.bookmarkData(options: .securityScopeAllowOnlyReadAccess, includingResourceValuesForKeys: nil, relativeTo: nil)
UserDefaults.standard.set(bookmark, forKey: "bookmark")
} catch let error as NSError {
print("Set Bookmark Fails: \(error.description)")
}
}
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let bookmarkData = UserDefaults.standard.object(forKey: "bookmark") as? Data {
do {
var bookmarkIsStale = false
let url = try URL.init(resolvingBookmarkData: bookmarkData as Data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &bookmarkIsStale)
url.startAccessingSecurityScopedResource()
} catch let error as NSError {
print("Bookmark Access Fails: \(error.description)")
}
}
}