diff --git a/README.md b/README.md index 396d42d..5a2fc37 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ See the "LICENSE" file for a full copy of the GNU GPL v3. ## Known bugs -- Returning an eBook to a library running Adobe Content Server 6 (ACS6) or newer will fail. The plugin will claim the book return was successful, but the book won't be marked as returned on the libraries' servers. This will hopefully be fixed with the next version of the plugin - I already dumped a bunch of logs from ADE so I know what my plugin is missing, now I just need to implement the fix and see if it works. It might be a good idea to stop returning books with the current version of the plugin, as that'd be a difference that the libraries could detect as doing something weird, if they're using ACS6. +- Versions 0.0.16 and below did sometimes return the wrong eBook (or none at all) when trying to return a book to the library through the "Loaned books" list, if you had multiple active loans from the same distributor / library. This will be fixed with 0.0.17. ## Setup diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index 837e980..4d49e70 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -41,6 +41,9 @@ # fix broken URLs with missing protocol, fix loan data for loans without device ID, # fix nonce calculation yet again, merge #26 to make importing a WINE auth more reliable, # update python-oscrypto to unofficial fork to fix OpenSSL 3 support. +# In Progress: +# Fix bug that would sometimes return the wrong book (or none at all) if you had +# multiple active loans from the same distributor. PLUGIN_NAME = "DeACSM" diff --git a/calibre-plugin/libadobeFulfill.py b/calibre-plugin/libadobeFulfill.py index 58462cd..228e26a 100644 --- a/calibre-plugin/libadobeFulfill.py +++ b/calibre-plugin/libadobeFulfill.py @@ -482,32 +482,28 @@ def fulfill(acsm_file, do_notify = False): -def updateLoanReturnData(fulfillmentResultToken): +def updateLoanReturnData(fulfillmentResultToken, forceTestBehaviour=False): NSMAP = { "adept" : "http://ns.adobe.com/adept" } adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag) dcNS = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag) try: - loanToken = fulfillmentResultToken.find("./%s" % (adNS("loanToken"))) - if (loanToken is None): - print("Loan token not found") + fulfillment_id = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken"), adNS("fulfillment"))).text + if (fulfillment_id is None): + print("Fulfillment ID not found, can't generate loan token") return False + except: print("Loan token error") return False try: - operatorURL = loanToken.find("./%s" % (adNS("operatorURL"))).text + operatorURL = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken"), adNS("operatorURL"))).text except: print("OperatorURL missing") return False - try: - loanID = None - loanID = loanToken.findall("./%s" % (adNS("loan")))[0].text - except: - pass book_name = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("metadata"), dcNS("title"))).text @@ -539,6 +535,18 @@ def updateLoanReturnData(fulfillmentResultToken): # "loanID" is the loan ID # "validUntil" is how long it's valid + new_loan_record = { + "book_name": book_name, + "user": userUUID, + "device": deviceUUID, + "loanID": fulfillment_id, + "operatorURL": operatorURL, + "validUntil": dsp_until + } + + if forceTestBehaviour: + return new_loan_record + try: import calibre_plugins.deacsm.prefs as prefs # type: ignore deacsmprefs = prefs.DeACSM_Prefs() @@ -552,14 +560,7 @@ def updateLoanReturnData(fulfillmentResultToken): # books, and can then return them. # Also, the config widget is responsible for cleaning up that list. - deacsmprefs["list_of_rented_books"].append({ - "book_name": book_name, - "user": userUUID, - "device": deviceUUID, - "loanID": loanID, - "operatorURL": operatorURL, - "validUntil": dsp_until - }) + deacsmprefs["list_of_rented_books"].append(new_loan_record) deacsmprefs.writeprefs() @@ -762,7 +763,9 @@ def performFulfillmentNotification(fulfillmentResultToken, forceOptional = False doc_send = "\n" + etree.tostring(full_text_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8") # Debug: Print notify request - #print(doc_send) + if (verbose_logging): + print("Notify payload XML:") + print(doc_send) try: code, msg = sendRequestDocuRC(doc_send, url) diff --git a/tests/main.py b/tests/main.py index ccaca93..e98592b 100755 --- a/tests/main.py +++ b/tests/main.py @@ -172,6 +172,37 @@ class TestAdobe(unittest.TestCase): self.assertEqual(sha_hash, "3452e3d11cdd70eb90323f291c06afafe10e098a", "Invalid SHA hash for node signing") + def test_hash_node_returnbugfix(self): + '''Check if the XML hash is correct when returning a book ...''' + + # I don't think there's ever a case where the hashing algorithm is different, + # but I needed this test during debugging and thought, hey, why not leave it in. + + mock_xml_str = """ + + urn:uuid:6e5393e0-ff13-4ae8-8f6c-6654182ac7d5 + urn:uuid:51abfbaf-f0e8-474d-b031-626c5224f90f + eVr2pi26AAAAAAAA + 2022-08-03T09:16:22Z + + 6ccfbc7a-349b-40ad-82d8-d7a4c717ca13-00000271 + 237493726-1749302749327354-Wed Aug 03 09:16:22 UTC 2022 + urn:uuid:6e5393e0-ff13-4ae8-8f6c-6654182ac7d5 + true + true + CB3Ql1FAJD957t5n749q5ZO8IzU= + + + """ + + mock_xml_obj = etree.fromstring(mock_xml_str) + sha_hash = libadobe.hash_node(mock_xml_obj).hexdigest().lower() + + self.assertEqual(sha_hash, "8b0a24ba37c4333d93650c6ce52f8ee779f21533", "Invalid SHA hash for node signing") + + + + def test_sign_node_old(self): '''Check if the external RSA library (unused) signs correctly''' @@ -324,6 +355,85 @@ class TestAdobe(unittest.TestCase): self.assertEqual(binascii.hexlify(msg), binascii.hexlify(expected_msg), "devkey encryption returned invalid result") +class TestPluginInterface(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def forcefail(self): + self.assertEqual(1, 2, "force fail") + + def test_loanReturnFulfillmentID(self): + '''Check if proper ID is used for the loan token''' + + # Previous versions of the plugin had a bug where sometimes the wrong loan token + # was used, which caused wrong (or no) books to be returned to a library. + # Adding a test case so this never happens again ... + + + mock_data = """ + + + + 34659b20-92c8-4004-9fd8-c5174e7eed47-00010214 + true + false + + urn:uuid:b7c6ccb8-1012-44a9-9c8b-0388d0c685f7 + 0 + + Book title for test + + + urn:uuid:2bd57a81-6192-4a1b-8eb2-64e2d197f9fa + urn:uuid:b7c6ccb8-1012-44a9-9c8b-0388d0c685f7 + standalone + urn:uuid:83681cbb-b6df-44a3-a423-c2b37ba66e84 + https://acs.example.com/fulfillment + 34659b20-92c8-4004-9fd8-c5174e7eed47-00010214 + urn:uuid:1f5c2437-58f2-4a24-9495-3e99155e6f98 + + + 34659b20-92c8-4004-9fd8-c5174e7eed47-00010214 + 2022-07-03T01:14:42Z + + + + + + + + + urn:uuid:2bd57a81-6192-4a1b-8eb2-64e2d197f9fa + https://acs.example.com/fulfillment + https://nasigningservice.adobe.com/licensesign + 6d2dc249-2bc0-43e1-a130-3866f85020d9-00003487 + 6d2dc249-2bc0-43e1-a130-3866f85020d9-00003467 + 34659b20-92c8-4004-9fd8-c5174e7eed47-00010214 + 11197eb9-3543-4b41-9c6e-03ffeaf277c0-00024754 + + + + """ + + + extracted_token = libadobeFulfill.updateLoanReturnData(etree.fromstring(mock_data), forceTestBehaviour=True) + + expected_token = { + "book_name": "Book title for test", + "device": 'urn:uuid:83681cbb-b6df-44a3-a423-c2b37ba66e84', + "user": 'urn:uuid:2bd57a81-6192-4a1b-8eb2-64e2d197f9fa', + "operatorURL": 'https://acs.example.com/fulfillment', + "loanID": '34659b20-92c8-4004-9fd8-c5174e7eed47-00010214', + "validUntil": '2022-07-03T01:14:42Z' + } + + self.assertEqual(extracted_token, expected_token, "Loan record generator broken") + + class TestOther(unittest.TestCase):