Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable directory download #2585

Merged
merged 15 commits into from
Apr 10, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
"version" : "509.1.1"
Expand Down
7 changes: 7 additions & 0 deletions DuckDuckGo/Common/Extensions/URLExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,13 @@ extension URL {
}
}

var isDirectory: Bool {
var isDirectory: ObjCBool = false
guard isFileURL,
FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else { return false }
return isDirectory.boolValue
}

mutating func setFileHidden(_ hidden: Bool) throws {
var resourceValues = URLResourceValues()
resourceValues.isHidden = true
Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ extension DownloadsTabExtension: NavigationResponder {
?? navigationResponse.mainFrameNavigation?.navigationAction

guard navigationResponse.httpResponse?.isSuccessful != false, // download non-http responses
!navigationResponse.url.isDirectory, // don‘t download a local directory
!responseCanShowMIMEType(navigationResponse) || navigationResponse.shouldDownload
// if user pressed Opt+Enter in the Address bar to download from a URL
|| (navigationResponse.mainFrameNavigation?.redirectHistory.last ?? navigationResponse.mainFrameNavigation?.navigationAction)?.navigationType == .custom(.userRequestedPageDownload)
Expand Down
94 changes: 94 additions & 0 deletions IntegrationTests/Downloads/DownloadsIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,100 @@ class DownloadsIntegrationTests: XCTestCase {
XCTAssertEqual(try? Data(contentsOf: fileUrl), data.html)
}

@MainActor
func testWhenUnsupportedMimeType_downloadStarts() async throws {
let preferences = DownloadsPreferences.shared
preferences.alwaysRequestDownloadLocation = false
preferences.selectedDownloadLocation = FileManager.default.temporaryDirectory

let downloadTaskFuture = FileDownloadManager.shared.downloadsPublisher.timeout(5).first().promise()
let suffix = Int.random(in: 0..<Int.max)
let url = URL.testsServer
.appendingPathComponent("fname_\(suffix).dat")
.appendingTestParameters(data: data.html,
headers: ["Content-Type": "application/unsupported-mime-type"])
let tab = tabViewModel.tab
_=await tab.setUrl(url, source: .link)?.result

let fileUrl = try await downloadTaskFuture.get().output
.timeout(1, scheduler: DispatchQueue.main) { .init(TimeoutError() as NSError) }.first().promise().get()

XCTAssertEqual(fileUrl, FileManager.default.temporaryDirectory.appendingPathComponent("fname_\(suffix).dat"))
XCTAssertEqual(try? Data(contentsOf: fileUrl), data.html)
}

@MainActor
func testWhenLocalFile_downloadStartsAlwaysDisplayingSavePanel() async throws {
let tempFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
let destDirURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: destDirURL, withIntermediateDirectories: true)
try data.html.write(to: tempFileURL)

let preferences = DownloadsPreferences.shared
preferences.alwaysRequestDownloadLocation = false
preferences.selectedDownloadLocation = destDirURL
let persistor = DownloadsPreferencesUserDefaultsPersistor()
persistor.lastUsedCustomDownloadLocation = destDirURL.path

let downloadTaskFuture = FileDownloadManager.shared.downloadsPublisher.timeout(5).first().promise()
let tab = tabViewModel.tab
_=await tab.setUrl(tempFileURL, source: .link)?.result

let eSaveDialogShown = expectation(description: "Save dialog shown")
let getSaveDialog = Task { @MainActor in
while true {
if let sheet = self.window.sheets.first as? NSSavePanel {
eSaveDialogShown.fulfill()
return sheet
}
try await Task.sleep(interval: 0.01)
}
}

if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [eSaveDialogShown], timeout: 5) {
getSaveDialog.cancel()
}
let saveDialog = try await getSaveDialog.value
window.endSheet(saveDialog, returnCode: .OK)

let fileUrl = try await downloadTaskFuture.get().output
.timeout(1, scheduler: DispatchQueue.main) { .init(TimeoutError() as NSError) }.first().promise().get()

XCTAssertEqual(fileUrl.resolvingSymlinksInPath(), destDirURL.appendingPathComponent(tempFileURL.lastPathComponent).resolvingSymlinksInPath())
XCTAssertEqual(try? Data(contentsOf: fileUrl), data.html)
}

@MainActor
func testWhenLocalNonExistentFile_loadingFails() async throws {
let preferences = DownloadsPreferences.shared
preferences.alwaysRequestDownloadLocation = false
preferences.selectedDownloadLocation = FileManager.default.temporaryDirectory
let tempFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)

let tab = tabViewModel.tab
let loadingResult = await tab.setUrl(tempFileURL, source: .link)?.result

XCTAssertThrowsError(try loadingResult?.get()) { error in
XCTAssertEqual((error as? URLError)?.code, .fileDoesNotExist)
}
}

@MainActor
func testWhenLocalFolder_loadingFails() async throws {
let preferences = DownloadsPreferences.shared
preferences.alwaysRequestDownloadLocation = false
preferences.selectedDownloadLocation = FileManager.default.temporaryDirectory
let dirURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true)

let tab = tabViewModel.tab
let loadingResult = await tab.setUrl(dirURL, source: .link)?.result

XCTAssertThrowsError(try loadingResult?.get()) { error in
XCTAssertEqual((error as? URLError)?.code, .fileIsDirectory)
}
}

@MainActor
func testWhenSaveDialogOpenInBackgroundTabAndTabIsClosed_downloadIsCancelled() async throws {
let persistor = DownloadsPreferencesUserDefaultsPersistor()
Expand Down
4 changes: 2 additions & 2 deletions IntegrationTests/Tab/TabContentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ class TabContentTests: XCTestCase {
return
}

// wait for print dialog to appear
// wait for save dialog to appear
let eSaveDialogShown = expectation(description: "Save dialog shown")
let getSaveDialog = Task { @MainActor in
while true {
Expand Down Expand Up @@ -277,7 +277,7 @@ class TabContentTests: XCTestCase {
let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
try await eNewtabPageLoaded.value

// wait for print dialog to appear
// wait for save dialog to appear
let eSaveDialogShown = expectation(description: "Save dialog shown")
let getSaveDialog = Task { @MainActor in
while true {
Expand Down
Loading