From 13dff956c2e0e57db7a3bf0224e49ac7d9b28db1 Mon Sep 17 00:00:00 2001 From: Eitot Date: Tue, 7 Sep 2021 06:33:15 +0200 Subject: [PATCH 1/3] Enable sandboxing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note: The system automatically migrates Vienna's library files to a sandbox container on launching Vienna with sandboxing enabled. This can be reversed by using the provided shell script, e.g. for development purposes or for downgrading to an earlier version of Vienna. The container-migration.plist file specifies the old and new locations for the migration. It should cover all of Vienna's directories and files, so that the user ideally ends up with a complete sandbox container. Some system-defined locations have to be changed to avoid duplication. For example, Apple moved the cookies storage from ~/Library/Cookies to ~/Library/HTTPStorages starting with macOS 11/Safari 14. Within sandbox containers however, ~/Library/Cookies is used. The automatic migration does not overwrite files. Therefore, a migration of ~/Library/HTTPStorages is attempted first. If that attempt is successful then the migration of ~/Library/Cookies should (silently) fail; otherwise ~/Library/Cookies is migrated instead. User preferences in ~/Library/Preferences are migrated automatically. User scripts are migrated from ~/Library/Scripts/Applications/Vienna to ~/Library/Application Scripts/ and a symlink is left at the former location; this also happens automatically. The shell script uses ditto to copy the directories. Ditto will merge directories rather than overwrite them, if the destination directory exists. It will, however, overwrite individual files. --- Vienna Tests/DirectoryMonitorTests.swift | 14 +- Vienna Tests/ExportTests.swift | 31 ++-- .../NSFileManagerExtensionTests.swift | 9 +- Vienna.xcodeproj/project.pbxproj | 20 ++- ...t.entitlements => Deployment.entitlements} | 8 + ....entitlements => Development.entitlements} | 8 + Vienna/Info.plist | 2 +- Vienna/Resources/container-migration.plist | 39 +++++ .../SharedSupport/undo-container-migration.sh | 155 ++++++++++++++++++ .../Sources/Preferences window/Preferences.m | 4 +- Vienna/Sources/Shared/NSFileManager+Paths.h | 10 +- Vienna/Sources/Shared/NSFileManager+Paths.m | 18 +- 12 files changed, 265 insertions(+), 53 deletions(-) rename Vienna/{ViennaDeployment.entitlements => Deployment.entitlements} (74%) rename Vienna/{Vienna.entitlements => Development.entitlements} (75%) create mode 100644 Vienna/Resources/container-migration.plist create mode 100755 Vienna/SharedSupport/undo-container-migration.sh diff --git a/Vienna Tests/DirectoryMonitorTests.swift b/Vienna Tests/DirectoryMonitorTests.swift index 960c51112a..2be1a93223 100644 --- a/Vienna Tests/DirectoryMonitorTests.swift +++ b/Vienna Tests/DirectoryMonitorTests.swift @@ -44,31 +44,31 @@ class DirectoryMonitorTests: XCTestCase { var testExpectation: XCTestExpectation? - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() // Set the test expectation. testExpectation = expectation(description: "The monitor's event handler is called") // Create the temp directory. - XCTAssertNoThrow(try fileManager.createDirectory(at: tempDirectory, withIntermediateDirectories: false)) + tempDirectory = try fileManager.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: fileManager.applicationScriptsDirectory, create: true) // Create the monitor. monitor = DirectoryMonitor(directories: [tempDirectory]) } - override func tearDown() { + override func tearDownWithError() throws { // Deinitialize the monitor and reset the failsafe. monitor = nil hasHandlerBeenCalled = false // Delete the temp directory. - XCTAssertNoThrow(try fileManager.removeItem(at: tempDirectory)) + try fileManager.removeItem(at: tempDirectory) // Unset the test expectation. testExpectation = nil - super.tearDown() + try super.tearDownWithError() } // MARK: Test methods @@ -167,7 +167,7 @@ class DirectoryMonitorTests: XCTestCase { let fileManager = FileManager.default - let tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString, isDirectory: true) + var tempDirectory: URL! var tempDirectoryItemCount: Int { return (try? fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil))?.count ?? -1 diff --git a/Vienna Tests/ExportTests.swift b/Vienna Tests/ExportTests.swift index a64c77883a..95ca64cab8 100644 --- a/Vienna Tests/ExportTests.swift +++ b/Vienna Tests/ExportTests.swift @@ -21,6 +21,21 @@ import XCTest class ExportTests: XCTestCase { + var tempURL: URL! + + override func setUpWithError() throws { + try super.setUpWithError() + + let downloadsDirectory = FileManager.default.downloadsDirectory + tempURL = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: downloadsDirectory, create: true) + } + + override func tearDownWithError() throws { + try FileManager.default.removeItem(at: tempURL) + + try super.tearDownWithError() + } + /// Test helper method to return an array of folders for export func foldersArray() -> [Any] { guard let database = Database.shared else { @@ -35,24 +50,16 @@ class ExportTests: XCTestCase { func testExportWithoutGroups() { // Test exporting feeds to opml file without groups let folders = self.foldersArray() - guard let tmpUrl = URL(string: "/tmp/vienna-test-nogroups.opml") else { - XCTAssertTrue(false) - fatalError("cannot happen") - } - - let countExported = Export.export(toFile: tmpUrl.absoluteString, from: folders, in: nil, withGroups: false) + let tmpUrl = tempURL.appendingPathComponent("vienna-test-nogroups.opml", isDirectory: false) + let countExported = Export.export(toFile: tmpUrl.path, from: folders, in: nil, withGroups: false) XCTAssertGreaterThan(countExported, 0, "Pass") } func testExportWithGroups() { // Test exporting feeds to opml file without groups let folders = self.foldersArray() - guard let tmpUrl = URL(string: "/tmp/vienna-test-groups.opml") else { - XCTAssertTrue(false) - fatalError("cannot happen") - } - - let countExported = Export.export(toFile: tmpUrl.absoluteString, from: folders, in: nil, withGroups: true) + let tmpUrl = tempURL.appendingPathComponent("vienna-test-groups.opml", isDirectory: false) + let countExported = Export.export(toFile: tmpUrl.path, from: folders, in: nil, withGroups: true) XCTAssertGreaterThan(countExported, 0, "Pass") } diff --git a/Vienna Tests/NSFileManagerExtensionTests.swift b/Vienna Tests/NSFileManagerExtensionTests.swift index 64b6bec4bd..e4e5d241b0 100644 --- a/Vienna Tests/NSFileManagerExtensionTests.swift +++ b/Vienna Tests/NSFileManagerExtensionTests.swift @@ -26,11 +26,14 @@ class NSFileManagerExtensionTests: XCTestCase { func testApplicationScriptsPath() throws { let result = FileManager.default.applicationScriptsDirectory - let fullPath = "\(homePath)/Library/Scripts/Applications/Vienna" + let userDirectory = try FileManager.default.url(for: .userDirectory, in: .localDomainMask, appropriateFor: nil, create: false) + .appendingPathComponent(NSUserName(), isDirectory: true) + let bundleID = try XCTUnwrap(bundleID) + let fullPath = "\(userDirectory.path)/Library/Application Scripts/\(bundleID)" XCTAssertEqual(result.path, fullPath) } - func testApplicationSupportPath() throws { + func testApplicationSupportPath() { let result = FileManager.default.applicationSupportDirectory let fullPath = "\(homePath)/Library/Application Support/Vienna" XCTAssertEqual(result.path, fullPath) @@ -43,7 +46,7 @@ class NSFileManagerExtensionTests: XCTestCase { XCTAssertEqual(result.path, fullPath) } - func testDownloadsPath() throws { + func testDownloadsPath() { let result = FileManager.default.downloadsDirectory let fullPath = "\(homePath)/Downloads" XCTAssertEqual(result.path, fullPath) diff --git a/Vienna.xcodeproj/project.pbxproj b/Vienna.xcodeproj/project.pbxproj index e4b9108613..d4afa3cc91 100644 --- a/Vienna.xcodeproj/project.pbxproj +++ b/Vienna.xcodeproj/project.pbxproj @@ -160,6 +160,7 @@ F6164C591E32A6660086261C /* DisclosureView.m in Sources */ = {isa = PBXBuildFile; fileRef = F6164C581E32A6660086261C /* DisclosureView.m */; }; F61CEA661F039E57009C878E /* ButtonToolbarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61CEA651F039E56009C878E /* ButtonToolbarItem.swift */; }; F61CEA681F03F277009C878E /* PlugInToolbarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61CEA671F03F277009C878E /* PlugInToolbarItem.swift */; }; + F6209C4423D3B87C00D5537D /* container-migration.plist in Resources */ = {isa = PBXBuildFile; fileRef = F6209C4323D3B87C00D5537D /* container-migration.plist */; }; F6209C5A23D3FFA700D5537D /* Vienna.sdef in Resources */ = {isa = PBXBuildFile; fileRef = F6209C5923D3FFA700D5537D /* Vienna.sdef */; }; F6257FF51E28485F0035E43C /* Downloads.xib in Resources */ = {isa = PBXBuildFile; fileRef = F6257FF71E28485F0035E43C /* Downloads.xib */; }; F625801C1E2853B90035E43C /* ActivityViewer.xib in Resources */ = {isa = PBXBuildFile; fileRef = F625801E1E2853B90035E43C /* ActivityViewer.xib */; }; @@ -212,6 +213,7 @@ F6B355DB256025D3008CA1ED /* PreferenceTabViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B355DA256025D3008CA1ED /* PreferenceTabViewItem.swift */; }; F6B3561525616313008CA1ED /* DownloadItem.m in Sources */ = {isa = PBXBuildFile; fileRef = F6B3561425616313008CA1ED /* DownloadItem.m */; }; F6B7BC3624A2A9A40051D76F /* FMDB in Frameworks */ = {isa = PBXBuildFile; productRef = F6B7BC3524A2A9A40051D76F /* FMDB */; }; + F6C9B03A26E6F3E0003C1058 /* undo-container-migration.sh in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = F668C10E26886CB6009AD505 /* undo-container-migration.sh */; }; F6C9DA70271BB3BB00FC3027 /* RSSFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = F6C9DA6F271BB3BB00FC3027 /* RSSFeed.m */; }; F6C9DA73271BB55000FC3027 /* AtomFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = F6C9DA72271BB55000FC3027 /* AtomFeed.m */; }; F6CE47131E7E3DCB0045EAD7 /* ActivityItem.m in Sources */ = {isa = PBXBuildFile; fileRef = F6CE47121E7E3DCB0045EAD7 /* ActivityItem.m */; }; @@ -260,6 +262,7 @@ files = ( B27CD00C1100E728001F3C83 /* Plugins in Copy Shared Support Files */, AA67F353089728AD008BBC37 /* Styles in Copy Shared Support Files */, + F6C9B03A26E6F3E0003C1058 /* undo-container-migration.sh in Copy Shared Support Files */, ); name = "Copy Shared Support Files"; runOnlyForDeploymentPostprocessing = 0; @@ -349,8 +352,8 @@ 3A7BD0DB1989AC7700E9444B /* VNAVerticallyCenteredTextFieldCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VNAVerticallyCenteredTextFieldCell.h; sourceTree = ""; }; 3A7BD0DC1989AC7700E9444B /* VNAVerticallyCenteredTextFieldCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VNAVerticallyCenteredTextFieldCell.m; sourceTree = ""; }; 3A8D9AE225A9DA4B0016F30F /* ArticleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleTests.swift; sourceTree = ""; }; - 3A932D8823BB999A009B8061 /* Vienna.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Vienna.entitlements; sourceTree = ""; }; - 3A932D8923BB999A009B8061 /* ViennaDeployment.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = ViennaDeployment.entitlements; sourceTree = ""; }; + 3A932D8823BB999A009B8061 /* Development.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Development.entitlements; sourceTree = ""; }; + 3A932D8923BB999A009B8061 /* Deployment.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Deployment.entitlements; sourceTree = ""; }; 3AC411A426BBFDFD004A8700 /* WebKitArticleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebKitArticleView.swift; sourceTree = ""; }; 3ADBA70C23DDAFCA00156722 /* Notarize.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = Notarize.sh; sourceTree = ""; }; 430C4ADB1661753F0079C9FC /* autorevision.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = autorevision.h; sourceTree = ""; }; @@ -469,6 +472,7 @@ F6164C581E32A6660086261C /* DisclosureView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DisclosureView.m; sourceTree = ""; }; F61CEA651F039E56009C878E /* ButtonToolbarItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ButtonToolbarItem.swift; sourceTree = ""; }; F61CEA671F03F277009C878E /* PlugInToolbarItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = PlugInToolbarItem.swift; sourceTree = ""; }; + F6209C4323D3B87C00D5537D /* container-migration.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "container-migration.plist"; sourceTree = ""; }; F6209C5923D3FFA700D5537D /* Vienna.sdef */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; lineEnding = 0; path = Vienna.sdef; sourceTree = ""; usesTabs = 1; }; F6232EF72B4B402F00D01D2A /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Predicates.strings; sourceTree = ""; }; F6257FF61E28485F0035E43C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/Downloads.xib; sourceTree = ""; }; @@ -538,6 +542,7 @@ F64C2CCA1E83825D00ED4E04 /* DateFormatterExtension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DateFormatterExtension.h; sourceTree = ""; }; F64C2CCB1E83825D00ED4E04 /* DateFormatterExtension.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DateFormatterExtension.m; sourceTree = ""; }; F65F2313294E4B5200605F06 /* SeparatorPredicateEditorRowTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorPredicateEditorRowTemplate.swift; sourceTree = ""; }; + F668C10E26886CB6009AD505 /* undo-container-migration.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; lineEnding = 0; path = "undo-container-migration.sh"; sourceTree = ""; }; F66A7E542A019006002F5D5E /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Predicates.strings; sourceTree = ""; }; F672876624A2D20A0043432F /* UpdatePreferencesViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UpdatePreferencesViewController.h; sourceTree = ""; }; F672876724A2D2170043432F /* UpdatePreferencesViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UpdatePreferencesViewController.m; sourceTree = ""; }; @@ -963,6 +968,7 @@ F63DA1741F1CF04F007CBED4 /* Lists */, F63DA1721F1CEFC3007CBED4 /* Strings */, 03F33D951DF2A2F900B04FAF /* Assets.xcassets */, + F6209C4323D3B87C00D5537D /* container-migration.plist */, F6209C5923D3FFA700D5537D /* Vienna.sdef */, ); path = Resources; @@ -993,6 +999,7 @@ children = ( B27CCFFD1100E728001F3C83 /* Plugins */, AA67F32E089727FB008BBC37 /* Styles */, + F668C10E26886CB6009AD505 /* undo-container-migration.sh */, ); path = SharedSupport; sourceTree = ""; @@ -1123,8 +1130,8 @@ F63DA16E1F1CEC69007CBED4 /* Resources */, F63DA1751F1CF182007CBED4 /* SharedSupport */, 430C4AE0166175C20079C9FC /* Info.plist */, - 3A932D8823BB999A009B8061 /* Vienna.entitlements */, - 3A932D8923BB999A009B8061 /* ViennaDeployment.entitlements */, + 3A932D8823BB999A009B8061 /* Development.entitlements */, + 3A932D8923BB999A009B8061 /* Deployment.entitlements */, 3A6CC18623BBA49D0084ABEE /* Codesigning.xcconfig */, ); path = Vienna; @@ -1540,6 +1547,7 @@ F6D572B71E26AAA100CDA909 /* RSSFeed.xib in Resources */, F6832F631F10C4C9007920D4 /* ExportAccessoryViewController.xib in Resources */, AACAEA3E0954E71100ACD502 /* DemoFeeds.plist in Resources */, + F6209C4423D3B87C00D5537D /* container-migration.plist in Resources */, F65D6D711E74619E00A30974 /* Credits.rtf in Resources */, F685D24C292460210097C0D3 /* Predicates.strings in Resources */, F6A7DE4F1E471A7F0017BE5E /* Vienna.help in Resources */, @@ -2350,7 +2358,7 @@ APPLY_RULES_IN_COPY_FILES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = AppKit; - CODE_SIGN_ENTITLEMENTS = Vienna/Vienna.entitlements; + CODE_SIGN_ENTITLEMENTS = Vienna/Development.entitlements; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -2398,7 +2406,7 @@ APPLY_RULES_IN_COPY_FILES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = AppKit; - CODE_SIGN_ENTITLEMENTS = Vienna/ViennaDeployment.entitlements; + CODE_SIGN_ENTITLEMENTS = Vienna/Deployment.entitlements; COMBINE_HIDPI_IMAGES = YES; DEPLOYMENT_POSTPROCESSING = YES; ENABLE_HARDENED_RUNTIME = YES; diff --git a/Vienna/ViennaDeployment.entitlements b/Vienna/Deployment.entitlements similarity index 74% rename from Vienna/ViennaDeployment.entitlements rename to Vienna/Deployment.entitlements index 5d629032b2..8cbe34b981 100644 --- a/Vienna/ViennaDeployment.entitlements +++ b/Vienna/Deployment.entitlements @@ -2,10 +2,18 @@ + com.apple.security.app-sandbox + com.apple.security.automation.apple-events com.apple.security.files.bookmarks.app-scope + com.apple.security.files.downloads.read-write + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + com.apple.security.temporary-exception.mach-lookup.global-name $(PRODUCT_BUNDLE_IDENTIFIER)-spks diff --git a/Vienna/Vienna.entitlements b/Vienna/Development.entitlements similarity index 75% rename from Vienna/Vienna.entitlements rename to Vienna/Development.entitlements index e1cf5d5af4..0540f698bb 100644 --- a/Vienna/Vienna.entitlements +++ b/Vienna/Development.entitlements @@ -2,12 +2,20 @@ + com.apple.security.app-sandbox + com.apple.security.automation.apple-events com.apple.security.cs.disable-library-validation com.apple.security.files.bookmarks.app-scope + com.apple.security.files.downloads.read-write + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + com.apple.security.temporary-exception.mach-lookup.global-name $(PRODUCT_BUNDLE_IDENTIFIER)-spks diff --git a/Vienna/Info.plist b/Vienna/Info.plist index fa820940ef..eeda6a9e1c 100644 --- a/Vienna/Info.plist +++ b/Vienna/Info.plist @@ -141,7 +141,7 @@ SUEnableAutomaticChecks SUEnableInstallerLauncherService - + SUPublicEDKey nPwwT+poO5Kmi1NZkE5OveKB8lvQVY20N22E8jgxLCg= UTExportedTypeDeclarations diff --git a/Vienna/Resources/container-migration.plist b/Vienna/Resources/container-migration.plist new file mode 100644 index 0000000000..5cf0891e71 --- /dev/null +++ b/Vienna/Resources/container-migration.plist @@ -0,0 +1,39 @@ + + + + + MigrateScriptsForApplication + Vienna + Move + + + ${Caches}/${BundleId}/WebKit + ${Caches}/WebKit + + ${Caches}/${BundleId} + + ${ApplicationSupport}/Vienna/Sources + ${Caches}/${BundleId}/Sources + + ${ApplicationSupport}/Vienna + ${Library}/HTTPStorages/${BundleId} + + ${Library}/HTTPStorages/${BundleId}.binarycookies + ${Library}/Cookies/Cookies.binarycookies + + + ${Library}/Cookies/${BundleId}.binarycookies + ${Library}/Cookies/Cookies.binarycookies + + ${Library}/Saved Application State/${BundleId}.savedState + + ${Library}/WebKit/${BundleId} + ${Library}/WebKit + + + ${Library}/WebKit/Databases/___IndexedDB/${BundleId} + ${Library}/WebKit/Databases/___IndexedDB + + + + diff --git a/Vienna/SharedSupport/undo-container-migration.sh b/Vienna/SharedSupport/undo-container-migration.sh new file mode 100755 index 0000000000..80f111da61 --- /dev/null +++ b/Vienna/SharedSupport/undo-container-migration.sh @@ -0,0 +1,155 @@ +#!/bin/sh +# +# undo-container-migration.sh +# Vienna +# +# Copyright 2021-2024 Eitot +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +LIBRARY="$HOME/Library" +BUNDLE_ID='uk.co.opencommunity.vienna2' +CONTAINER="$LIBRARY/Containers/$BUNDLE_ID" +CONTAINER_LIBRARY="$CONTAINER/Data/Library" + +set -e + +if [ ! -d "$CONTAINER" ] +then + printf 'No sandbox container found for Vienna\n' >&2 + exit 1 +fi + +if [ -n "$(killall -s Vienna 2>/dev/null)" ] +then + printf 'Please quit Vienna and try again\n' >&2 + exit 1 +fi + +printf 'This script attempts to undo the container migration. Use this only to ' +printf 'revert to an older version of Vienna that does not support sandboxing. ' +printf 'Please make sure that Vienna is not opened during this process.\n' + +tput bold +printf 'Are you sure you want to do this? [y/N] ' +tput sgr0 +read -r +if ! printf '%s' "$REPLY" | grep -q -E '^[Yy]$' +then + exit 1 +fi + +# Configure ditto to use verbose mode and abort if encountering a fatal error. +alias ditto='ditto -v' +export DITTOABORT=1 + +# User scripts are located in ~/Library/Application Scripts/ instead +# of the sandbox container when sandboxing is enabled. Without sandboxing, they +# are located in ~/Library/Scripts/Applications/. A symlink was added +# at that location as a result of the automatic migration. +if [ -L "$LIBRARY/Scripts/Applications/Vienna" ] +then + unlink "$LIBRARY/Scripts/Applications/Vienna" \ + && printf 'Unlinked symlink %s\n' "$LIBRARY/Scripts/Applications/Vienna" +fi + +if [ -d "$LIBRARY/Application Scripts/$BUNDLE_ID" ] +then + ditto "$LIBRARY/Application Scripts/$BUNDLE_ID" \ + "$LIBRARY/Scripts/Applications/Vienna" \ + && rm -r "$LIBRARY/Application Scripts/$BUNDLE_ID" +fi + +if [ -d "$CONTAINER_LIBRARY/WebKit/Databases/___IndexedDB" ] +then + ditto "$CONTAINER_LIBRARY/WebKit/Databases/___IndexedDB" \ + "$LIBRARY/WebKit/Databases/___IndexedDB/$BUNDLE_ID" \ + && rm -r "$CONTAINER_LIBRARY/WebKit/Databases/___IndexedDB" +fi + +if [ -d "$CONTAINER_LIBRARY/WebKit" ] +then + ditto "$CONTAINER_LIBRARY/WebKit" "$LIBRARY/WebKit/$BUNDLE_ID" +fi + +if [ -d "$CONTAINER_LIBRARY/Saved Application State/$BUNDLE_ID.savedState" ] +then + ditto "$CONTAINER_LIBRARY/Saved Application State/$BUNDLE_ID.savedState" \ + "$LIBRARY/Saved Application State/$BUNDLE_ID.savedState" +fi + +# Preferences are not explicitely declared in the migration manifest file; the +# behaviour is implicit. +if [ -f "$CONTAINER_LIBRARY/Preferences/$BUNDLE_ID.plist" ] +then + ditto "$CONTAINER_LIBRARY/Preferences/$BUNDLE_ID.plist" \ + "$LIBRARY/Preferences/$BUNDLE_ID.plist" +fi + +# The location of cookies files of non-sandboxed applications changed from +# ~/Library/Cookies to ~/Library/HTTPStorages as of macOS 11 (and probably +# Safari 14). +MACOS_VERSION="$(sw_vers -productVersion)" +MACOS_MAJOR_VERSION="${MACOS_VERSION%%.*}" + +if [ "$MACOS_MAJOR_VERSION" -ge 11 ] || [ -d "$LIBRARY/HTTPStorages" ] +then + if [ -f "$CONTAINER_LIBRARY/Cookies/Cookies.binarycookies" ] + then + ditto "$CONTAINER_LIBRARY/Cookies/Cookies.binarycookies" \ + "$LIBRARY/HTTPStorages/$BUNDLE_ID.binarycookies" + fi +else + if [ -f "$CONTAINER_LIBRARY/Cookies/Cookies.binarycookies" ] + then + ditto "$CONTAINER_LIBRARY/Cookies/Cookies.binarycookies" \ + "$LIBRARY/Cookies/$BUNDLE_ID.binarycookies" + fi +fi + +if [ -d "$CONTAINER_LIBRARY/HTTPStorages/$BUNDLE_ID" ] +then + ditto "$CONTAINER_LIBRARY/HTTPStorages/$BUNDLE_ID" \ + "$LIBRARY/HTTPStorages/$BUNDLE_ID" +fi + +if [ -d "$CONTAINER_LIBRARY/Application Support/Vienna" ] +then + ditto "$CONTAINER_LIBRARY/Application Support/Vienna" \ + "$LIBRARY/Application Support/Vienna" +fi + +# The Sources subdirectory was moved from Library/Application Support to +# Library/Caches. +if [ -d "$CONTAINER_LIBRARY/Caches/$BUNDLE_ID/Sources" ] +then + ditto "$CONTAINER_LIBRARY/Caches/$BUNDLE_ID/Sources" \ + "$LIBRARY/Application Support/Vienna/Sources" \ + && rm -r "$CONTAINER_LIBRARY/Caches/$BUNDLE_ID/Sources" +fi + +# The WebKit subdirectory in Library/Caches used to be located within the app's +# caches directory (Library/Caches//WebKit). +if [ -d "$CONTAINER_LIBRARY/Caches/WebKit" ] +then + ditto "$CONTAINER_LIBRARY/Caches/WebKit" "$LIBRARY/Caches/$BUNDLE_ID/WebKit" +fi + +if [ -d "$CONTAINER_LIBRARY/Caches/$BUNDLE_ID" ] +then + ditto "$CONTAINER_LIBRARY/Caches/$BUNDLE_ID" "$LIBRARY/Caches/$BUNDLE_ID" +fi + +osascript -e "tell application \"Finder\" to delete POSIX file \"$CONTAINER\"" \ +1>/dev/null && printf 'Moved %s to Trash\n' "$CONTAINER" diff --git a/Vienna/Sources/Preferences window/Preferences.m b/Vienna/Sources/Preferences window/Preferences.m index ee2338175c..a6d2fe4631 100644 --- a/Vienna/Sources/Preferences window/Preferences.m +++ b/Vienna/Sources/Preferences window/Preferences.m @@ -116,8 +116,8 @@ -(instancetype)init // Application-specific folder locations defaultDatabase = [userPrefs stringForKey:MAPref_DefaultDatabase]; NSFileManager *fileManager = NSFileManager.defaultManager; - NSString *appSupportPath = fileManager.vna_applicationSupportDirectory.path; - feedSourcesFolder = [appSupportPath stringByAppendingPathComponent:MA_FeedSourcesFolder_Name]; + NSString *cachesPath = fileManager.vna_cachesDirectory.path; + feedSourcesFolder = [cachesPath stringByAppendingPathComponent:MA_FeedSourcesFolder_Name]; // Load those settings that we cache. foldersTreeSortMethod = [self integerForKey:MAPref_AutoSortFoldersTree]; diff --git a/Vienna/Sources/Shared/NSFileManager+Paths.h b/Vienna/Sources/Shared/NSFileManager+Paths.h index cb22953d90..ec1e43939f 100644 --- a/Vienna/Sources/Shared/NSFileManager+Paths.h +++ b/Vienna/Sources/Shared/NSFileManager+Paths.h @@ -23,17 +23,17 @@ NS_ASSUME_NONNULL_BEGIN @interface NSFileManager (Paths) -/// The scripts directory for the current user (Library/Application Scripts or -/// Library/Scripts/Applications depending on whether sandboxing is enabled). +/// The scripts directory for Vienna for the current user +/// (Library/Application Scripts/). @property (readonly, nonatomic) NSURL *vna_applicationScriptsDirectory NS_SWIFT_NAME(applicationScriptsDirectory); -/// The application support directory for the current user (Library/Application -/// Support). +/// The application support directory for Vienna for the current user +/// (Library/Application Support/Vienna). @property (readonly, nonatomic) NSURL *vna_applicationSupportDirectory NS_SWIFT_NAME(applicationSupportDirectory); -/// The caches directory for the current user (Library/Caches). +/// The caches directory for the current user (Library/Caches/). @property (readonly, nonatomic) NSURL *vna_cachesDirectory NS_SWIFT_NAME(cachesDirectory); diff --git a/Vienna/Sources/Shared/NSFileManager+Paths.m b/Vienna/Sources/Shared/NSFileManager+Paths.m index fb8ba35864..c6605b501c 100644 --- a/Vienna/Sources/Shared/NSFileManager+Paths.m +++ b/Vienna/Sources/Shared/NSFileManager+Paths.m @@ -25,12 +25,6 @@ @implementation NSFileManager (Paths) -// The NSApplicationScriptsDirectory search path returns a subdirectory in the -// Library/Application Scripts directory. Vienna currently uses a subdirectory -// in Library/Scripts/Applications instead. The sandboxing migration performs -// an automatic migration to Library/Application Scripts/ -// and places a symlink at the old location. This code needs to be updated -// accordingly. - (NSURL *)vna_applicationScriptsDirectory { static NSURL *url = nil; if (url) { @@ -39,16 +33,11 @@ - (NSURL *)vna_applicationScriptsDirectory { NSFileManager *fileManager = NSFileManager.defaultManager; NSError *error = nil; - // Replace NSLibraryDirectory with NSApplicationScriptsDirectory for - // sandboxing. - url = [fileManager URLForDirectory:NSLibraryDirectory + url = [fileManager URLForDirectory:NSApplicationScriptsDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:&error]; - // Remove the following lines for sandboxing. - url = [url URLByAppendingPathComponent:@"Scripts/Applications/Vienna" - isDirectory:YES]; if (!url && error) { const char *function = __PRETTY_FUNCTION__; @@ -59,9 +48,6 @@ - (NSURL *)vna_applicationScriptsDirectory { return (id _Nonnull)url; } -// According to Apple's file-system programming guide, the bundle identifier -// should be used as the subdirectory name. However, Vienna presently uses the -// app name instead. This should be changed when Vienna migrates to sandboxing. - (NSURL *)vna_applicationSupportDirectory { static NSURL *url = nil; if (url) { @@ -75,8 +61,6 @@ - (NSURL *)vna_applicationSupportDirectory { appropriateForURL:nil create:NO error:&error]; - // For sandboxing, use NSBundle.mainBundle.bundleIdentifier instead of the - // application name (recommendation by Apple, but not required). url = [url URLByAppendingPathComponent:@"Vienna" isDirectory:YES]; From 9bf66666f5617537bfc17cbc98aa1e30b746addb Mon Sep 17 00:00:00 2001 From: Eitot Date: Sat, 1 Jul 2023 23:06:10 +0200 Subject: [PATCH 2/3] Modify code for download folder selection to work with sandboxing NSOpenSavePanelDelegate methods do not work with sandboxing. The URLs returned by the delegate callbacks are inaccessible until NSOpenPanel calls the completion handler. A workaround is to validate the URL after it was selected. When the URL is inaccessible (i.e. not writable) then an error is shown and the open panel reopens, giving the user the opportunity to choose a different directory or cancel. --- .../GeneralPreferencesViewController.h | 2 +- .../GeneralPreferencesViewController.m | 71 +++++++++++-------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/Vienna/Sources/Preferences window/GeneralPreferencesViewController.h b/Vienna/Sources/Preferences window/GeneralPreferencesViewController.h index 8823d4ea9b..54afdd96e6 100644 --- a/Vienna/Sources/Preferences window/GeneralPreferencesViewController.h +++ b/Vienna/Sources/Preferences window/GeneralPreferencesViewController.h @@ -20,7 +20,7 @@ @import Cocoa; -@interface GeneralPreferencesViewController : NSViewController +@interface GeneralPreferencesViewController : NSViewController // Action functions -(IBAction)changeCheckFrequency:(id)sender; diff --git a/Vienna/Sources/Preferences window/GeneralPreferencesViewController.m b/Vienna/Sources/Preferences window/GeneralPreferencesViewController.m index 69e9762d84..f4c971e047 100644 --- a/Vienna/Sources/Preferences window/GeneralPreferencesViewController.m +++ b/Vienna/Sources/Preferences window/GeneralPreferencesViewController.m @@ -278,20 +278,43 @@ -(IBAction)changeOpenLinksInExternalBrowser:(id)sender * Bring up the folder browser to pick a new download folder. */ -(IBAction)changeDownloadFolder:(id)sender +{ + NSFileManager *fileManager = NSFileManager.defaultManager; + [self chooseDirectoryWithRootDirectory:fileManager.vna_downloadsDirectory]; +} + +- (void)chooseDirectoryWithRootDirectory:(NSURL *)rootDirectoryURL { NSOpenPanel *openPanel = [NSOpenPanel openPanel]; - openPanel.delegate = self; openPanel.canChooseFiles = NO; openPanel.canChooseDirectories = YES; openPanel.canCreateDirectories = YES; openPanel.allowsMultipleSelection = NO; openPanel.prompt = NSLocalizedString(@"Select", @"Label of a button on an open panel"); - - openPanel.directoryURL = NSFileManager.defaultManager.vna_downloadsDirectory; + openPanel.directoryURL = rootDirectoryURL; [openPanel beginSheetModalForWindow:self.view.window - completionHandler:^(NSInteger returnCode) { - if (returnCode == NSModalResponseOK) { + completionHandler:^(NSModalResponse result) { + if (result == NSModalResponseOK && openPanel.URL) { + NSFileManager *fileManager = NSFileManager.defaultManager; + if (![fileManager isWritableFileAtPath:openPanel.URL.path]) { + NSString *str = + NSLocalizedString(@"This folder cannot be chosen because " + "you don’t have permission.", + @"Message text of a modal alert"); + NSDictionary *userInfoDict = @{NSLocalizedDescriptionKey: str}; + NSError *error = + [NSError errorWithDomain:NSCocoaErrorDomain + code:NSFileWriteNoPermissionError + userInfo:userInfoDict]; + [self presentError:error + modalForWindow:self.view.window + delegate:self + didPresentSelector:@selector(didPresentErrorWithRecovery:contextInfo:) + contextInfo:(__bridge_retained void *)openPanel.URL]; + return; + } + NSError *error = nil; NSData *data = [VNASecurityScopedBookmark bookmarkDataFromFileURL:openPanel.URL error:&error]; @@ -300,12 +323,24 @@ -(IBAction)changeDownloadFolder:(id)sender [userDefaults setObject:data forKey:MAPref_DownloadsFolderBookmark]; [self updateDownloadsPopUp:openPanel.URL.path]; } - } else if (returnCode == NSModalResponseCancel) { + } else { [self->downloadFolder selectItemAtIndex:0]; } }]; } +// Do not change this method signature without consulting the documentation of +// `-presentError:modalForWindow:delegate:didPresentSelector:contextInfo:`. If +// the signature does not match, the contextInfo parameter might not work. +- (void)didPresentErrorWithRecovery:(BOOL)didRecover + contextInfo:(nullable void *)contextInfo +{ + NSURL *previousDirectoryURL = (__bridge_transfer NSURL *)contextInfo; + if ([previousDirectoryURL isKindOfClass:[NSURL class]]) { + [self chooseDirectoryWithRootDirectory:previousDirectoryURL]; + } +} + /* updateDownloadsPopUp * Update the Downloads folder popup with the specified download folder path and image. */ @@ -445,28 +480,4 @@ -(void)dealloc [[NSNotificationCenter defaultCenter] removeObserver:self]; } -// MARK: - NSOpenSavePanelDelegate - -- (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url { - NSFileManager *fileManager = NSFileManager.defaultManager; - return [fileManager isWritableFileAtPath:url.path]; -} - -- (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError **)outError { - NSFileManager *fileManager = NSFileManager.defaultManager; - BOOL isWritable = [fileManager isWritableFileAtPath:url.path]; - if (!isWritable) { - NSString *str = NSLocalizedString(@"This folder cannot be chosen " - "because you don’t have permission.", - @"Message text of a modal alert"); - NSDictionary *userInfoDict = @{NSLocalizedDescriptionKey: str}; - if (outError) { - *outError = [NSError errorWithDomain:NSCocoaErrorDomain - code:NSFileWriteNoPermissionError - userInfo:userInfoDict]; - } - } - return isWritable; -} - @end From 0beb28f968c0ffec477f4521667bb0e413e9ebbf Mon Sep 17 00:00:00 2001 From: Eitot Date: Sat, 1 Jul 2023 23:11:59 +0200 Subject: [PATCH 3/3] Add initial error handling to DownloadManager for permission errors --- .../Sources/Download window/DownloadManager.m | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Vienna/Sources/Download window/DownloadManager.m b/Vienna/Sources/Download window/DownloadManager.m index eddf4258b9..4064162f4f 100644 --- a/Vienna/Sources/Download window/DownloadManager.m +++ b/Vienna/Sources/Download window/DownloadManager.m @@ -322,10 +322,19 @@ - (void)URLSession:(NSURLSession *)session didFinishDownloadingToURL:(NSURL *)location { dispatch_sync(dispatch_get_main_queue(), ^{ DownloadItem *item = [self itemForSessionTask:downloadTask]; - [NSFileManager.defaultManager moveItemAtURL:location - toURL:item.fileURL - error:nil]; - item.state = DownloadStateCompleted; + NSError *error = nil; + BOOL success = [NSFileManager.defaultManager moveItemAtURL:location + toURL:item.fileURL + error:&error]; + if (success) { + item.state = DownloadStateCompleted; + } else { + item.state = DownloadStateFailed; + if (error.code == NSFileWriteNoPermissionError) { + // TODO: Implement error reporting + NSLog(@"%@", error.localizedDescription); + } + } [self deliverNotificationForDownloadItem:item]; }); }