Fix theme-importing — use startAccessingSecurityScopedResource and stopAccessingSecurityScopedResource, which appear to be required now.

This commit is contained in:
Brent Simmons
2024-12-31 21:15:51 -08:00
parent f2e12f6b2e
commit b0b5ad018c
5 changed files with 57 additions and 38 deletions

View File

@@ -16,14 +16,14 @@ struct ArticleTheme: Equatable {
private static let defaultThemeName = NSLocalizedString("Default", comment: "Default") private static let defaultThemeName = NSLocalizedString("Default", comment: "Default")
private static let unknownValue = NSLocalizedString("Unknown", comment: "Unknown Value") private static let unknownValue = NSLocalizedString("Unknown", comment: "Unknown Value")
let path: String? let url: URL?
let template: String? let template: String?
let css: String? let css: String?
let isAppTheme: Bool let isAppTheme: Bool
var name: String { var name: String {
guard let path = path else { return Self.defaultThemeName } guard let url else { return Self.defaultThemeName }
return Self.themeNameForPath(path) return Self.themeNameForPath(url.path)
} }
var creatorHomePage: String { var creatorHomePage: String {
@@ -41,7 +41,7 @@ struct ArticleTheme: Equatable {
private let info: ArticleThemePlist? private let info: ArticleThemePlist?
init() { init() {
self.path = nil; self.url = nil
self.info = ArticleThemePlist(name: "Article Theme", themeIdentifier: "com.ranchero.netnewswire.theme.article", creatorHomePage: "https://netnewswire.com/", creatorName: "Ranchero Software", version: 1) self.info = ArticleThemePlist(name: "Article Theme", themeIdentifier: "com.ranchero.netnewswire.theme.article", creatorHomePage: "https://netnewswire.com/", creatorName: "Ranchero Software", version: 1)
let corePath = Bundle.main.path(forResource: "core", ofType: "css")! let corePath = Bundle.main.path(forResource: "core", ofType: "css")!
@@ -54,27 +54,33 @@ struct ArticleTheme: Equatable {
isAppTheme = true isAppTheme = true
} }
init(path: String, isAppTheme: Bool) throws { init(url: URL, isAppTheme: Bool) throws {
self.path = path
_ = url.startAccessingSecurityScopedResource()
defer {
url.stopAccessingSecurityScopedResource()
}
let infoPath = (path as NSString).appendingPathComponent("Info.plist") self.url = url
let data = try Data(contentsOf: URL(fileURLWithPath: infoPath))
self.info = try PropertyListDecoder().decode(ArticleThemePlist.self, from: data) let coreURL = Bundle.main.url(forResource: "core", withExtension: "css")!
let styleSheetURL = url.appendingPathComponent("stylesheet.css")
let corePath = Bundle.main.path(forResource: "core", ofType: "css")! if let stylesheetCSS = Self.stringAtPath(styleSheetURL.path) {
let stylesheetPath = (path as NSString).appendingPathComponent("stylesheet.css") self.css = Self.stringAtPath(coreURL.path)! + "\n" + stylesheetCSS
if let stylesheetCSS = Self.stringAtPath(stylesheetPath) {
self.css = Self.stringAtPath(corePath)! + "\n" + stylesheetCSS
} else { } else {
self.css = nil self.css = nil
} }
let templatePath = (path as NSString).appendingPathComponent("template.html") let templateURL = url.appendingPathComponent("template.html")
self.template = Self.stringAtPath(templatePath) self.template = Self.stringAtPath(templateURL.path)
self.isAppTheme = isAppTheme self.isAppTheme = isAppTheme
let infoURL = url.appendingPathComponent("Info.plist")
let data = try Data(contentsOf: infoURL)
self.info = try PropertyListDecoder().decode(ArticleThemePlist.self, from: data)
} }
static func stringAtPath(_ f: String) -> String? { static func stringAtPath(_ f: String) -> String? {
if !FileManager.default.fileExists(atPath: f) { if !FileManager.default.fileExists(atPath: f) {
return nil return nil

View File

@@ -97,20 +97,20 @@ final class ArticleThemesManager: NSObject, NSFilePresenter {
return ArticleTheme.defaultTheme return ArticleTheme.defaultTheme
} }
let path: String let url: URL
let isAppTheme: Bool let isAppTheme: Bool
if let appThemePath = Bundle.main.url(forResource: themeName, withExtension: ArticleTheme.nnwThemeSuffix)?.path { if let appThemeURL = Bundle.main.url(forResource: themeName, withExtension: ArticleTheme.nnwThemeSuffix) {
path = appThemePath url = appThemeURL
isAppTheme = true isAppTheme = true
} else if let installedPath = pathForThemeName(themeName, folder: folderPath) { } else if let installedPath = pathForThemeName(themeName, folder: folderPath) {
path = installedPath url = URL(fileURLWithPath: installedPath)
isAppTheme = false isAppTheme = false
} else { } else {
return nil return nil
} }
do { do {
return try ArticleTheme(path: path, isAppTheme: isAppTheme) return try ArticleTheme(url: url, isAppTheme: isAppTheme)
} catch { } catch {
NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error])
return nil return nil

View File

@@ -1251,7 +1251,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
func importTheme(filename: String) { func importTheme(filename: String) {
do { do {
try ArticleThemeImporter.importTheme(controller: rootSplitViewController, filename: filename) try ArticleThemeImporter.importTheme(controller: rootSplitViewController, url: URL(fileURLWithPath: filename))
} catch { } catch {
NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error]) NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error])
} }

View File

@@ -10,9 +10,9 @@ import UIKit
struct ArticleThemeImporter { struct ArticleThemeImporter {
static func importTheme(controller: UIViewController, filename: String) throws { static func importTheme(controller: UIViewController, url: URL) throws {
let theme = try ArticleTheme(path: filename, isAppTheme: false) let theme = try ArticleTheme(url: url, isAppTheme: false)
let localizedTitleText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text") let localizedTitleText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text")
let title = NSString.localizedStringWithFormat(localizedTitleText as NSString, theme.name, theme.creatorName) as String let title = NSString.localizedStringWithFormat(localizedTitleText as NSString, theme.name, theme.creatorName) as String
@@ -24,18 +24,24 @@ struct ArticleThemeImporter {
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
if let url = URL(string: theme.creatorHomePage) { if let websiteURL = URL(string: theme.creatorHomePage) {
let visitSiteTitle = NSLocalizedString("Show Website", comment: "Show Website") let visitSiteTitle = NSLocalizedString("Show Website", comment: "Show Website")
let visitSiteAction = UIAlertAction(title: visitSiteTitle, style: .default) { action in let visitSiteAction = UIAlertAction(title: visitSiteTitle, style: .default) { action in
UIApplication.shared.open(url) UIApplication.shared.open(websiteURL)
try? Self.importTheme(controller: controller, filename: filename) try? Self.importTheme(controller: controller, url: url)
} }
alertController.addAction(visitSiteAction) alertController.addAction(visitSiteAction)
} }
func importTheme() { func importTheme() {
_ = url.startAccessingSecurityScopedResource()
defer {
url.stopAccessingSecurityScopedResource()
}
do { do {
try ArticleThemesManager.shared.importTheme(filename: filename) try ArticleThemesManager.shared.importTheme(filename: url.path)
confirmImportSuccess(controller: controller, themeName: theme.name) confirmImportSuccess(controller: controller, themeName: theme.name)
} catch { } catch {
controller.presentError(error) controller.presentError(error)
@@ -45,7 +51,7 @@ struct ArticleThemeImporter {
let installThemeTitle = NSLocalizedString("Install Theme", comment: "Install Theme") let installThemeTitle = NSLocalizedString("Install Theme", comment: "Install Theme")
let installThemeAction = UIAlertAction(title: installThemeTitle, style: .default) { action in let installThemeAction = UIAlertAction(title: installThemeTitle, style: .default) { action in
if ArticleThemesManager.shared.themeExists(filename: filename) { if ArticleThemesManager.shared.themeExists(filename: url.path) {
let title = NSLocalizedString("Duplicate Theme", comment: "Duplicate Theme") let title = NSLocalizedString("Duplicate Theme", comment: "Duplicate Theme")
let localizedMessageText = NSLocalizedString("The theme “%@” already exists. Overwrite it?", comment: "Overwrite theme") let localizedMessageText = NSLocalizedString("The theme “%@” already exists. Overwrite it?", comment: "Overwrite theme")
let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name) as String let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name) as String

View File

@@ -117,11 +117,18 @@ extension ArticleThemesTableViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else { return } guard let url = urls.first else { return }
do {
try ArticleThemeImporter.importTheme(controller: self, filename: url.standardizedFileURL.path) if url.startAccessingSecurityScopedResource() {
} catch {
NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) defer {
url.stopAccessingSecurityScopedResource()
}
do {
try ArticleThemeImporter.importTheme(controller: self, url: url)
} catch {
NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error])
}
} }
} }
} }