Skip to content

Commit d612029

Browse files
authored
Allow access to the LCP License Document even if the status check fails (#647)
1 parent 806ece1 commit d612029

File tree

6 files changed

+47
-22
lines changed

6 files changed

+47
-22
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
All notable changes to this project will be documented in this file. Take a look at [the migration guide](docs/Migration%20Guide.md) to upgrade between two major versions.
44

5-
<!-- ## [Unreleased] -->
5+
## [Unreleased]
6+
7+
### Changed
8+
9+
#### LCP
10+
11+
* The LCP License Document is now accessible via `publication.lcpLicense?.license`, even if the license validation fails with a status error. This is useful for checking the end date of an expired license, for example.
12+
613

714
## [3.4.0]
815

Sources/LCP/Content Protection/LCPContentProtection.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ private final class LCPContentProtectionService: ContentProtectionService {
156156

157157
init(license: LCPLicense? = nil, error: Error? = nil) {
158158
self.license = license
159-
self.error = error
159+
self.error = error ?? license?.error.map { LCPError.licenseStatus($0) }
160160
}
161161

162162
convenience init(result: Result<LCPLicense, LCPError>) {
@@ -179,7 +179,7 @@ private final class LCPContentProtectionService: ContentProtectionService {
179179
let scheme: ContentProtectionScheme = .lcp
180180

181181
var isRestricted: Bool {
182-
license == nil
182+
license?.isRestricted ?? true
183183
}
184184

185185
var rights: UserRights {

Sources/LCP/LCPLicense.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ public protocol LCPLicense: UserRights {
1414
var license: LicenseDocument { get }
1515
var status: StatusDocument? { get }
1616

17+
/// The license is restricted if there is a status error.
18+
var isRestricted: Bool { get }
19+
20+
/// Status error detected while validating the license, e.g. if the license
21+
/// is expired or revoked.
22+
var error: StatusError? { get }
23+
1724
/// Deciphers the given encrypted data to be displayed in the reader.
1825
func decipher(_ data: Data) throws -> Data?
1926

Sources/LCP/License/License.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,31 @@ extension License: LCPLicense {
4545
documents.status
4646
}
4747

48+
public var isRestricted: Bool {
49+
error != nil
50+
}
51+
52+
public var error: StatusError? {
53+
switch documents.context {
54+
case .success:
55+
return nil
56+
case let .failure(error):
57+
return error
58+
}
59+
}
60+
4861
public var encryptionProfile: String? {
4962
license.encryption.profile
5063
}
5164

5265
public func decipher(_ data: Data) throws -> Data? {
53-
let context = try documents.getContext()
66+
let context = try documents.context.get()
5467
return client.decrypt(data: data, using: context)
5568
}
5669

5770
func charactersToCopyLeft() async -> Int? {
71+
guard !isRestricted else { return 0 }
72+
5873
do {
5974
return try await licenses.userRights(for: license.id).copy
6075
} catch {
@@ -64,13 +79,17 @@ extension License: LCPLicense {
6479
}
6580

6681
func canCopy(text: String) async -> Bool {
82+
guard !isRestricted else { return false }
83+
6784
guard let charactersLeft = await charactersToCopyLeft() else {
6885
return true
6986
}
7087
return text.count <= charactersLeft
7188
}
7289

7390
func copy(text: String) async -> Bool {
91+
guard !isRestricted else { return false }
92+
7493
do {
7594
var allowed = true
7695
try await licenses.updateUserRights(for: license.id) { rights in
@@ -94,6 +113,8 @@ extension License: LCPLicense {
94113
}
95114

96115
func pagesToPrintLeft() async -> Int? {
116+
guard !isRestricted else { return 0 }
117+
97118
do {
98119
return try await licenses.userRights(for: license.id).print
99120
} catch {
@@ -103,13 +124,17 @@ extension License: LCPLicense {
103124
}
104125

105126
func canPrint(pageCount: Int) async -> Bool {
127+
guard !isRestricted else { return false }
128+
106129
guard let pageLeft = await pagesToPrintLeft() else {
107130
return true
108131
}
109132
return pageCount <= pageLeft
110133
}
111134

112135
func print(pageCount: Int) async -> Bool {
136+
guard !isRestricted else { return false }
137+
113138
do {
114139
var allowed = true
115140
try await licenses.updateUserRights(for: license.id) { rights in

Sources/LCP/License/LicenseValidation.swift

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,19 @@ private let supportedProfiles = [
2323
"http://readium.org/lcp/profile-2.9",
2424
]
2525

26-
typealias Context = Either<LCPClientContext, StatusError>
26+
typealias Context = Result<LCPClientContext, StatusError>
2727

2828
// Holds the License/Status Documents and the DRM context, once validated.
2929
struct ValidatedDocuments {
3030
let license: LicenseDocument
31-
fileprivate let context: Context
31+
let context: Context
3232
let status: StatusDocument?
3333

3434
fileprivate init(_ license: LicenseDocument, _ context: Context, _ status: StatusDocument? = nil) {
3535
self.license = license
3636
self.context = context
3737
self.status = status
3838
}
39-
40-
func getContext() throws -> LCPClientContext {
41-
switch context {
42-
case let .left(context):
43-
return context
44-
case let .right(error):
45-
throw error
46-
}
47-
}
4839
}
4940

5041
/// Validation workflow of the License and Status Documents.
@@ -195,7 +186,7 @@ extension LicenseValidation {
195186
// 4. Check the dates and license status
196187
case let (.checkLicenseStatus(license, status, _), .checkedLicenseStatus(error)):
197188
if let error = error {
198-
self = .valid(ValidatedDocuments(license, .right(error), status))
189+
self = .valid(ValidatedDocuments(license, .failure(error), status))
199190
} else {
200191
self = .requestPassphrase(license, status)
201192
}
@@ -210,7 +201,7 @@ extension LicenseValidation {
210201

211202
// 6. Validate the license integrity
212203
case let (.validateIntegrity(license, status, _), .validatedIntegrity(context)):
213-
let documents = ValidatedDocuments(license, .left(context), status)
204+
let documents = ValidatedDocuments(license, .success(context), status)
214205
if let link = status?.link(for: .register) {
215206
self = .registerDevice(documents, link)
216207
} else {

Sources/LCP/Services/LicensesService.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,6 @@ final class LicensesService: Loggable {
102102
throw LCPError.missingPassphrase
103103
}
104104

105-
// Check the license status error if there's any
106-
// Note: Right now we don't want to return a License if it fails the Status check, that's why we attempt to get the DRM context. But it could change if we want to access, for example, the License metadata or perform an LSD interaction, but without being able to decrypt the book. In which case, we could remove this line.
107-
// Note2: The License already gets in this state when we perform a `return` successfully. We can't decrypt anymore but we still have access to the License Documents and LSD interactions.
108-
_ = try documents.getContext()
109-
110105
return License(documents: documents, client: client, validation: validation, licenses: licenses, device: device, httpClient: httpClient)
111106
}
112107

0 commit comments

Comments
 (0)