Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ once the version tag exists.

## [Unreleased]

### Added

- A password protected Word, Excel or PowerPoint file says so, instead of
failing to open for no stated reason.

### Changed

- Text copied out of a PDF reads as words, marking it highlights the words, and
a few more PDFs open at all.
- Documents keep the rows of a repeating table header, which had gone missing.

### Fixed

- A long document opens at the top of its first page on an iPad. It opened a
Expand Down
2 changes: 1 addition & 1 deletion OpenDocumentReader.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,7 @@
repositoryURL = "https://github.com/opendocument-app/OpenDocument.core.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 6.9.0;
minimumVersion = 6.10.0;
};
};
AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions OpenDocumentReader/CoreWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ let CoreWrapperErrorDomain = "app.opendocument.CoreWrapperErrorDomain"
case wrongPassword = 2
/// Not something odrcore renders for us — see the guard in `translate`.
case unsupportedFileType = 3
/// Locked, and odrcore has no way in whatever the password — a legacy Word,
/// Excel or PowerPoint file.
case undecryptable = 4
}

private func coreWrapperError(_ code: CoreWrapperError, _ description: String) -> NSError {
Expand Down Expand Up @@ -116,6 +119,10 @@ private func isCsv(_ file: DecodedFile) -> Bool { file.fileType == .commaSeparat
where error.code == ODRError.wrongPassword.rawValue
{
throw coreWrapperError(.wrongPassword, "wrong password")
} catch let error as NSError
where error.code == ODRError.unsupportedOperation.rawValue
{
throw coreWrapperError(.undecryptable, "odrcore cannot decrypt this format")
}
}

Expand Down
12 changes: 10 additions & 2 deletions OpenDocumentReader/DocumentViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -853,8 +853,14 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel
// attention: wrong for extensions like ".pages.zip"
let fileType = doc.fileURL.pathExtension.lowercased()

// no password opens one of these, so asking for one would only ask
// again, and the web view makes nothing of a locked file either
let isLocked =
(error as NSError).domain == CoreWrapperErrorDomain
&& (error as NSError).code == CoreWrapperError.undecryptable.rawValue

let fileName = doc.fileURL.absoluteString.lowercased()
if systemRenderedExtensions.contains(where: fileName.hasSuffix) {
if !isLocked, systemRenderedExtensions.contains(where: fileName.hasSuffix) {
// not odrcore's to render, but the web view knows the format
documentNavigation = self.webview.loadFileURL(doc.fileURL, allowingReadAccessTo: doc.fileURL)

Expand All @@ -873,7 +879,9 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel

documentNavigation = nil
loadMessage(
"<h1>\(NSLocalizedString("error", comment: ""))</h1>\(NSLocalizedString("toast_error_generic", comment: ""))"
"<h1>\(NSLocalizedString("error", comment: ""))</h1>"
+ NSLocalizedString(
isLocked ? "toast_error_password_protected" : "toast_error_generic", comment: "")
)

AnalyticsManager.shared.report(
Expand Down
122 changes: 122 additions & 0 deletions OpenDocumentReaderTests/LockedDocumentTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import WebKit
import XCTest

@testable import OpenDocumentReader

/// A legacy Word, Excel or PowerPoint file can say it is password protected,
/// and no password opens it: odrcore reads the flag but cannot decrypt any of
/// them. The reader has to say so rather than ask, and rather than hand the
/// file to the web view, which makes nothing of it either.
class LockedDocumentTests: XCTestCase {
private let temporaryDirectory = NSTemporaryDirectory()
private var documentURL: URL!
private var window: UIWindow!
private var controller: DocumentViewController!
private var document: Document!

override func setUpWithError() throws {
documentURL = try copyFixture()
}

override func tearDown() {
window?.isHidden = true
window = nil
controller = nil
document = nil
}

/// Out of the read-only test bundle, and away from the temporary directory
/// translating uses for its cache and output.
private func copyFixture() throws -> URL {
let documentsURL = try FileManager.default.url(
for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)

let url = documentsURL.appendingPathComponent("locked.doc")
try? FileManager.default.removeItem(at: url)

let bundlePath = try XCTUnwrap(
Bundle(for: Self.self).path(forResource: "test-encrypted", ofType: "doc"))
try FileManager.default.copyItem(at: URL(fileURLWithPath: bundlePath), to: url)

return url
}

/// Its own code, not `wrongPassword`: that one puts the prompt up, and the
/// prompt would only come back.
func testALockedLegacyFileReportsItsOwnError() throws {
let wrapper = CoreWrapper()

for password in [nil, "secret"] {
XCTAssertThrowsError(
try wrapper.translate(
documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: password,
editable: false)
) { error in
XCTAssertEqual((error as NSError).code, CoreWrapperError.undecryptable.rawValue)
}
}
}

/// `.doc` is one of the formats the reader otherwise falls back to the web
/// view for, so the message has to beat the fallback.
func testALockedLegacyFileSaysWhyItDidNotOpen() throws {
try present(documentURL)

let opened = expectation(description: "opened")
document.open { _ in opened.fulfill() }
wait(for: [opened], timeout: 60)

waitForPage(where: "document.body.innerText.length > 0")

let shown = try XCTUnwrap(evaluate("document.body.innerText") as? String)
XCTAssertTrue(
shown.contains(NSLocalizedString("toast_error_password_protected", comment: "")), shown)
XCTAssertFalse(controller.webview.url?.isFileURL ?? false, "the file was handed to the web view")
}

// MARK: - helpers

private func present(_ url: URL) throws {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle(for: DocumentViewController.self))
controller = try XCTUnwrap(
storyboard.instantiateViewController(withIdentifier: "TextDocumentViewController")
as? DocumentViewController)

document = Document(fileURL: url)
controller.document = document

window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
window.rootViewController = controller
window.makeKeyAndVisible()

controller.view.layoutIfNeeded()
}

private func waitForPage(
where condition: String, file: StaticString = #filePath, line: UInt = #line
) {
let deadline = Date().addingTimeInterval(60)

while Date() < deadline {
if evaluate(condition) as? Bool == true { return }

_ = XCTWaiter.wait(for: [expectation(description: "a turn of the run loop")], timeout: 0.1)
}

XCTFail("timed out waiting for \(condition)", file: file, line: line)
}

@discardableResult
private func evaluate(_ script: String) -> Any? {
let done = expectation(description: "evaluated")
var result: Any?

controller.webview.evaluateJavaScript(script) { value, _ in
result = value
done.fulfill()
}
wait(for: [done], timeout: 30)

return result
}
}
Binary file added OpenDocumentReaderTests/test-encrypted.doc
Binary file not shown.