diff --git a/browser/src/control/Control.ESignature.ts b/browser/src/control/Control.ESignature.ts index d700bf89a7ab2..216ffec526a5f 100644 --- a/browser/src/control/Control.ESignature.ts +++ b/browser/src/control/Control.ESignature.ts @@ -19,6 +19,13 @@ namespace cool { commandValues: SignatureResponse; } + export interface CommandResultResponse { + commandName: string; + success: boolean; + // Depends on the value of commandName + result: any; + } + export interface HashSendResponse { doc_id: string; available_methods: Array; @@ -100,6 +107,7 @@ namespace cool { this.clientId = clientId; app.map.on('commandvalues', this.onCommandValues.bind(this)); + app.map.on('commandresult', this.onCommandResult.bind(this)); } insert(): void { @@ -107,6 +115,14 @@ namespace cool { app.socket.sendMessage('commandvalues command=.uno:Signature'); } + // Handles the result of dispatched UNO commands + onCommandResult(event: CommandResultResponse): void { + if (event.commandName == '.uno:PrepareSignature') { + const response = event.result; + this.handleSendHashResponse(event.success, response); + } + } + // Handles the command values response for .uno:Signature onCommandValues(event: CommandValuesResponse): void { if (event.commandName != '.uno:Signature') { @@ -121,14 +137,12 @@ namespace cool { const digest = signatureResponse.digest; // Step 2: send the hash, get a document ID. - const url = this.url + '/api/signatures/prepare-files-for-signing'; const redirectUrl = window.makeHttpUrl('/cool/signature'); const documentName = ( document.querySelector('#document-name-input') ); const fileName = documentName.value; const body = { - secret: this.secret, client_id: this.clientId, // Create a PKCS#7 binary signature container_type: 'cades', @@ -146,44 +160,14 @@ namespace cool { // Automatic file download will not happen after signing nodownload: true, }; - const headers = { - 'Content-Type': 'application/json', + const args = { + body: body, }; - const request = new Request(url, { - method: 'POST', - body: JSON.stringify(body), - headers: headers, - }); - window.fetch(request).then( - (response) => { - this.handleSendHashBytes(response); - }, - (error) => { - app.console.log( - 'failed to fetch /api/signatures/prepare-files-for-signing: ' + - error.message, - ); - }, - ); - } - - // Handles the 'send hash' response bytes - handleSendHashBytes(response: Response): void { - response.json().then( - (json) => { - this.handleSendHashJson(response.ok, json); - }, - (error) => { - app.console.log( - 'failed to parse response from /api/signatures/prepare-files-for-signing as JSON: ' + - error.message, - ); - }, - ); + app.map.sendUnoCommand('.uno:PrepareSignature', args); } // Handles the 'send hash' response JSON - handleSendHashJson(ok: boolean, response: HashSendResponse): void { + handleSendHashResponse(ok: boolean, response: HashSendResponse): void { if (!ok) { app.console.log( '/api/signatures/prepare-files-for-signing failed: ' + diff --git a/browser/src/control/Toolbar.js b/browser/src/control/Toolbar.js index 3e43db46752f4..61a394459bf8c 100644 --- a/browser/src/control/Toolbar.js +++ b/browser/src/control/Toolbar.js @@ -378,7 +378,7 @@ L.Map.include({ var isAllowedInReadOnly = false; var allowedCommands = ['.uno:Save', '.uno:WordCountDialog', - '.uno:Signature', '.uno:ShowResolvedAnnotations', + '.uno:Signature', '.uno:PrepareSignature', '.uno:ShowResolvedAnnotations', '.uno:ToolbarMode?Mode:string=notebookbar_online.ui', '.uno:ToolbarMode?Mode:string=Default', '.uno:ExportToEPUB', '.uno:ExportToPDF', '.uno:ExportDirectToPDF', '.uno:MoveKeepInsertMode', '.uno:ShowRuler']; if (app.isCommentEditingAllowed()) { diff --git a/cypress_test/integration_tests/desktop/draw/esign_spec.js b/cypress_test/integration_tests/desktop/draw/esign_spec.js index aa2ce986cb883..57b1c75babe6a 100644 --- a/cypress_test/integration_tests/desktop/draw/esign_spec.js +++ b/cypress_test/integration_tests/desktop/draw/esign_spec.js @@ -8,8 +8,21 @@ describe(['tagdesktop'], 'Electronic sign operations.', function() { // Given a document that can be signed: helper.setupAndLoadDocument('draw/esign.pdf', /*isMultiUser=*/false, /*copyCertificates=*/true); - cy.intercept('POST', 'https://test.eideasy.com/api/signatures/prepare-files-for-signing', - {fixture : 'fixtures/eideasy-send-hash.json'}).as('sendHash'); + let sendHashResult; + cy.fixture('fixtures/eideasy-send-hash.json').then((result) => { + sendHashResult = result; + }); + cy.getFrameWindow().then(function(win) { + const sendUnoCommand = cy.stub(win.app.map, 'sendUnoCommand'); + sendUnoCommand.withArgs('.uno:PrepareSignature').as('sendHash').callsFake((commandName, args) => { + expect(args.body.signature_redirect).to.satisfy(url => url.endsWith('/cool/signature')); + // File name is like esign-Create-an-electronic-signature--0wvs9.pdf + expect(args.body.files[0].fileName).to.match(/^esign.*pdf$/i); + win.app.map.fire('commandresult', {commandName: '.uno:PrepareSignature', success: true, result: sendHashResult}); + }); + // Call the original sendUnoCommand() for other commands + sendUnoCommand.callThrough(); + }); cy.getFrameWindow() .then(function(win) { cy.stub(win, 'open').as('windowOpen'); @@ -20,11 +33,7 @@ describe(['tagdesktop'], 'Electronic sign operations.', function() { // When signing that document: cy.cGet('#menu-insert').click(); cy.cGet('#menu-insert-esignature').click(); - cy.wait(['@sendHash']).then(interception => { - expect(interception.request.body.signature_redirect).to.satisfy(url => url.endsWith('/cool/signature')); - // File name is like esign-Create-an-electronic-signature--0wvs9.pdf - expect(interception.request.body.files[0].fileName).to.match(/^esign.*pdf$/i); - }); + cy.get('@sendHash').should('be.called'); cy.cGet('#ESignatureDialog button#ok').click(); cy.get('@windowOpen').should('be.called'); const response = { diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp index b1392f26f3ae9..c3019e5c0211b 100644 --- a/wsd/ClientSession.cpp +++ b/wsd/ClientSession.cpp @@ -428,6 +428,90 @@ void ClientSession::onTileProcessed(TileWireId wireId) LOG_INF("Tileprocessed message with an unknown wire-id '" << wireId << "' from session " << getId()); } +#if !MOBILEAPP +namespace +{ +std::shared_ptr makePrepareSignatureSession(const std::shared_ptr clientSession, const std::string& requestUrl) +{ + // Create the session and set a finished callback + std::shared_ptr httpSession = http::Session::create(requestUrl); + if (!httpSession) + { + LOG_WRN("PrepareSignature: failed to create HTTP session"); + return nullptr; + } + + http::Session::FinishedCallback finishedCallback = [clientSession](const std::shared_ptr& session) + { + const std::shared_ptr httpResponse = session->response(); + Poco::JSON::Object::Ptr resultArguments = new Poco::JSON::Object(); + resultArguments->set("commandName", ".uno:PrepareSignature"); + + bool ok = httpResponse->statusLine().statusCode() == http::StatusCode::OK; + resultArguments->set("success", ok); + + const std::string& responseBody = httpResponse->getBody(); + Poco::JSON::Object::Ptr responseBodyObject = new Poco::JSON::Object(); + if (!JsonUtil::parseJSON(responseBody, responseBodyObject)) + { + LOG_WRN("PrepareSignature: failed to parse response body as JSON"); + return; + } + resultArguments->set("result", responseBodyObject); + + std::ostringstream oss; + resultArguments->stringify(oss); + std::string result = "unocommandresult: " + oss.str(); + clientSession->sendTextFrame(result); + }; + httpSession->setFinishedHandler(std::move(finishedCallback)); + return httpSession; +} +} + +bool ClientSession::handlePrepareSignature(const StringVector& tokens) +{ + // Make the HTTP session: this requires an URL + Poco::JSON::Object::Ptr userPrivateInfoObject = new Poco::JSON::Object(); + if (!JsonUtil::parseJSON(getUserPrivateInfo(), userPrivateInfoObject)) + { + LOG_WRN("PrepareSignature: failed to parse user private info as JSON"); + return false; + } + std::string requestUrl; + JsonUtil::findJSONValue(userPrivateInfoObject, "ESignatureBaseUrl", requestUrl); + requestUrl += "/api/signatures/prepare-files-for-signing"; + std::shared_ptr httpSession = makePrepareSignatureSession(client_from_this(), requestUrl); + if (!httpSession) + { + return false; + } + + // Make the request: this requires a JSON body, where we set the secret + std::string commandArguments = tokens.cat(' ', 2); + Poco::JSON::Object::Ptr commandArgumentsObject; + if (!JsonUtil::parseJSON(commandArguments, commandArgumentsObject)) + { + LOG_WRN("PrepareSignature: failed to parse arguments as JSON"); + return false; + } + auto requestBodyObject = commandArgumentsObject->get("body").extract(); + std::string secret; + JsonUtil::findJSONValue(userPrivateInfoObject, "ESignatureSecret", secret); + requestBodyObject->set("secret", secret); + std::string requestBody; + std::stringstream oss; + requestBodyObject->stringify(oss); + requestBody = oss.str(); + http::Request httpRequest(Poco::URI(requestUrl).getPathAndQuery()); + httpRequest.setVerb(http::Request::VERB_POST); + httpRequest.setBody(requestBody, "application/json"); + std::shared_ptr docBroker = getDocumentBroker(); + httpSession->asyncRequest(httpRequest, docBroker->getPoll()); + return true; +} +#endif + bool ClientSession::_handleInput(const char *buffer, int length) { LOG_TRC("handling incoming [" << getAbbreviatedMessage(buffer, length) << ']'); @@ -1200,6 +1284,13 @@ bool ClientSession::_handleInput(const char *buffer, int length) tokens.equals(0, "geta11ycaretposition") || tokens.equals(0, "getpresentationinfo")) { +#if !MOBILEAPP + if (tokens.equals(0, "uno") && tokens.equals(1, ".uno:PrepareSignature")) + { + return handlePrepareSignature(tokens); + } +#endif + if (tokens.equals(0, "key")) _keyEvents++; diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp index 68f564d72a929..2ef1123a6da22 100644 --- a/wsd/ClientSession.hpp +++ b/wsd/ClientSession.hpp @@ -289,6 +289,8 @@ class ClientSession final : public Session virtual bool _handleInput(const char* buffer, int length) override; + bool handlePrepareSignature(const StringVector& tokens); + bool loadDocument(const char* buffer, int length, const StringVector& tokens, const std::shared_ptr& docBroker); bool getStatus(const char* buffer, int length,