diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00706f8..641854b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/OpenDocumentReader.xcodeproj/project.pbxproj b/OpenDocumentReader.xcodeproj/project.pbxproj
index 21446e8..06e7cd2 100644
--- a/OpenDocumentReader.xcodeproj/project.pbxproj
+++ b/OpenDocumentReader.xcodeproj/project.pbxproj
@@ -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" */ = {
diff --git a/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 9fcc859..94b725a 100644
--- a/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/opendocument-app/OpenDocument.core.git",
"state" : {
- "revision" : "fb696177bed250b4e2c0aebae326ea6025bca49e",
- "version" : "6.9.0"
+ "revision" : "9edbc14c1844cd432820b268f18e9b58e5a8b28a",
+ "version" : "6.10.0"
}
},
{
diff --git a/OpenDocumentReader/CoreWrapper.swift b/OpenDocumentReader/CoreWrapper.swift
index 2a74f25..f59f59a 100644
--- a/OpenDocumentReader/CoreWrapper.swift
+++ b/OpenDocumentReader/CoreWrapper.swift
@@ -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 {
@@ -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")
}
}
diff --git a/OpenDocumentReader/DocumentViewController.swift b/OpenDocumentReader/DocumentViewController.swift
index 53d6c4a..f76fa3b 100644
--- a/OpenDocumentReader/DocumentViewController.swift
+++ b/OpenDocumentReader/DocumentViewController.swift
@@ -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)
@@ -873,7 +879,9 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel
documentNavigation = nil
loadMessage(
- "
\(NSLocalizedString("error", comment: ""))
\(NSLocalizedString("toast_error_generic", comment: ""))"
+ "\(NSLocalizedString("error", comment: ""))
"
+ + NSLocalizedString(
+ isLocked ? "toast_error_password_protected" : "toast_error_generic", comment: "")
)
AnalyticsManager.shared.report(
diff --git a/OpenDocumentReaderTests/LockedDocumentTests.swift b/OpenDocumentReaderTests/LockedDocumentTests.swift
new file mode 100644
index 0000000..62e6ec8
--- /dev/null
+++ b/OpenDocumentReaderTests/LockedDocumentTests.swift
@@ -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
+ }
+}
diff --git a/OpenDocumentReaderTests/test-encrypted.doc b/OpenDocumentReaderTests/test-encrypted.doc
new file mode 100644
index 0000000..07eba2a
Binary files /dev/null and b/OpenDocumentReaderTests/test-encrypted.doc differ