From 84f528bcff12e39b5ed172a4789aab27c9f89a19 Mon Sep 17 00:00:00 2001 From: Mitali Patel Date: Fri, 15 Dec 2017 14:38:02 +0530 Subject: [PATCH] [IMP] Portal - Share link : Easily share the url of a document Purpose ======= - Quickly share the url to someone else (a client, a colleague,...) - Ensure that the recipient can access at least the portal view of the shared record. - Typically used when a client cannot retrieve the mail to access his order. The share link can be used in this case. Specifications ============== For any object inheriting form portal.mixin: - Add a button SHARE (not visible in edit mode) - When clicking on this button, a popup opens with : - A warning message for tasks and projects only (see below) - the link (like in gmail) that can be copied - Recipients - mail composer (with preselected template) ==> see below - button [Send Link] [Copy Link] Discard - After sharing document, put internal note like "Document shared to xyz,...." with template message - Anyone with the link, even anonymous user (not logged in) can have access to the document with the access token provided in the url. Impacted models: - account.invoice (Community) - project.project (Community) - project.task (Community) - purchase.order (Community) - sale.order (Community) - helpdesk.ticket (Enterprise) Warning messages and access rules: Allowed : - SO canceled or draft will be accessible with the link with access_token - If the customer account is B2B (signup not enabled), the recipient will anyway see the document as the user specifically wants the recipient to see the document. Restrictions : - For Project and Task, if the privacy is not public, then, there is a contradiction between the access_token mechanism and the privacy of the document. - A warning message will be displayed in the share wizard to inform the user if the document cannot be visible by the recipients and to ask him to set the privacy to 'Visible by following customer'. The send button will, in that case, be hidden. - To avoid to block the share for a new project, default privacy value is now set to 'Visible by followong customer' Technical implementation ======================== - Move the access_token mechanism (field + methods + mail controller) to the portal.mixin to be able to use it in a generic way for each object inheriting the portal.mixin - Generalise a part of the _*model*_get_page_view_values method into a single one in portal - Generalize the _*model*_check_access into the portal controller of the portal module - Remove the init_column + default value for the access_token > old records have an access_token, > new one won't but it will be generated on demand via the get_access_token Done for performance reasons - Add share button into action menu separately. + kanban view context menu (except for task and project where button not in action menu but 'simple' button for task and project because other modules already provide action to send documents by email, which is not the case for project and task.) - Add a sign_token used to authentify the recipient in the portal view chatter, if any. The message will be posted as if the user was logged in. - Set the _get_share_url as private for security reason - Add a redirect parameter to _get_share_url to get If false : The direct portal view url If True : The redirect url (mail/view/?) - Cleaning up unnecessary code - Bug fix : - Before, if user was not logged and record had partner_id, if partner id was null, post message was done as admin. Now, the post message is done as public user. - If the user had an uid but had no access_token, he could be able to gain the access token of the record. check_access_rights was missing in the get_access_action. Task ID : 30985 Closes #25629 --- addons/account/controllers/__init__.py | 1 - addons/account/controllers/mail.py | 30 ---- addons/account/controllers/portal.py | 32 +---- addons/account/data/account_data.xml | 8 ++ addons/account/models/account_invoice.py | 66 +-------- addons/account/views/account_invoice_view.xml | 1 - .../views/account_portal_templates.xml | 2 + addons/mail/controllers/main.py | 6 +- addons/mail/data/mail_data.xml | 4 +- addons/portal/__manifest__.py | 1 + addons/portal/controllers/mail.py | 51 ++++++- addons/portal/controllers/portal.py | 42 +++++- addons/portal/data/portal_data.xml | 10 ++ addons/portal/models/portal_mixin.py | 135 +++++++++++++----- .../portal/static/src/xml/portal_chatter.xml | 2 + addons/portal/views/portal_templates.xml | 2 +- addons/portal/wizard/__init__.py | 1 + addons/portal/wizard/portal_share.py | 74 ++++++++++ addons/portal/wizard/portal_share_views.xml | 35 +++++ addons/project/controllers/portal.py | 58 +++++--- addons/project/models/project.py | 74 +++------- .../views/project_portal_templates.xml | 3 + addons/project/views/project_views.xml | 9 +- addons/purchase/controllers/portal.py | 23 +-- addons/purchase/data/purchase_data.xml | 8 ++ addons/purchase/models/purchase.py | 11 +- addons/purchase/views/purchase_views.xml | 1 - addons/sale/controllers/__init__.py | 1 - addons/sale/controllers/mail.py | 30 ---- addons/sale/controllers/portal.py | 40 +----- addons/sale/data/sale_data.xml | 9 ++ addons/sale/models/sale.py | 70 +-------- addons/web/static/src/scss/fields.scss | 4 + addons/web/static/src/xml/base.xml | 4 +- .../static/tests/fields/basic_fields_tests.js | 14 +- addons/website_quote/controllers/main.py | 10 +- addons/website_quote/models/sale_order.py | 4 +- .../website_sale_digital/controllers/main.py | 6 +- 38 files changed, 473 insertions(+), 409 deletions(-) delete mode 100644 addons/account/controllers/mail.py create mode 100644 addons/portal/wizard/portal_share.py create mode 100644 addons/portal/wizard/portal_share_views.xml delete mode 100644 addons/sale/controllers/mail.py diff --git a/addons/account/controllers/__init__.py b/addons/account/controllers/__init__.py index 6d24304de46..1a195b71756 100644 --- a/addons/account/controllers/__init__.py +++ b/addons/account/controllers/__init__.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import mail from . import onboarding from . import portal diff --git a/addons/account/controllers/mail.py b/addons/account/controllers/mail.py deleted file mode 100644 index 564856b909c..00000000000 --- a/addons/account/controllers/mail.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import werkzeug - -from odoo.addons.mail.controllers.main import MailController -from odoo.exceptions import AccessError -from odoo.http import request -from odoo.tools.misc import consteq - - -class MailController(MailController): - - @classmethod - def _redirect_to_record(cls, model, res_id, access_token=None): - # If the current user doesn't have access to the invoice, but provided - # a valid access token, redirect him to the front-end view. - if model == 'account.invoice' and res_id and access_token: - uid = request.session.uid or request.env.ref('base.public_user').id - record_sudo = request.env[model].sudo().browse(res_id).exists() - try: - record_sudo.sudo(uid).check_access_rights('read') - record_sudo.sudo(uid).check_access_rule('read') - except AccessError: - if record_sudo.access_token and consteq(record_sudo.access_token, access_token): - record_action = record_sudo.with_context( - force_website=True).get_access_action(uid) - if record_action['type'] == 'ir.actions.act_url': - return werkzeug.utils.redirect(record_action['url']) - return super(MailController, cls)._redirect_to_record(model, res_id, access_token=access_token) diff --git a/addons/account/controllers/portal.py b/addons/account/controllers/portal.py index c59be47e588..0da9ea570dc 100644 --- a/addons/account/controllers/portal.py +++ b/addons/account/controllers/portal.py @@ -8,7 +8,6 @@ from odoo import http, _ from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager, get_records_pager from odoo.exceptions import AccessError from odoo.http import request -from odoo.tools import consteq class PortalAccount(CustomerPortal): @@ -32,38 +31,13 @@ class PortalAccount(CustomerPortal): # My Invoices # ------------------------------------------------------------ - def _invoice_check_access(self, invoice_id, access_token=None): - invoice = request.env['account.invoice'].browse([invoice_id]) - invoice_sudo = invoice.sudo() - try: - invoice.check_access_rights('read') - invoice.check_access_rule('read') - except AccessError: - if not access_token or not consteq(invoice_sudo.access_token, access_token): - raise - return invoice_sudo - def _invoice_get_page_view_values(self, invoice, access_token, **kwargs): values = { 'page_name': 'invoice', 'invoice': invoice, } - if access_token: - # force breadcrumbs even if access_token to `invite` users to register if they click on it - values['no_breadcrumbs'] = False - values['access_token'] = access_token + return self._get_page_view_values(invoice, access_token, values, 'my_invoices_history', False, **kwargs) - if kwargs.get('error'): - values['error'] = kwargs['error'] - if kwargs.get('warning'): - values['warning'] = kwargs['warning'] - if kwargs.get('success'): - values['success'] = kwargs['success'] - - history = request.session.get('my_invoices_history', []) - values.update(get_records_pager(history, invoice)) - - return values @http.route(['/my/invoices', '/my/invoices/page/'], type='http', auth="user", website=True) def portal_my_invoices(self, page=1, date_begin=None, date_end=None, sortby=None, **kw): @@ -117,7 +91,7 @@ class PortalAccount(CustomerPortal): @http.route(['/my/invoices/'], type='http', auth="public", website=True) def portal_my_invoice_detail(self, invoice_id, access_token=None, **kw): try: - invoice_sudo = self._invoice_check_access(invoice_id, access_token) + invoice_sudo = self._document_check_access('account.invoice', invoice_id, access_token) except AccessError: return request.redirect('/my') @@ -130,7 +104,7 @@ class PortalAccount(CustomerPortal): ], type='http', auth="public", website=True) def portal_my_invoice_report(self, invoice_id, access_token=None, **kw): try: - invoice_sudo = self._invoice_check_access(invoice_id, access_token) + invoice_sudo = self._document_check_access('account.invoice', invoice_id, access_token) except AccessError: return request.redirect('/my') diff --git a/addons/account/data/account_data.xml b/addons/account/data/account_data.xml index f7fa31bd517..bd2c6a8d683 100644 --- a/addons/account/data/account_data.xml +++ b/addons/account/data/account_data.xml @@ -174,5 +174,13 @@ selection + + + Share + + + code + action = records.action_share() + diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 16b0d7d9340..fa235ee9d67 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -45,8 +45,6 @@ class AccountInvoice(models.Model): _description = "Invoice" _order = "date_invoice desc, number desc, id desc" - def _get_default_access_token(self): - return str(uuid.uuid4()) def _get_default_incoterm(self): return self.env.user.company_id.incoterm_id @@ -243,9 +241,6 @@ class AccountInvoice(models.Model): ], readonly=True, states={'draft': [('readonly', False)]}, index=True, change_default=True, default=lambda self: self._context.get('type', 'out_invoice'), track_visibility='always') - access_token = fields.Char( - 'Security Token', copy=False, - default=_get_default_access_token) refund_invoice_id = fields.Many2one('account.invoice', string="Invoice for which this invoice is the credit note") number = fields.Char(related='move_id.name', store=True, readonly=True, copy=False) @@ -428,10 +423,10 @@ class AccountInvoice(models.Model): domain += [('journal_id', '=', self.journal_id.id), ('state', 'not in', ['draft', 'cancel'])] return journal_sequence, domain - def _compute_portal_url(self): - super(AccountInvoice, self)._compute_portal_url() + def _compute_access_url(self): + super(AccountInvoice, self)._compute_access_url() for order in self: - order.portal_url = '/my/invoices/%s' % (order.id) + order.access_url = '/my/invoices/%s' % (order.id) @api.depends('state', 'journal_id', 'date_invoice') def _get_sequence_prefix(self): @@ -564,27 +559,6 @@ class AccountInvoice(models.Model): view_id = get_view_id('invoice_form', 'account.invoice.form').id return super(AccountInvoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) - @api.model_cr_context - def _init_column(self, column_name): - """ Initialize the value of the given column for existing rows. - - Overridden here because we need to generate different access tokens - and by default _init_column calls the default method once and applies - it for every record. - """ - if column_name != 'access_token': - super(AccountInvoice, self)._init_column(column_name) - else: - query = """UPDATE %(table_name)s - SET %(column_name)s = md5(md5(random()::varchar || id::varchar) || clock_timestamp()::varchar)::uuid::varchar - WHERE %(column_name)s IS NULL - """ % {'table_name': self._table, 'column_name': column_name} - self.env.cr.execute(query) - - def _generate_access_token(self): - for invoice in self: - invoice.access_token = self._get_default_access_token() - @api.multi def invoice_print(self): """ Print the invoice and mark it as sent, so that we can see more @@ -946,40 +920,6 @@ class AccountInvoice(models.Model): return groups - @api.multi - def get_access_action(self, access_uid=None): - """ Instead of the classic form view, redirect to the online invoice for portal users. """ - self.ensure_one() - user, record = self.env.user, self - if access_uid: - user = self.env['res.users'].sudo().browse(access_uid) - record = self.sudo(user) - - if user.share or self.env.context.get('force_website'): - try: - record.check_access_rule('read') - except exceptions.AccessError: - if self.env.context.get('force_website'): - return { - 'type': 'ir.actions.act_url', - 'url': '/my/invoices/%s' % self.id, - 'target': 'self', - 'res_id': self.id, - } - else: - pass - else: - return { - 'type': 'ir.actions.act_url', - 'url': '/my/invoices/%s?access_token=%s' % (self.id, self.access_token), - 'target': 'self', - 'res_id': self.id, - } - return super(AccountInvoice, self).get_access_action(access_uid) - - def get_mail_url(self): - return self.get_share_url() - @api.multi def get_formview_id(self, access_uid=None): """ Update form view id of action to open the invoice """ diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 5dd758a4ef4..5dbd235cf28 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -887,6 +887,5 @@ action = action_values domain="[('journal_id','=', active_id)]" res_model="account.invoice" src_model="account.journal"/> - diff --git a/addons/account/views/account_portal_templates.xml b/addons/account/views/account_portal_templates.xml index 289a1d75486..dc7d1c61f65 100644 --- a/addons/account/views/account_portal_templates.xml +++ b/addons/account/views/account_portal_templates.xml @@ -132,6 +132,8 @@ + + diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index 949a715cddb..1600189c7b9 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -49,7 +49,9 @@ class MailController(http.Controller): return comparison, record, redirect @classmethod - def _redirect_to_record(cls, model, res_id, access_token=None): + def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs): + # access_token and kwargs are used in the portal controller override for the Send by email or Share Link + # to give access to the record to a recipient that has normally no access. uid = request.session.uid # no model / res_id, meaning no possible record -> redirect to login @@ -178,7 +180,7 @@ class MailController(http.Controller): """ if res_id and isinstance(res_id, pycompat.string_types): res_id = int(res_id) - return self._redirect_to_record(model, res_id, access_token) + return self._redirect_to_record(model, res_id, access_token, **kwargs) @http.route('/mail/assign', type='http', auth='user', methods=['GET']) def mail_action_assign(self, model, res_id, token=None): diff --git a/addons/mail/data/mail_data.xml b/addons/mail/data/mail_data.xml index c499dd5c5e7..7af17890aae 100644 --- a/addons/mail/data/mail_data.xml +++ b/addons/mail/data/mail_data.xml @@ -229,7 +229,7 @@ @@ -242,7 +242,7 @@ - + diff --git a/addons/portal/__manifest__.py b/addons/portal/__manifest__.py index 6f1c0186a86..e9fe1e36151 100644 --- a/addons/portal/__manifest__.py +++ b/addons/portal/__manifest__.py @@ -20,6 +20,7 @@ a dependency towards website edition and customization capabilities.""", 'data/portal_data.xml', 'views/assets.xml', 'views/portal_templates.xml', + 'wizard/portal_share_views.xml', 'wizard/portal_wizard_views.xml', ], 'qweb': [ diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py index 89dbded3676..073e7153eeb 100644 --- a/addons/portal/controllers/mail.py +++ b/addons/portal/controllers/mail.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import werkzeug +from werkzeug import urls from werkzeug.exceptions import NotFound, Forbidden from odoo import http from odoo.http import request from odoo.osv import expression from odoo.tools import consteq, plaintext2html +from odoo.addons.mail.controllers.main import MailController +from odoo.exceptions import AccessError def _has_token_access(res_model, res_id, token=''): @@ -14,6 +18,7 @@ def _has_token_access(res_model, res_id, token=''): token_field = request.env[res_model]._mail_post_token_field return (token and record and consteq(record[token_field], token)) + def _message_post_helper(res_model='', res_id=None, message='', token='', nosubscribe=True, **kw): """ Generic chatter function, allowing to write on *any* object that inherits mail.thread. If a token is specified, all logged in users will be able to write a message regardless @@ -38,7 +43,12 @@ def _message_post_helper(res_model='', res_id=None, message='', token='', nosubs if access_as_sudo: record = record.sudo() if request.env.user == request.env.ref('base.public_user'): - author_id = record.partner_id.id if hasattr(record, 'partner_id') else author_id + if kw.get('pid') and consteq(kw.get('hash'), record._sign_token(int(kw.get('pid')))): + author_id = kw.get('pid') + else: + # TODO : After adding the pid and sign_token in access_url when send invoice by email, remove this line + # TODO : Author must be Public User (to rename to 'Anonymous') + author_id = record.partner_id.id if hasattr(record, 'partner_id') and record.partner_id.id else author_id else: if not author_id: raise NotFound() @@ -106,3 +116,42 @@ class PortalChatter(http.Controller): 'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(), 'message_count': Message.search_count(domain) } + + +class MailController(MailController): + + @classmethod + def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs): + """ If the current user doesn't have access to the document, but provided + a valid access token, redirect him to the front-end view. + If the partner_id and hash parameters are given, add those parameters to the redirect url + to authentify the recipient in the chatter, if any. + + :param model: the model name of the record that will be visualized + :param res_id: the id of the record + :param access_token: token that gives access to the record + bypassing the rights and rules restriction of the user. + :param kwargs: Typically, it can receive a partner_id and a hash (sign_token). + If so, those two parameters are used to authentify the recipient in the chatter, if any. + :return: + """ + if issubclass(type(request.env[model]), request.env.registry['portal.mixin']): + uid = request.session.uid or request.env.ref('base.public_user').id + record_sudo = request.env[model].sudo().browse(res_id).exists() + try: + record_sudo.sudo(uid).check_access_rights('read') + record_sudo.sudo(uid).check_access_rule('read') + except AccessError: + if record_sudo.access_token and access_token and consteq(record_sudo.access_token, access_token): + record_action = record_sudo.with_context(force_website=True).get_access_action() + if record_action['type'] == 'ir.actions.act_url': + pid = kwargs.get('pid') + hash = kwargs.get('hash') + url = record_action['url'] + if pid and hash: + url = urls.url_parse(url) + url_params = url.decode_query() + url_params.update([("pid", pid), ("hash", hash)]) + url = url.replace(query=urls.url_encode(url_params)).to_url() + return werkzeug.utils.redirect(url) + return super(MailController, cls)._redirect_to_record(model, res_id, access_token=access_token) diff --git a/addons/portal/controllers/portal.py b/addons/portal/controllers/portal.py index 31eb8e933cc..eedf1876952 100644 --- a/addons/portal/controllers/portal.py +++ b/addons/portal/controllers/portal.py @@ -7,8 +7,9 @@ from werkzeug import urls from odoo import fields as odoo_fields, tools, _ from odoo.osv import expression -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError, AccessError from odoo.http import Controller, request, route +from odoo.tools import consteq from odoo.addons.web.controllers.main import WebClient # -------------------------------------------------- @@ -75,8 +76,8 @@ def pager(url, total, page=1, step=30, scope=5, url_args=None): def get_records_pager(ids, current): - if current.id in ids and (hasattr(current, 'website_url') or hasattr(current, 'portal_url')): - attr_name = 'portal_url' if hasattr(current, 'portal_url') else 'website_url' + if current.id in ids and (hasattr(current, 'website_url') or hasattr(current, 'access_url')): + attr_name = 'access_url' if hasattr(current, 'access_url') else 'website_url' idx = ids.index(current.id) return { 'prev_record': idx != 0 and getattr(current.browse(ids[idx - 1]), attr_name), @@ -228,3 +229,38 @@ class CustomerPortal(Controller): error_message.append("Unknown field '%s'" % ','.join(unknown)) return error, error_message + + def _document_check_access(self, model_name, document_id, access_token=None): + document = request.env[model_name].browse([document_id]) + document_sudo = document.sudo() + try: + document.check_access_rights('read') + document.check_access_rule('read') + except AccessError: + if not access_token or not consteq(document_sudo.access_token, access_token): + raise + return document_sudo + + def _get_page_view_values(self, document, access_token, values, session_history, no_breadcrumbs, **kwargs): + if access_token: + # if no_breadcrumbs = False -> force breadcrumbs even if access_token to `invite` users to register if they click on it + values['no_breadcrumbs'] = no_breadcrumbs + values['access_token'] = access_token + + # Those are used notably whenever the payment form is implied in the portal. + if kwargs.get('error'): + values['error'] = kwargs['error'] + if kwargs.get('warning'): + values['warning'] = kwargs['warning'] + if kwargs.get('success'): + values['success'] = kwargs['success'] + # Email token for posting messages in portal view with identified author + if kwargs.get('pid'): + values['pid'] = kwargs['pid'] + if kwargs.get('hash'): + values['hash'] = kwargs['hash'] + + history = request.session.get(session_history, []) + values.update(get_records_pager(history, document)) + + return values diff --git a/addons/portal/data/portal_data.xml b/addons/portal/data/portal_data.xml index 0cab34b3c01..47535af4f5e 100644 --- a/addons/portal/data/portal_data.xml +++ b/addons/portal/data/portal_data.xml @@ -94,5 +94,15 @@ + diff --git a/addons/portal/models/portal_mixin.py b/addons/portal/models/portal_mixin.py index 68a8ea55c34..5dfef905606 100644 --- a/addons/portal/models/portal_mixin.py +++ b/addons/portal/models/portal_mixin.py @@ -1,61 +1,69 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +import uuid +import hashlib +import hmac from werkzeug.urls import url_encode - -from odoo import api, fields, models, _ +from odoo import api, exceptions, fields, models, tools, _ class PortalMixin(models.AbstractModel): _name = "portal.mixin" - portal_url = fields.Char( - 'Portal Access URL', compute='_compute_portal_url', + access_url = fields.Char( + 'Portal Access URL', compute='_compute_access_url', help='Customer Portal URL') + access_token = fields.Char('Security Token', copy=False) + + # to display the warning from specific model + access_warning = fields.Text("Access warning", compute="_compute_access_warning") + + def _compute_access_warning(self): + for mixin in self: + mixin.access_warning = '' @api.multi - def _compute_portal_url(self): + def _compute_access_url(self): for record in self: - record.portal_url = '#' + record.access_url = '#' - def _get_access_token_field(self): - """ Give the field used to fetch the customer portal access token, if - any. Override this method if the field holding the token is named - differently. """ - return 'access_token' if 'access_token' in self else False - - def _get_customer_field(self): - """ Give the field used to fetch the customer partner_id, if any. - Override this method if the field holding the token is named differently. """ - return 'partner_id' if 'partner_id' in self else False - - def _get_access_token(self): + def _portal_ensure_token(self): """ Get the current record access token """ - field = self._get_access_token_field() - return self[field] if field else False + self.access_token = self.access_token if self.access_token else str(uuid.uuid4()) + return self.access_token - def _get_customer(self): - """ Get the current record custome (res.partner record) """ - field = self._get_customer_field() - return self[field] if field else self.env['res.partner'] - - def get_share_url(self): + def _get_share_url(self, redirect=False, signup_partner=False, pid=None): + """ + Build the url of the record that will be sent by mail and adds additional parameters such as + access_token to bypass the recipient's rights, + signup_partner to allows the user to create easily an account, + hash token to allow the user to be authenticated in the chatter of the record portal view, if applicable + :param redirect : Send the redirect url instead of the direct portal share url + :param signup_partner: allows the user to create an account with pre-filled fields. + :param pid: = partner_id - when given, a hash is generated to allow the user to be authenticated + in the portal chatter, if any in the target page, + if the user is redirected to the portal instead of the backend. + :return: the url of the record with access parameters, if any. + """ self.ensure_one() params = { 'model': self._name, 'res_id': self.id, } - if hasattr(self, 'access_token') and self.access_token: - params['access_token'] = self.access_token - if hasattr(self, 'partner_id') and self.partner_id: + if hasattr(self, 'access_token'): + params['access_token'] = self._portal_ensure_token() + if pid: + params['pid'] = pid + params['hash'] = self._sign_token(pid) + if signup_partner and hasattr(self, 'partner_id') and self.partner_id: params.update(self.partner_id.signup_get_auth_param()[self.partner_id.id]) - return '/mail/view?' + url_encode(params) + return '/mail/view?' if redirect else self.access_url + url_encode(params) @api.multi def _notify_get_groups(self, message, groups): - access_token = self._get_access_token() - customer = self._get_customer() + access_token = self._portal_ensure_token() + customer = self['partner_id'] if access_token and customer: additional_params = { @@ -76,3 +84,64 @@ class PortalMixin(models.AbstractModel): else: new_group = [] return super(PortalMixin, self)._notify_get_groups(message, new_group + groups) + + @api.multi + def get_access_action(self, access_uid=None): + """ Instead of the classic form view, redirect to the online document for + portal users or if force_website=True in the context. """ + self.ensure_one() + + user, record = self.env.user, self + if access_uid: + try: + record.check_access_rights('read') + record.check_access_rule("read") + except exceptions.AccessError: + return super(PortalMixin, self).get_access_action(access_uid) + user = self.env['res.users'].sudo().browse(access_uid) + record = self.sudo(user) + if user.share or self.env.context.get('force_website'): + try: + record.check_access_rights('read') + record.check_access_rule('read') + except exceptions.AccessError: + if self.env.context.get('force_website'): + return { + 'type': 'ir.actions.act_url', + 'url': record.access_url, + 'target': 'self', + 'res_id': record.id, + } + else: + pass + else: + return { + 'type': 'ir.actions.act_url', + 'url': record._get_share_url(), + 'target': 'self', + 'res_id': record.id, + } + return super(PortalMixin, self).get_access_action(access_uid) + + @api.model + def action_share(self): + action = self.env.ref('portal.portal_share_action').read()[0] + action['context'] = {'active_id': self.env.context['active_id'], + 'active_model': self.env.context['active_model']} + return action + + @api.multi + def _sign_token(self, pid): + """Generate a secure hash for this record with the email of the recipient with whom the record have been shared. + + This is used to determine who is opening the link + to be able for the recipient to post messages on the document's portal view. + + :param str email: + Email of the recipient that opened the link. + """ + self.ensure_one() + secret = self.env["ir.config_parameter"].sudo().get_param( + "database.secret") + token = (self.env.cr.dbname, self.access_token, pid) + return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha256).hexdigest() diff --git a/addons/portal/static/src/xml/portal_chatter.xml b/addons/portal/static/src/xml/portal_chatter.xml index dcfbf922e04..a1683dac803 100644 --- a/addons/portal/static/src/xml/portal_chatter.xml +++ b/addons/portal/static/src/xml/portal_chatter.xml @@ -32,6 +32,8 @@ + + diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index c900b426c6d..73f5cd4767e 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -92,6 +92,9 @@ project.project
+
+