From 906a55e193b9ed77572335f89b0f7b273f0a1dee Mon Sep 17 00:00:00 2001 From: Benjamin Willig Date: Mon, 13 Nov 2023 08:59:26 +0100 Subject: [PATCH] [ADD] generate punchout setup using qweb, handle cxml-base64, use save_session and auth=none for browser form post url --- punchout/__manifest__.py | 8 +- punchout/controllers/main.py | 20 +- punchout/data/cxml/common.xml | 43 +++ punchout/data/cxml/punchout_setup_request.xml | 18 ++ punchout/i18n/fr.po | 213 ++++++++----- punchout/models/__init__.py | 2 +- punchout/models/punchout_backend.py | 40 ++- ...unchout_request.py => punchout_session.py} | 279 ++++++++++-------- ...chout_request.xml => punchout_session.xml} | 6 +- punchout/tests/common.py | 10 +- punchout/tests/cxml/test_store_request.xml | 3 +- .../cxml/test_unknown_buyer_cookie_id.xml | 2 +- punchout/tests/test_punchout.py | 39 +-- punchout/views/punchout_backend.xml | 5 +- ...chout_request.xml => punchout_session.xml} | 46 +-- .../models/punchout_backend.py | 1 - punchout_queue_job/__manifest__.py | 2 +- punchout_queue_job/models/__init__.py | 2 +- ...unchout_request.py => punchout_session.py} | 10 +- ...chout_request.xml => punchout_session.xml} | 6 +- requirements.txt | 1 - 21 files changed, 452 insertions(+), 304 deletions(-) create mode 100644 punchout/data/cxml/common.xml create mode 100644 punchout/data/cxml/punchout_setup_request.xml rename punchout/models/{punchout_request.py => punchout_session.py} (57%) rename punchout/security/{punchout_request.xml => punchout_session.xml} (67%) rename punchout/views/{punchout_request.xml => punchout_session.xml} (67%) rename punchout_queue_job/models/{punchout_request.py => punchout_session.py} (86%) rename punchout_queue_job/views/{punchout_request.xml => punchout_session.xml} (80%) diff --git a/punchout/__manifest__.py b/punchout/__manifest__.py index 48dcde379fe..8932c978bb1 100644 --- a/punchout/__manifest__.py +++ b/punchout/__manifest__.py @@ -14,9 +14,11 @@ ], "data": [ "security/punchout_backend.xml", - "security/punchout_request.xml", + "security/punchout_session.xml", + "data/cxml/common.xml", + "data/cxml/punchout_setup_request.xml", "views/punchout_backend.xml", - "views/punchout_request.xml", + "views/punchout_session.xml", ], - "external_dependencies": {"python": ["cryptography", "lxml"]}, + "external_dependencies": {"python": ["lxml"]}, } diff --git a/punchout/controllers/main.py b/punchout/controllers/main.py index f5bf13281b5..05948ddff7d 100644 --- a/punchout/controllers/main.py +++ b/punchout/controllers/main.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import logging +from base64 import b64decode from odoo.http import Controller, local_redirect, request, route @@ -12,20 +13,25 @@ class PunchoutController(Controller): @route( "/punchout/cxml/receive/", type="http", - auth="user", + auth="none", methods=["POST"], + save_session=False, csrf=False, ) def receive_punchout_response(self, backend_id, *args, **kwargs): env = request.env - cxml_string = kwargs.get("cxml-urlencoded") - punchout_request = ( - env["punchout.request"] + cxml_b64_string = kwargs.get("cXML-base64") + cxml_string = False + if cxml_b64_string: + cxml_string = b64decode(cxml_b64_string) + cxml_string = cxml_string or kwargs.get("cxml-urlencoded") + punchout_session = ( + env["punchout.session"] .sudo() - ._store_punchout_request(backend_id, cxml_string) + ._store_punchout_session_response(backend_id, cxml_string) ) backend = env["punchout.backend"].sudo().browse(backend_id) - if not punchout_request: + if not punchout_session: redirect_url = backend._get_redirect_url() _logger.error( "Unable to link the punchout response to a punchout.request " @@ -33,5 +39,5 @@ def receive_punchout_response(self, backend_id, *args, **kwargs): cxml_string, ) else: - redirect_url = punchout_request._get_redirect_url() + redirect_url = punchout_session._get_redirect_url() return local_redirect(redirect_url) diff --git a/punchout/data/cxml/common.xml b/punchout/data/cxml/common.xml new file mode 100644 index 00000000000..76a7529c009 --- /dev/null +++ b/punchout/data/cxml/common.xml @@ -0,0 +1,43 @@ + + + + + + + + + diff --git a/punchout/data/cxml/punchout_setup_request.xml b/punchout/data/cxml/punchout_setup_request.xml new file mode 100644 index 00000000000..8eedffdfd65 --- /dev/null +++ b/punchout/data/cxml/punchout_setup_request.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/punchout/i18n/fr.po b/punchout/i18n/fr.po index 84ba4bc008f..9f131b2f6ad 100644 --- a/punchout/i18n/fr.po +++ b/punchout/i18n/fr.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 13.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-10-30 15:26+0000\n" -"PO-Revision-Date: 2023-10-30 15:26+0000\n" +"POT-Creation-Date: 2023-11-27 17:24+0000\n" +"PO-Revision-Date: 2023-11-27 17:24+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -26,27 +26,27 @@ msgid "Access" msgstr "Accéder" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_needaction +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_needaction msgid "Action Needed" msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__action_process_allowed +#: model:ir.model.fields,field_description:punchout.field_punchout_session__action_process_allowed msgid "Action Process Allowed" msgstr "" #. module: punchout -#: model_terms:ir.ui.view,arch_db:punchout.punchout_request_form_view -msgid "An error occured during the process of the request." -msgstr "Une erreur est survenue lors du traitement de la requête." +#: model_terms:ir.ui.view,arch_db:punchout.punchout_session_form_view +msgid "An error occured during the process of the session." +msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_attachment_count +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_attachment_count msgid "Attachment Count" msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__backend_id +#: model:ir.model.fields,field_description:punchout.field_punchout_session__backend_id msgid "Backend" msgstr "" @@ -68,22 +68,27 @@ msgid "Closed" msgstr "Fermé" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__buyer_cookie_id +#: model:ir.model.fields,field_description:punchout.field_punchout_session__buyer_cookie_id msgid "Cookie" msgstr "" #. module: punchout #: model:ir.model.fields,field_description:punchout.field_punchout_backend__create_uid -#: model:ir.model.fields,field_description:punchout.field_punchout_request__create_uid +#: model:ir.model.fields,field_description:punchout.field_punchout_session__create_uid msgid "Created by" msgstr "" #. module: punchout #: model:ir.model.fields,field_description:punchout.field_punchout_backend__create_date -#: model:ir.model.fields,field_description:punchout.field_punchout_request__create_date +#: model:ir.model.fields,field_description:punchout.field_punchout_session__create_date msgid "Created on" msgstr "" +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__dtd_file +msgid "DTD File for validation" +msgstr "" + #. module: punchout #: model:ir.model.fields,field_description:punchout.field_punchout_backend__deployment_mode #: model_terms:ir.ui.view,arch_db:punchout.punchout_backend_search_view @@ -92,38 +97,53 @@ msgstr "" #. module: punchout #: model:ir.model.fields,field_description:punchout.field_punchout_backend__display_name -#: model:ir.model.fields,field_description:punchout.field_punchout_request__display_name +#: model:ir.model.fields,field_description:punchout.field_punchout_session__display_name msgid "Display Name" msgstr "" #. module: punchout -#: code:addons/punchout/models/punchout_request.py:0 +#: code:addons/punchout/models/punchout_session.py:0 #, python-format msgid "Done" msgstr "Traitée" #. module: punchout #: code:addons/punchout/models/punchout_backend.py:0 -#: code:addons/punchout/models/punchout_request.py:0 +#: code:addons/punchout/models/punchout_session.py:0 #, python-format msgid "Draft" msgstr "Brouillon" #. module: punchout -#: model_terms:ir.ui.view,arch_db:punchout.punchout_request_form_view +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__dtd_filename +msgid "Dtd Filename" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__dtd_version +msgid "Dtd Version" +msgstr "" + +#. module: punchout +#: model_terms:ir.ui.view,arch_db:punchout.punchout_session_form_view msgid "Erreur" msgstr "" #. module: punchout -#: code:addons/punchout/models/punchout_request.py:0 +#: code:addons/punchout/models/punchout_session.py:0 #, python-format msgid "Error" msgstr "Erreur" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__error_message +#: model:ir.model.fields,field_description:punchout.field_punchout_session__error_message msgid "Error Message" -msgstr "Message d'erreur" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_session__expiration_date +msgid "Expiration Date" +msgstr "" #. module: punchout #: model:ir.model.fields,help:punchout.field_punchout_backend__browser_form_post_url @@ -131,17 +151,17 @@ msgid "Exposed URL where the shopping cart must be sent back to." msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_follower_ids +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_follower_ids msgid "Followers" msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_channel_ids +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_channel_ids msgid "Followers (Channels)" msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_partner_ids +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_partner_ids msgid "Followers (Partners)" msgstr "" @@ -162,61 +182,61 @@ msgstr "" #. module: punchout #: model:ir.model.fields,field_description:punchout.field_punchout_backend__id -#: model:ir.model.fields,field_description:punchout.field_punchout_request__id +#: model:ir.model.fields,field_description:punchout.field_punchout_session__id msgid "ID" msgstr "" #. module: punchout -#: model:ir.model.fields,help:punchout.field_punchout_request__message_needaction -#: model:ir.model.fields,help:punchout.field_punchout_request__message_unread +#: model:ir.model.fields,help:punchout.field_punchout_session__message_needaction +#: model:ir.model.fields,help:punchout.field_punchout_session__message_unread msgid "If checked, new messages require your attention." msgstr "" #. module: punchout -#: model:ir.model.fields,help:punchout.field_punchout_request__message_has_error +#: model:ir.model.fields,help:punchout.field_punchout_session__message_has_error msgid "If checked, some messages have a delivery error." msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_is_follower +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_is_follower msgid "Is Follower" msgstr "" -#. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_backend__buyer_cookie_encryption_key -msgid "Key to encrypt the buyer cookie" -msgstr "Clé pour l'encryption du buyer cookie" - #. module: punchout #: model:ir.model.fields,field_description:punchout.field_punchout_backend____last_update -#: model:ir.model.fields,field_description:punchout.field_punchout_request____last_update +#: model:ir.model.fields,field_description:punchout.field_punchout_session____last_update msgid "Last Modified on" msgstr "" #. module: punchout #: model:ir.model.fields,field_description:punchout.field_punchout_backend__write_uid -#: model:ir.model.fields,field_description:punchout.field_punchout_request__write_uid +#: model:ir.model.fields,field_description:punchout.field_punchout_session__write_uid msgid "Last Updated by" msgstr "" #. module: punchout #: model:ir.model.fields,field_description:punchout.field_punchout_backend__write_date -#: model:ir.model.fields,field_description:punchout.field_punchout_request__write_date +#: model:ir.model.fields,field_description:punchout.field_punchout_session__write_date msgid "Last Updated on" msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_main_attachment_id +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_main_attachment_id msgid "Main Attachment" msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_has_error +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__session_duration +msgid "Maximum session duration" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_has_error msgid "Message Delivery error" msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_ids +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_ids msgid "Messages" msgstr "" @@ -231,33 +251,33 @@ msgid "Networkid" msgstr "" #. module: punchout -#: code:addons/punchout/models/punchout_request.py:0 +#: code:addons/punchout/models/punchout_session.py:0 #, python-format msgid "No punchout backend found to initialize the connection." msgstr "Aucun backend punchout trouvé pour initialisé la connexion." #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_needaction_counter +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_needaction_counter msgid "Number of Actions" msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_has_error_counter +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_has_error_counter msgid "Number of errors" msgstr "" #. module: punchout -#: model:ir.model.fields,help:punchout.field_punchout_request__message_needaction_counter +#: model:ir.model.fields,help:punchout.field_punchout_session__message_needaction_counter msgid "Number of messages which requires an action" msgstr "" #. module: punchout -#: model:ir.model.fields,help:punchout.field_punchout_request__message_has_error_counter +#: model:ir.model.fields,help:punchout.field_punchout_session__message_has_error_counter msgid "Number of messages with delivery error" msgstr "" #. module: punchout -#: model:ir.model.fields,help:punchout.field_punchout_request__message_unread_counter +#: model:ir.model.fields,help:punchout.field_punchout_session__message_unread_counter msgid "Number of unread messages" msgstr "" @@ -268,12 +288,12 @@ msgid "Open" msgstr "Ouvert" #. module: punchout -#: model_terms:ir.ui.view,arch_db:punchout.punchout_request_form_view +#: model_terms:ir.ui.view,arch_db:punchout.punchout_session_form_view msgid "Process" msgstr "Traiter" #. module: punchout -#: code:addons/punchout/models/punchout_request.py:0 +#: code:addons/punchout/models/punchout_session.py:0 #, python-format msgid "Processing the request" msgstr "Traitement de la requête" @@ -295,38 +315,63 @@ msgid "PunchOut Backends" msgstr "Backends PunchOut" #. module: punchout -#: model:ir.actions.act_window,name:punchout.punchout_request_act_window -#: model:ir.ui.menu,name:punchout.punchout_request_menu -msgid "PunchOut Requests" -msgstr "Requêtes PunchOut" - -#. module: punchout -#: code:addons/punchout/models/punchout_request.py:0 -#: code:addons/punchout/models/punchout_request.py:0 +#: code:addons/punchout/models/punchout_session.py:0 +#: code:addons/punchout/models/punchout_session.py:0 #, python-format msgid "PunchOut request error" msgstr "" +#. module: punchout +#: model:ir.actions.act_window,name:punchout.punchout_session_act_window +#: model:ir.ui.menu,name:punchout.punchout_session_menu +msgid "PunchOut sessions" +msgstr "Sessions PunchOut" + #. module: punchout #: model:ir.actions.act_window,name:punchout.punchout_backend_act_window msgid "Punchout Backends" -msgstr "" +msgstr "Backends PunchOut" #. module: punchout -#: model:ir.model,name:punchout.model_punchout_request -msgid "Punchout Request" -msgstr "Requête Punchout " +#: model:ir.model,name:punchout.model_punchout_session +msgid "Punchout Session" +msgstr "Session PunchOut" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__cxml_response -#: model_terms:ir.ui.view,arch_db:punchout.punchout_request_form_view +#: model_terms:ir.ui.view,arch_db:punchout.punchout_session_form_view +msgid "Received" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_session__cxml_response +#: model_terms:ir.ui.view,arch_db:punchout.punchout_session_form_view msgid "Response" -msgstr "Réponse" +msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__cxml_response_date +#: model:ir.model.fields,field_description:punchout.field_punchout_session__cxml_response_date msgid "Response Date" -msgstr "Date de la réponse" +msgstr "" + +#. module: punchout +#: model_terms:ir.ui.view,arch_db:punchout.punchout_session_form_view +msgid "Sent" +msgstr "" + +#. module: punchout +#: model_terms:ir.ui.view,arch_db:punchout.punchout_session_form_view +msgid "Setup" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_session__cxml_setup_request +msgid "Setup Request" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_session__cxml_setup_request_response +msgid "Setup Request Response" +msgstr "" #. module: punchout #: model:ir.model.fields,field_description:punchout.field_punchout_backend__shared_secret @@ -334,13 +379,13 @@ msgid "Shared secret" msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__punchout_url +#: model:ir.model.fields,field_description:punchout.field_punchout_session__punchout_url msgid "Start URL" -msgstr "URL de départ" +msgstr "" #. module: punchout #: model:ir.model.fields,field_description:punchout.field_punchout_backend__state -#: model:ir.model.fields,field_description:punchout.field_punchout_request__state +#: model:ir.model.fields,field_description:punchout.field_punchout_session__state msgid "State" msgstr "Statut" @@ -350,11 +395,23 @@ msgid "Test or production" msgstr "" #. module: punchout -#: code:addons/punchout/models/punchout_request.py:0 +#: code:addons/punchout/models/punchout_backend.py:0 +#, python-format +msgid "The duration of the session must be greater than 0. {name}" +msgstr "" + +#. module: punchout +#: code:addons/punchout/models/punchout_session.py:0 #, python-format msgid "The response has been received and will be processed." msgstr "La réponse a été reçue et va être traitée." +#. module: punchout +#: code:addons/punchout/models/punchout_session.py:0 +#, python-format +msgid "The response received from the backend is not valid." +msgstr "" + #. module: punchout #: code:addons/punchout/models/punchout_backend.py:0 #: model:ir.model.constraint,message:punchout.constraint_punchout_backend_name_unique @@ -363,10 +420,10 @@ msgid "This PunchOut backend already exists." msgstr "" #. module: punchout -#: code:addons/punchout/models/punchout_request.py:0 +#: code:addons/punchout/models/punchout_session.py:0 #, python-format msgid "To Process" -msgstr "" +msgstr "A traiter" #. module: punchout #: model:ir.model.fields,field_description:punchout.field_punchout_backend__to_domain @@ -384,19 +441,25 @@ msgid "URL" msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_unread +#: code:addons/punchout/models/punchout_session.py:0 +#, python-format +msgid "Unable to process the request" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_unread msgid "Unread Messages" msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_unread_counter +#: model:ir.model.fields,field_description:punchout.field_punchout_session__message_unread_counter msgid "Unread Messages Counter" msgstr "" #. module: punchout -#: model:ir.model.fields,field_description:punchout.field_punchout_request__user_id +#: model:ir.model.fields,field_description:punchout.field_punchout_session__user_id msgid "User" -msgstr "Utilisateur" +msgstr "" #. module: punchout #: model:ir.model.fields,field_description:punchout.field_punchout_backend__user_agent @@ -404,13 +467,13 @@ msgid "User agent" msgstr "" #. module: punchout -#: code:addons/punchout/models/punchout_request.py:0 +#: code:addons/punchout/models/punchout_session.py:0 #, python-format msgid "You are not allowed to process this request. " msgstr "" #. module: punchout -#: code:addons/punchout/models/punchout_request.py:0 +#: code:addons/punchout/models/punchout_session.py:0 #, python-format msgid "" "You must set a personnal email in your preferences in order to access this " diff --git a/punchout/models/__init__.py b/punchout/models/__init__.py index cff0fd5f1d3..6308ac9c341 100644 --- a/punchout/models/__init__.py +++ b/punchout/models/__init__.py @@ -1,2 +1,2 @@ from . import punchout_backend -from . import punchout_request +from . import punchout_session diff --git a/punchout/models/punchout_backend.py b/punchout/models/punchout_backend.py index 5b1d85992e7..76358f0f053 100644 --- a/punchout/models/punchout_backend.py +++ b/punchout/models/punchout_backend.py @@ -1,9 +1,9 @@ # Copyright 2023 ACSONE SA/NV # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + from odoo import _, api, fields, models -from odoo.exceptions import UserError -from odoo.http import request as odoo_request +from odoo.exceptions import UserError, ValidationError class PunchoutBackend(models.Model): @@ -37,12 +37,23 @@ class PunchoutBackend(models.Model): help="Exposed URL where the shopping cart must be sent back to.", required=True, ) - buyer_cookie_encryption_key = fields.Char( - string="Key to encrypt the buyer cookie", - groups="base.group_system", - required=True, + dtd_version = fields.Char(default="1.2.008",) + dtd_file = fields.Binary( + string="DTD File for validation", groups="base.group_system", ) + dtd_filename = fields.Char(groups="base.group_system",) state = fields.Selection(selection="_selection_state", default="draft") + session_duration = fields.Integer(string="Maximum session duration", default=7200,) + + @api.constrains("session_duration") + def _check_session_duration(self): + for rec in self: + if rec.session_duration <= 0: + raise ValidationError( + _( + "The duration of the session must be greater than 0. {name}" + ).format(name=rec.display_name) + ) @api.model def _selection_state(self): @@ -78,9 +89,7 @@ def _get_browser_form_post_url(self): ) ) - return "/".join( - [base_url, url, str(self.id), f"?session_id={odoo_request.session.sid}"] - ) + return "/".join([base_url, url, str(self.id), f"?db={self.env.cr.dbname}"]) def _check_access_backend(self): """ @@ -93,7 +102,7 @@ def redirect_to_backend(self): self.ensure_one() self._check_access_backend() return ( - self.env["punchout.request"] + self.env["punchout.session"] .with_context(punchout_backend_id=self.id,) ._redirect_to_punchout() ) @@ -101,3 +110,14 @@ def redirect_to_backend(self): def _get_redirect_url(self): self.ensure_one() return "/web" + + def _get_cxml_version(self): + self.ensure_one() + return self.dtd_version + + def _get_cxml_dtd_declaration(self): + self.ensure_one() + version = self._get_cxml_version() + dtd_link = f"http://xml.cxml.org/schemas/cXML/{version}/cXML.dtd" + declaration = f'' + return declaration diff --git a/punchout/models/punchout_request.py b/punchout/models/punchout_session.py similarity index 57% rename from punchout/models/punchout_request.py rename to punchout/models/punchout_session.py index 7b2e1685c71..6d6e2ecce4a 100644 --- a/punchout/models/punchout_request.py +++ b/punchout/models/punchout_session.py @@ -5,14 +5,17 @@ import os import random import time +from base64 import b64decode from datetime import datetime +from io import StringIO from urllib.parse import urlparse from uuid import uuid4 import lxml.etree as ET import pytz import requests -from cryptography.fernet import Fernet, InvalidToken +from dateutil.relativedelta import relativedelta +from lxml.etree import XMLSyntaxError from odoo import _, api, fields, models from odoo.exceptions import UserError @@ -20,13 +23,13 @@ _logger = logging.getLogger(__name__) -class PunchoutRequest(models.Model): - _name = "punchout.request" +class PunchoutSession(models.Model): + _name = "punchout.session" _inherit = [ "mail.thread", ] _order = "create_date desc" - _description = "Punchout Request" + _description = "Punchout Session" backend_id = fields.Many2one(comodel_name="punchout.backend", readonly=True,) user_id = fields.Many2one( @@ -34,9 +37,18 @@ class PunchoutRequest(models.Model): ) buyer_cookie_id = fields.Char(readonly=True, string="Cookie") punchout_url = fields.Char(readonly=True, string="Start URL") - cxml_request = fields.Text(readonly=True, string="Request",) + cxml_setup_request = fields.Text(readonly=True, string="Setup Request",) + cxml_setup_request_response = fields.Text( + readonly=True, string="Setup Request Response", + ) cxml_response = fields.Text(readonly=True, string="Response",) cxml_response_date = fields.Datetime(readonly=True, string="Response Date",) + expiration_date = fields.Datetime( + compute="_compute_expiration_date", + store=True, + readonly=True, + compute_sudo=True, + ) error_message = fields.Text(readonly=True,) state = fields.Selection( selection="_selection_state", default="draft", tracking=True, @@ -48,6 +60,16 @@ def _compute_action_process_allowed(self): for rec in self: rec.action_process_allowed = rec.state in ("to_process", "error") + @api.depends( + "backend_id", "create_date", + ) + def _compute_expiration_date(self): + for rec in self: + ref_date = rec.create_date or fields.Datetime.now() + rec.expiration_date = ref_date + relativedelta( + seconds=rec.backend_id.session_duration + ) + @api.model def _selection_state(self): return [ @@ -78,55 +100,36 @@ def _get_punchout_payload_identity(self): random_numbers = "".join(map(str, random_numbers_list)) return f"{timestamp}{pid}{random_numbers}@{domain}" - @api.model - def _get_punchout_request_header_credential(self, backend, header, credential_type): - """ - Adds the asked credential tag in the header. - :param backend: punchout.backend - :param header: xml.etree.ElementTree.Element (header) - :param credential_type: "From", "To" or "Sender" - :return: xml.etree.ElementTree.Element (header) - """ - header_cred_type = ET.SubElement(header, credential_type) - cred_domain, cred_identity = backend._get_domain_and_identity(credential_type) - header_cred = ET.SubElement( - header_cred_type, "Credential", attrib={"domain": cred_domain} - ) - ET.SubElement(header_cred, "Identity").text = cred_identity - if credential_type == "Sender": - ET.SubElement(header_cred, "SharedSecret").text = backend.shared_secret - ET.SubElement(header_cred_type, "UserAgent").text = backend.user_agent - return header - - def _get_punchout_request_header(self, cxml, backend): - """ - Adds the header tag in the cXML PunchOut request element. - :param cxml: xml.etree.ElementTree.Element (cxml) - :param backend: punchout.backend - :return: xml.etree.ElementTree.Element (cxml) - """ - header = ET.SubElement(cxml, "Header") - self._get_punchout_request_header_credential(backend, header, "From") - self._get_punchout_request_header_credential(backend, header, "To") - self._get_punchout_request_header_credential(backend, header, "Sender") - return cxml - def _get_punchout_buyer_cookie(self): return f"{self.env.user.id}-{uuid4()}" - def _get_encrypted_punchout_buyer_cookie(self, backend, buyer_cookie): - encyrption_key = backend.buyer_cookie_encryption_key.encode() - return Fernet(encyrption_key).encrypt(buyer_cookie.encode("utf-8")) - def _get_punchout_request_user_email(self): return self.env.user.email - def _get_punchout_request_setup(self, backend, buyer_cookie_id): - payload_id = self._get_punchout_payload_identity() - request_timestamp = self._get_punchout_request_timestamp() - encrypted_buyer_cookie_id = self._get_encrypted_punchout_buyer_cookie( - backend, buyer_cookie_id + def _render_cxml_operation(self, template_xml_id, template_values=None): + self.ensure_one() + backend = self.backend_id + template_values = template_values or {} + template_values.update( + {"session": self, "backend": backend, "user": self.env.user} ) + cxml = ( + self.env["ir.ui.view"] + .sudo() + .render_template(template_xml_id, template_values) + ) + cxml_request_element = ET.fromstring(cxml) + ET.indent(cxml_request_element) + cxml_request_str = ET.tostring( + cxml_request_element, + encoding="UTF-8", + xml_declaration=True, + pretty_print=True, + doctype=backend._get_cxml_dtd_declaration(), + ).decode("utf-8") + return cxml_request_str + + def _get_punchout_request_setup(self, session): user_email = self._get_punchout_request_user_email() if not user_email: raise UserError( @@ -135,33 +138,10 @@ def _get_punchout_request_setup(self, backend, buyer_cookie_id): "in order to access this feature." ) ) - cxml = ET.Element( - "cXML", - attrib={ - "version": "1.2.008", - "{http://www.w3.org/XML/1998/namespace}lang": "en", - "payloadID": payload_id, - "timestamp": request_timestamp, - }, - ) - self._get_punchout_request_header(cxml, backend) - - deployment_mode = backend.deployment_mode - request = ET.SubElement( - cxml, "Request", attrib={"deploymentMode": deployment_mode} - ) - setup_request = ET.SubElement( - request, "PunchOutSetupRequest", attrib={"operation": "create"} + cxml_request_str = session._render_cxml_operation( + "punchout.cxml_punchout_PunchOutSetupRequest" ) - ET.SubElement(setup_request, "BuyerCookie").text = encrypted_buyer_cookie_id - ET.SubElement( - setup_request, "Extrinsic", attrib={"name": "UserEmail"} - ).text = user_email - browser_form_post = ET.SubElement(setup_request, "BrowserFormPost") - ET.SubElement( - browser_form_post, "URL" - ).text = backend._get_browser_form_post_url() - return cxml + return cxml_request_str @api.model def _check_punchout_request_ok(self, response): @@ -179,15 +159,8 @@ def _check_punchout_request_ok(self, response): f"URL: {response.url}" ) _logger.error(log_msg) - raise UserError( - _( - "The PunchOut request with URL {url} returned " - "{status_code} ({reason})." - ).format( - url=response.url, - status_code=response.status_code, - reason=response.reason, - ) + self.env.user.notify_warning( + title=_("PunchOut request error"), message=log_msg, sticky=True, ) if not 200 <= cxml_status_code <= 400: log_msg = ( @@ -208,21 +181,10 @@ def _check_punchout_request_ok(self, response): ) return res - def _get_post_punchout_setup_url(self, request, buyer_cookie_id): - punchout_backend = request.backend_id + def _get_post_punchout_setup_url(self, session): + punchout_backend = session.backend_id punchout_setup_url = punchout_backend.url - cxml_request_element = self._get_punchout_request_setup( - punchout_backend, buyer_cookie_id - ) - cxml_request_str = ET.tostring( - cxml_request_element, - encoding="UTF-8", - xml_declaration=True, - pretty_print=True, - doctype='', - ).decode("utf-8") - request.write({"cxml_request": cxml_request_str}) + cxml_request_str = self._get_punchout_request_setup(session) _logger.info("PunchOut %s: posting setup request", self._name) response = requests.post( punchout_setup_url, @@ -230,9 +192,18 @@ def _get_post_punchout_setup_url(self, request, buyer_cookie_id): headers={"Content-Type": "text/xml"}, timeout=30, ) + response_tree = ET.fromstring(response.content) + session.write( + { + "cxml_setup_request": cxml_request_str, + "cxml_setup_request_response": ET.tostring( + response_tree, pretty_print=True + ), + } + ) if not self._check_punchout_request_ok(response): return {} - response_tree = ET.fromstring(response.content) + start_page_url = "" for url in response_tree.findall( "./Response/PunchOutSetupResponse/StartPage/URL" @@ -242,27 +213,30 @@ def _get_post_punchout_setup_url(self, request, buyer_cookie_id): @api.model def _redirect_to_punchout(self): - request = self.sudo()._create_punchout_request() + session = self.sudo()._create_punchout_session() + if not session.punchout_url: + return False return { "type": "ir.actions.act_url", - "url": request.punchout_url, + "url": session.punchout_url, "target": "new", } @api.model - def _create_punchout_request(self): + def _create_punchout_session(self): punchout_backend = self._get_punchout_backend_to_use() buyer_cookie_id = self._get_punchout_buyer_cookie() - request = self.env["punchout.request"].create( + session = self.env["punchout.session"].create( { "user_id": self.env.user.id, "buyer_cookie_id": buyer_cookie_id, "backend_id": punchout_backend.id, } ) - url = self._get_post_punchout_setup_url(request, buyer_cookie_id) - request.write({"punchout_url": url}) - return request + url = self._get_post_punchout_setup_url(session) + if url: + session.write({"punchout_url": url}) + return session @api.model def _get_punchout_backend_to_use(self): @@ -279,47 +253,96 @@ def _get_punchout_backend_to_use(self): return backend @api.model - def _store_punchout_request(self, backend_id, cxml_string): - backend = self.env["punchout.backend"].sudo().browse(backend_id) - tree = ET.fromstring(cxml_string.encode()) - buyer_cookie = tree.find(".//BuyerCookie") - encrypted_buyer_cookie_id = ( - buyer_cookie.text.strip() if buyer_cookie is not None else False + def _store_punchout_session_response(self, backend_id, cxml_string): + cxml = cxml_string.encode() + tree = ET.fromstring(cxml) + buyer_cookie_elem = tree.find(".//BuyerCookie") + buyer_cookie_id = ( + buyer_cookie_elem.text.strip() if buyer_cookie_elem is not None else "" ) - if not encrypted_buyer_cookie_id: + if not buyer_cookie_id: _logger.error( "Unable to find a buyer cookie from the cXml punchout response \n%s", ET.tostring(tree, pretty_print=True), ) return False - encryption_key = backend.buyer_cookie_encryption_key.encode() - fernet = Fernet(encryption_key) - encrypted_buyer_cookie_id_b = encrypted_buyer_cookie_id.encode() - - try: - buyer_cookie_id = fernet.decrypt(encrypted_buyer_cookie_id_b).decode() - except InvalidToken: - _logger.error( - "Unable to decode given buyer cookie. %s", encrypted_buyer_cookie_id - ) - return False - request = self.sudo().search( - [("buyer_cookie_id", "=", buyer_cookie_id)], limit=1, + session = self.sudo().search( + [ + ("buyer_cookie_id", "=", buyer_cookie_id), + ("backend_id", "=", backend_id), + ], + limit=1, ) - if not request: + if not session: _logger.error( "Unable to find a request with given buyer cookie %s", buyer_cookie_id ) return False - request.with_user(request.user_id).sudo().write( + + session.write( { "cxml_response": ET.tostring(tree, pretty_print=True), "cxml_response_date": fields.Datetime.now(), - "state": "to_process", } ) - return request + xml_validation = session._validate_cxml() + is_valid = xml_validation.get("valid") + if is_valid: + session.write({"state": "to_process"}) + session._notify_punchout_response_received() + else: + session.write( + {"state": "error", "error_message": xml_validation.get("error")} + ) + session._notify_response_validation_error() + if session.expiration_date <= fields.Datetime.now(): + session.write( + {"state": "error", "error_message": "punchout.session expired"} + ) + return session + + def _validate_cxml(self): + self.ensure_one() + cxml = self.cxml_response + tree = ET.fromstring(cxml) + backend = self.backend_id + dtd_data = backend.dtd_file + if not dtd_data: + return {"valid": True} + try: + dtd_file = b64decode(dtd_data).decode() + dtd_io = StringIO(dtd_file) + dtd = ET.DTD(dtd_io) + dtd.validate(tree) + dtd_io.close() + except XMLSyntaxError as e: + _logger.exception(e) + return { + "valid": False, + "error": e.msg, + } + return { + "valid": True, + } + + def _notify_punchout_response_received(self): + self.ensure_one() + user = self.user_id + user.with_user(user).notify_info( + title=_("Processing the request"), + message=_("The response has been received and will be processed."), + sticky=False, + ) + + def _notify_response_validation_error(self): + self.ensure_one() + user = self.user_id + user.with_user(user).notify_warning( + title=_("Unable to process the request"), + message=_("The response received from the backend is not valid."), + sticky=False, + ) def _check_action_process_allowed(self): for rec in self: diff --git a/punchout/security/punchout_request.xml b/punchout/security/punchout_session.xml similarity index 67% rename from punchout/security/punchout_request.xml rename to punchout/security/punchout_session.xml index 2f54ce01ffd..1c0ae94010a 100644 --- a/punchout/security/punchout_request.xml +++ b/punchout/security/punchout_session.xml @@ -2,9 +2,9 @@ - - punchout.request access manager - + + punchout.session access manager + diff --git a/punchout/tests/common.py b/punchout/tests/common.py index 83c4a20b4a6..4825fe534e6 100644 --- a/punchout/tests/common.py +++ b/punchout/tests/common.py @@ -11,8 +11,7 @@ class TestPunchoutCommon(SavepointCase): def setUpClass(cls): super().setUpClass() cls.backend_model = cls.env["punchout.backend"] - cls.request_model = cls.env["punchout.request"] - cls.buyer_cookie_encryption_key = "9Ndn3znJUntZpwF51nXsMokf1Xt0X3jjolMX-AD5_W0=" + cls.session_model = cls.env["punchout.session"] cls.backend = cls.backend_model.create( { "name": uuid4(), @@ -25,11 +24,10 @@ def setUpClass(cls): "user_agent": "/", "deployment_mode": "test", "browser_form_post_url": "/punchout/cxml/receive/", - "buyer_cookie_encryption_key": cls.buyer_cookie_encryption_key, } ) - cls.request = cls.request_model.create( + cls.session = cls.session_model.create( { "backend_id": cls.backend.id, "buyer_cookie_id": "2-cc162436-fcab-4cfb-888d-abfd8708520d", @@ -43,4 +41,6 @@ def _get_response_xml_content(self, filepath, filename): return content.decode() def _store_response(self, cxml_string): - return self.request_model._store_punchout_request(self.backend.id, cxml_string,) + return self.session_model._store_punchout_session_response( + self.backend.id, cxml_string, + ) diff --git a/punchout/tests/cxml/test_store_request.xml b/punchout/tests/cxml/test_store_request.xml index e06b9428f6f..9f33439a2d0 100644 --- a/punchout/tests/cxml/test_store_request.xml +++ b/punchout/tests/cxml/test_store_request.xml @@ -21,8 +21,7 @@ - gAAAAABlEZQA3rzR0dp8XxpC0cAf7zlPZmFavA9hfy67iNkl5KBPSvWjIVlxL1rSnYIdDg_qF5txVul5j5bTISSQjC0l66lxtPVy1VSKrnbAXL6-GwKYZBmHMVx85kNBrp7XmmRmeFZk + 2-cc162436-fcab-4cfb-888d-abfd8708520d 166.3900 diff --git a/punchout/tests/cxml/test_unknown_buyer_cookie_id.xml b/punchout/tests/cxml/test_unknown_buyer_cookie_id.xml index 4467595e75e..297611200ea 100644 --- a/punchout/tests/cxml/test_unknown_buyer_cookie_id.xml +++ b/punchout/tests/cxml/test_unknown_buyer_cookie_id.xml @@ -22,7 +22,7 @@ - gAAAAABlEZ7rysdjEwnI_1kDvjugV9aZ7FUV1Dm8gk4o6mtFd-2I0he19jpvi1uURwzXfDASK_TFpnBf4DJoCHKhGz2m_pUgoTnUlH2iPq9BbrYtRn7sVUNuoZvDKvwLb9mxFQgNaE8Y + 2-cc162436-fcab-4cfb-888d-abfd8708520d-false diff --git a/punchout/tests/test_punchout.py b/punchout/tests/test_punchout.py index bdf8959d277..41985a7fd60 100644 --- a/punchout/tests/test_punchout.py +++ b/punchout/tests/test_punchout.py @@ -11,43 +11,12 @@ class TestPunchout(TestPunchoutCommon): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.backend_model = cls.env["punchout.backend"] - cls.request_model = cls.env["punchout.request"] - - cls.backend = cls.backend_model.create( - { - "name": "/", - "from_domain": "from", - "from_identity": "from", - "to_domain": "to", - "to_identity": "to", - "url": "/", - "shared_secret": "/", - "user_agent": "/", - "deployment_mode": "test", - "browser_form_post_url": "/punchout/cxml/receive/", - "buyer_cookie_encryption_key": ( - "9Ndn3znJUntZpwF51nXsMokf1Xt0X3jjolMX-AD5_W0=" - ), - } - ) - - cls.request = cls.request_model.create( - { - "backend_id": cls.backend.id, - "buyer_cookie_id": "2-cc162436-fcab-4cfb-888d-abfd8708520d", - } - ) - def test_store_request(self): cxml_string = self._get_response_xml_content(PATH, "test_store_request.xml") - request = self._store_response(cxml_string) - request.invalidate_cache([]) - self.assertEqual(request.state, "to_process") - self.assertTrue(bool(request.cxml_response)) + session = self._store_response(cxml_string) + session.invalidate_cache([]) + self.assertEqual(session.state, "to_process") + self.assertTrue(bool(session.cxml_response)) @mute_logger("odoo.addons.punchout.models.punchout_request") def test_unknown_buyer_cookie_id(self): diff --git a/punchout/views/punchout_backend.xml b/punchout/views/punchout_backend.xml index 655d01134e5..4cf1da35121 100644 --- a/punchout/views/punchout_backend.xml +++ b/punchout/views/punchout_backend.xml @@ -36,7 +36,10 @@ - + + + +
diff --git a/punchout/views/punchout_request.xml b/punchout/views/punchout_session.xml similarity index 67% rename from punchout/views/punchout_request.xml rename to punchout/views/punchout_session.xml index 547b30c027f..b311529099e 100644 --- a/punchout/views/punchout_request.xml +++ b/punchout/views/punchout_session.xml @@ -2,9 +2,9 @@ - - punchout.request.form (in punchout) - punchout.request + + punchout.session.form (in punchout) + punchout.session
@@ -28,7 +28,7 @@ class="alert alert-danger" role="alert" > - An error occured during the process of the request. + An error occured during the process of the session.
@@ -40,16 +40,20 @@ + - - - - - + + + + + + + + - - punchout.request.search (in punchout) - punchout.request + + punchout.session.search (in punchout) + punchout.session @@ -77,9 +81,9 @@ - - punchout.request.tree (in punchout) - punchout.request + + punchout.session.tree (in punchout) + punchout.session - - PunchOut Requests - punchout.request + + PunchOut sessions + punchout.session tree,form [] {} - - PunchOut Requests + + PunchOut sessions - + diff --git a/punchout_environment/models/punchout_backend.py b/punchout_environment/models/punchout_backend.py index 6743b8d7f01..00a00a5ebfb 100644 --- a/punchout_environment/models/punchout_backend.py +++ b/punchout_environment/models/punchout_backend.py @@ -20,5 +20,4 @@ def _server_env_fields(self): "user_agent": {}, "deployment_mode": {}, "browser_form_post_url": {}, - "buyer_cookie_encryption_key": {}, } diff --git a/punchout_queue_job/__manifest__.py b/punchout_queue_job/__manifest__.py index e53e8849442..5fb9900f2b4 100644 --- a/punchout_queue_job/__manifest__.py +++ b/punchout_queue_job/__manifest__.py @@ -13,6 +13,6 @@ # oca/edi "punchout", ], - "data": ["views/punchout_request.xml"], + "data": ["views/punchout_session.xml"], "demo": [], } diff --git a/punchout_queue_job/models/__init__.py b/punchout_queue_job/models/__init__.py index 733a40f2aec..83ae1636b4e 100644 --- a/punchout_queue_job/models/__init__.py +++ b/punchout_queue_job/models/__init__.py @@ -1 +1 @@ -from . import punchout_request +from . import punchout_session diff --git a/punchout_queue_job/models/punchout_request.py b/punchout_queue_job/models/punchout_session.py similarity index 86% rename from punchout_queue_job/models/punchout_request.py rename to punchout_queue_job/models/punchout_session.py index 9e61832f702..6d93bc42946 100644 --- a/punchout_queue_job/models/punchout_request.py +++ b/punchout_queue_job/models/punchout_session.py @@ -9,8 +9,8 @@ _logger = logging.getLogger(__name__) -class PunchoutRequest(models.Model): - _inherit = "punchout.request" +class PunchoutSession(models.Model): + _inherit = "punchout.session" has_job_access = fields.Boolean(compute="_compute_has_job_access",) job_count = fields.Integer(compute="_compute_job_count",) @@ -48,9 +48,9 @@ def action_show_job_list(self): action.update({"domain": self._get_queue_job_list_domain(), "context": {}}) return action - def _store_punchout_request(self, *args, **kwargs): - request = super()._store_punchout_request(*args, **kwargs) - if request: + def _store_punchout_session_response(self, *args, **kwargs): + request = super()._store_punchout_session_response(*args, **kwargs) + if request and request.state == "to_process": request.with_user(request.user_id).with_delay( description=request._get_queue_job_description() ).action_process() diff --git a/punchout_queue_job/views/punchout_request.xml b/punchout_queue_job/views/punchout_session.xml similarity index 80% rename from punchout_queue_job/views/punchout_request.xml rename to punchout_queue_job/views/punchout_session.xml index 97696174418..7a8951075e7 100644 --- a/punchout_queue_job/views/punchout_request.xml +++ b/punchout_queue_job/views/punchout_session.xml @@ -2,9 +2,9 @@ - - punchout.request - + + punchout.session + diff --git a/requirements.txt b/requirements.txt index 0cf3e25f667..ea466fcc0dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ # generated from manifests external_dependencies -cryptography factur-x lxml pyyaml