diff --git a/report_qweb_encrypt/__manifest__.py b/report_qweb_encrypt/__manifest__.py index 13eb7269..347f2012 100644 --- a/report_qweb_encrypt/__manifest__.py +++ b/report_qweb_encrypt/__manifest__.py @@ -1,21 +1,21 @@ # Copyright 2020 Creu Blanca +# Copyright 2020 Ecosoft Co., Ltd. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - 'name': 'Report Qweb Encrypt', - 'summary': """ - Allow to encrypt qweb pdfs""", - 'version': '12.0.1.0.0', - 'license': 'AGPL-3', - 'author': 'Creu Blanca,Odoo Community Association (OCA)', - 'website': 'https://github.com/OCA/reporting-engine', - 'depends': [ - 'web', + "name": "Report Qweb Encrypt", + "summary": "Allow to encrypt qweb pdfs", + "version": "12.0.1.0.0", + "license": "AGPL-3", + "author": "Creu Blanca,Ecosoft,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/reporting-engine", + "depends": [ + "web", ], - 'data': [ - 'views/ir_actions_report.xml', - 'templates/assets.xml' - ], - 'demo': [ + "data": [ + "views/ir_actions_report.xml", + "templates/assets.xml" ], + "installable": True, + "maintainers": ["kittiu"], } diff --git a/report_qweb_encrypt/controllers/main.py b/report_qweb_encrypt/controllers/main.py index 0d1c8c56..4bbdceeb 100644 --- a/report_qweb_encrypt/controllers/main.py +++ b/report_qweb_encrypt/controllers/main.py @@ -1,42 +1,33 @@ +# Copyright 2020 Creu Blanca +# Copyright 2020 Ecosoft Co., Ltd. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo.addons.web.controllers import main as report -from odoo.http import route +from odoo.http import route, request from werkzeug.urls import url_decode import json -import logging -from io import BytesIO - -_logger = logging.getLogger(__name__) -try: - from PyPDF2 import PdfFileReader, PdfFileWriter -except ImportError as err: - _logger.debug(err) class ReportController(report.ReportController): @route() def report_download(self, data, token): result = super().report_download(data, token) + # When report is downloaded from print action, this function is called, + # but this function cannot pass context (manually entered password) to + # report.render_qweb_pdf(), encrypton for manual password is done here. requestcontent = json.loads(data) - url, type = requestcontent[0], requestcontent[1] + url, ttype = requestcontent[0], requestcontent[1] if ( - type in ['qweb-pdf'] and - result.headers['Content-Type'] == "application/pdf" and - '?' in url + ttype in ["qweb-pdf"] and + result.headers["Content-Type"] == "application/pdf" and + "?" in url ): - url_data = dict(url_decode(url.split('?')[1]).items()) - if 'context' in url_data: - context_data = json.loads(url_data['context']) - if 'encrypt_password' in context_data: - # We need to encrypt here because this function is not - # passing context, so we need to implement this again - + url_data = dict(url_decode(url.split("?")[1]).items()) + if "context" in url_data: + context = json.loads(url_data["context"]) + if "encrypt_password" in context: + Report = request.env["ir.actions.report"] data = result.get_data() - output_pdf = PdfFileWriter() - in_buff = BytesIO(data) - pdf = PdfFileReader(in_buff) - output_pdf.appendPagesFromReader(pdf) - output_pdf.encrypt(context_data['encrypt_password']) - buff = BytesIO() - output_pdf.write(buff) - result.set_data(buff.getvalue()) + encrypted_data = Report._encrypt_pdf( + data, context["encrypt_password"]) + result.set_data(encrypted_data) return result diff --git a/report_qweb_encrypt/models/ir_actions_report.py b/report_qweb_encrypt/models/ir_actions_report.py index 93c2aaa3..06921845 100644 --- a/report_qweb_encrypt/models/ir_actions_report.py +++ b/report_qweb_encrypt/models/ir_actions_report.py @@ -1,9 +1,13 @@ # Copyright 2020 Creu Blanca +# Copyright 2020 Ecosoft Co., Ltd. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import fields, models +import time import logging +from odoo import fields, models, _ from io import BytesIO +from odoo.tools.safe_eval import safe_eval +from odoo.exceptions import ValidationError + _logger = logging.getLogger(__name__) try: @@ -13,21 +17,56 @@ except ImportError as err: class IrActionsReport(models.Model): + _inherit = "ir.actions.report" - _inherit = 'ir.actions.report' - - encrypt = fields.Boolean() + encrypt = fields.Selection( + [("manual", "Manual Input Password"), + ("auto", "Auto Generated Password")], + string="Encryption", + help="* Manual Input Password: allow user to key in password on the fly. " + "This option available only on document print action.\n" + "* Auto Generated Password: system will auto encrypt password when PDF " + "created, based on provided python syntax." + ) + encrypt_password = fields.Char( + help="Python code syntax to gnerate password.", + ) def render_qweb_pdf(self, res_ids=None, data=None): - document, type = super(IrActionsReport, self).render_qweb_pdf( + document, ttype = super(IrActionsReport, self).render_qweb_pdf( res_ids=res_ids, data=data) - if self.encrypt and self.env.context.get('encrypt_password', False): - output_pdf = PdfFileWriter() - in_buff = BytesIO(document) - pdf = PdfFileReader(in_buff) - output_pdf.appendPagesFromReader(pdf) - output_pdf.encrypt(self.env.context.get('encrypt_password')) - buff = BytesIO() - output_pdf.write(buff) - document = buff.getvalue() - return document, type + password = self._get_pdf_password(res_ids[:1]) + document = self._encrypt_pdf(document, password) + return document, ttype + + def _get_pdf_password(self, res_id): + encrypt_password = False + if self.encrypt == "manual": + # If use document print action, report_download() is called, + # but that can't pass context (encrypt_password) here. + # As such, file will be encrypted by report_download() again. + # -- + # Following is used just in case when context is passed in. + encrypt_password = self._context.get("encrypt_password", False) + elif self.encrypt == "auto" and self.encrypt_password: + obj = self.env[self.model].browse(res_id) + try: + encrypt_password = safe_eval(self.encrypt_password, + {'object': obj, 'time': time}) + except: + raise ValidationError( + _("Python code used for encryption password is invalid.\n%s") + % self.encrypt_password) + return encrypt_password + + def _encrypt_pdf(self, data, password): + if not password: + return data + output_pdf = PdfFileWriter() + in_buff = BytesIO(data) + pdf = PdfFileReader(in_buff) + output_pdf.appendPagesFromReader(pdf) + output_pdf.encrypt(password) + buff = BytesIO() + output_pdf.write(buff) + return buff.getvalue() diff --git a/report_qweb_encrypt/readme/CONTRIBUTORS.rst b/report_qweb_encrypt/readme/CONTRIBUTORS.rst index 94e7b0a4..b7e5dfa7 100644 --- a/report_qweb_encrypt/readme/CONTRIBUTORS.rst +++ b/report_qweb_encrypt/readme/CONTRIBUTORS.rst @@ -1,2 +1,3 @@ * Enric Tobella * Jaime Arroyo +* Kitti U. diff --git a/report_qweb_encrypt/readme/DESCRIPTION.rst b/report_qweb_encrypt/readme/DESCRIPTION.rst index f111d795..2f31e4e9 100644 --- a/report_qweb_encrypt/readme/DESCRIPTION.rst +++ b/report_qweb_encrypt/readme/DESCRIPTION.rst @@ -1,2 +1,4 @@ -This module allows you to encrypt pdf files with a password when -downloading them. +This module allow you to encrypt PDF with a password seting option, + +* Manually keyin password (only applicable for record print action) +* Auto generated password based on object data (python) diff --git a/report_qweb_encrypt/readme/USAGE.rst b/report_qweb_encrypt/readme/USAGE.rst index aed8cb93..1704c796 100644 --- a/report_qweb_encrypt/readme/USAGE.rst +++ b/report_qweb_encrypt/readme/USAGE.rst @@ -1 +1 @@ -To make a report encryptable mark the field `Encryptable` in the report record. +To make a report encryptable mark the field `Encryption` in the report record. diff --git a/report_qweb_encrypt/static/src/js/report/action_manager_report.js b/report_qweb_encrypt/static/src/js/report/action_manager_report.js index e70ae09b..5e45630e 100644 --- a/report_qweb_encrypt/static/src/js/report/action_manager_report.js +++ b/report_qweb_encrypt/static/src/js/report/action_manager_report.js @@ -11,7 +11,7 @@ odoo.define("report_qweb_encrypt.Dialog", function (require) { var _t = core._t; var EncryptDialog = Dialog.extend({ - events: _.extend({} , Dialog.prototype.events, { + events: _.extend({}, Dialog.prototype.events, { change: '_onChange', }), _setValue: function () { @@ -49,11 +49,13 @@ odoo.define("report_qweb_encrypt.Dialog", function (require) { ActionManager.include({ _executeReportAction: function (action, options, password) { - if (action.encrypt && password === undefined) { + if (action.encrypt === 'manual' + && action.report_type === 'qweb-pdf' + && password === undefined) { EncryptDialog.askPassword(this, action, options); - return $.Deferred() + return $.Deferred(); } - else if (action.encrypt) { + else if (action.encrypt === 'manual') { action.context = _.extend({}, action.context, { encrypt_password: password, }) @@ -62,7 +64,7 @@ odoo.define("report_qweb_encrypt.Dialog", function (require) { }, _makeReportUrls: function (action) { var reportUrls = this._super.apply(this, arguments); - if (action.encrypt && action.context.encrypt_password) { + if (action.encrypt === 'manual' && action.context.encrypt_password) { if (_.isUndefined(action.data) || _.isNull(action.data) || (_.isObject(action.data) && _.isEmpty(action.data))) { var serializedOptionsPath = '?context=' + encodeURIComponent(JSON.stringify({ diff --git a/report_qweb_encrypt/tests/__init__.py b/report_qweb_encrypt/tests/__init__.py new file mode 100644 index 00000000..c6a7ce0a --- /dev/null +++ b/report_qweb_encrypt/tests/__init__.py @@ -0,0 +1 @@ +from . import test_report_qweb_encrypt diff --git a/report_qweb_encrypt/tests/test_report_qweb_encrypt.py b/report_qweb_encrypt/tests/test_report_qweb_encrypt.py new file mode 100644 index 00000000..8a8a1f2d --- /dev/null +++ b/report_qweb_encrypt/tests/test_report_qweb_encrypt.py @@ -0,0 +1,33 @@ +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.exceptions import ValidationError +from odoo.tests.common import HttpCase + + +class TestReportQwebEncrypt(HttpCase): + + def test_report_qweb_no_encrypt(self): + ctx = {"force_report_rendering": True} + report = self.env.ref("web.action_report_internalpreview") + report.encrypt = False + pdf, _ = report.with_context(ctx).render_qweb_pdf([1]) + self.assertFalse(pdf.count(b"/Encrypt")) + + def test_report_qweb_auto_encrypt(self): + ctx = {"force_report_rendering": True} + report = self.env.ref("web.action_report_internalpreview") + report.encrypt = "auto" + report.encrypt_password = False + # If no encrypt_password, still not encrypted + pdf, _ = report.with_context(ctx).render_qweb_pdf([1]) + self.assertFalse(pdf.count(b"/Encrypt")) + # If invalid encrypt_password, show error + report.encrypt_password = "invalid python syntax" + with self.assertRaises(ValidationError): + pdf, _ = report.with_context(ctx).render_qweb_pdf([1]) + # Valid python string for password + report.encrypt_password = "'secretcode'" + pdf, _ = report.with_context(ctx).render_qweb_pdf([1]) + self.assertTrue(pdf.count(b"/Encrypt")) + + # TODO: test_report_qweb_manual_encrypt, require JS test? diff --git a/report_qweb_encrypt/views/ir_actions_report.xml b/report_qweb_encrypt/views/ir_actions_report.xml index a596e04c..fdff60b6 100644 --- a/report_qweb_encrypt/views/ir_actions_report.xml +++ b/report_qweb_encrypt/views/ir_actions_report.xml @@ -1,5 +1,6 @@ @@ -10,11 +11,15 @@ - + - -