Skip to content

Commit 06d65f9

Browse files
robodooOdoo Online
authored andcommitted
[FW][FIX] mail, portal: improve mail/view redirections
Global purpose is to make mail/view controller more resilient by improving redirections when not having access to the record. Notably * correctly route internal users to discuss, portal users to /my and unlogged users to login; * keep original mail/view route when performing redirection after login so that we try the routing again; * fix various issues in redirect computation, such as lost access token parameter or partial reconstruction of URLs; Improve test coverage. Task-4685166 closes odoo#213889 Forward-port-of: odoo#213750 Forward-port-of: odoo#212545 Signed-off-by: Thibault Delavallee (tde) <[email protected]>
2 parents 4b73941 + 633ef9f commit 06d65f9

File tree

12 files changed

+433
-236
lines changed

12 files changed

+433
-236
lines changed

addons/crm/controllers/main.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from odoo.addons.mail.controllers.mail import MailController
66
from odoo import http
7-
from odoo.http import request
87

98
_logger = logging.getLogger(__name__)
109

@@ -19,7 +18,7 @@ def crm_lead_case_mark_won(self, res_id, token):
1918
record.action_set_won_rainbowman()
2019
except Exception:
2120
_logger.exception("Could not mark crm.lead as won")
22-
return MailController._redirect_to_messaging()
21+
return MailController._redirect_to_generic_fallback('crm.lead', res_id)
2322
return redirect
2423

2524
@http.route('/lead/case_mark_lost', type='http', auth='user', methods=['GET'])
@@ -30,7 +29,7 @@ def crm_lead_case_mark_lost(self, res_id, token):
3029
record.action_set_lost()
3130
except Exception:
3231
_logger.exception("Could not mark crm.lead as lost")
33-
return MailController._redirect_to_messaging()
32+
return MailController._redirect_to_generic_fallback('crm.lead', res_id)
3433
return redirect
3534

3635
@http.route('/lead/convert', type='http', auth='user', methods=['GET'])
@@ -41,5 +40,5 @@ def crm_lead_convert(self, res_id, token):
4140
record.convert_opportunity(record.partner_id)
4241
except Exception:
4342
_logger.exception("Could not convert crm.lead to opportunity")
44-
return MailController._redirect_to_messaging()
43+
return MailController._redirect_to_generic_fallback('crm.lead', res_id)
4544
return redirect

addons/hr_holidays/controllers/main.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def hr_holidays_request_validate(self, res_id, token):
2424
try:
2525
record.action_validate()
2626
except Exception:
27-
return MailController._redirect_to_messaging()
27+
return MailController._redirect_to_generic_fallback('hr.leave', res_id)
2828
return redirect
2929

3030
@http.route('/leave/refuse', type='http', auth='user', methods=['GET'])
@@ -34,7 +34,7 @@ def hr_holidays_request_refuse(self, res_id, token):
3434
try:
3535
record.action_refuse()
3636
except Exception:
37-
return MailController._redirect_to_messaging()
37+
return MailController._redirect_to_generic_fallback('hr.leave', res_id)
3838
return redirect
3939

4040
@http.route('/allocation/validate', type='http', auth='user', methods=['GET'])
@@ -44,7 +44,7 @@ def hr_holidays_allocation_validate(self, res_id, token):
4444
try:
4545
record.action_approve()
4646
except Exception:
47-
return MailController._redirect_to_messaging()
47+
return MailController._redirect_to_generic_fallback('hr.leave.allocation', res_id)
4848
return redirect
4949

5050
@http.route('/allocation/refuse', type='http', auth='user', methods=['GET'])
@@ -54,5 +54,5 @@ def hr_holidays_allocation_refuse(self, res_id, token):
5454
try:
5555
record.action_refuse()
5656
except Exception:
57-
return MailController._redirect_to_messaging()
57+
return MailController._redirect_to_generic_fallback('hr.leave.allocation', res_id)
5858
return redirect

addons/mail/controllers/mail.py

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,33 @@
1919
class MailController(http.Controller):
2020
_cp_path = '/mail'
2121

22+
@classmethod
23+
def _redirect_to_generic_fallback(cls, model, res_id, access_token=None, **kwargs):
24+
if request.session.uid is None:
25+
return cls._redirect_to_login_with_mail_view(
26+
model, res_id, access_token=access_token, **kwargs,
27+
)
28+
return cls._redirect_to_messaging()
29+
2230
@classmethod
2331
def _redirect_to_messaging(cls):
2432
url = '/odoo/action-mail.action_discuss'
2533
return request.redirect(url)
2634

35+
@classmethod
36+
def _redirect_to_login_with_mail_view(cls, model, res_id, access_token=None, **kwargs):
37+
url_base = '/mail/view'
38+
url_params = request.env['mail.thread']._get_action_link_params(
39+
'view', **{
40+
'model': model,
41+
'res_id': res_id,
42+
'access_token': access_token,
43+
**kwargs,
44+
}
45+
)
46+
mail_view_url = f'{url_base}?{url_encode(url_params, sort=True)}'
47+
return request.redirect(f'/web/login?{url_encode({"redirect": mail_view_url})}')
48+
2749
@classmethod
2850
def _check_token(cls, token):
2951
base_link = request.httprequest.path
@@ -37,12 +59,12 @@ def _check_token_and_record_or_redirect(cls, model, res_id, token):
3759
comparison = cls._check_token(token)
3860
if not comparison:
3961
_logger.warning('Invalid token in route %s', request.httprequest.url)
40-
return comparison, None, cls._redirect_to_messaging()
62+
return comparison, None, cls._redirect_to_generic_fallback(model, res_id)
4163
try:
4264
record = request.env[model].browse(res_id).exists()
4365
except Exception:
4466
record = None
45-
redirect = cls._redirect_to_messaging()
67+
redirect = cls._redirect_to_generic_fallback(model, res_id)
4668
else:
4769
redirect = cls._redirect_to_record(model, res_id)
4870
return comparison, record, redirect
@@ -57,20 +79,26 @@ def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs):
5779

5880
# no model / res_id, meaning no possible record -> redirect to login
5981
if not model or not res_id or model not in request.env:
60-
return cls._redirect_to_messaging()
82+
return cls._redirect_to_generic_fallback(
83+
model, res_id, access_token=access_token, **kwargs,
84+
)
6185

6286
# find the access action using sudo to have the details about the access link
6387
RecordModel = request.env[model]
6488
record_sudo = RecordModel.sudo().browse(res_id).exists()
6589
if not record_sudo:
6690
# record does not seem to exist -> redirect to login
67-
return cls._redirect_to_messaging()
91+
return cls._redirect_to_generic_fallback(
92+
model, res_id, access_token=access_token, **kwargs,
93+
)
6894

6995
suggested_company = record_sudo._get_redirect_suggested_company()
7096
# the record has a window redirection: check access rights
7197
if uid is not None:
7298
if not RecordModel.with_user(uid).has_access('read'):
73-
return cls._redirect_to_messaging()
99+
return cls._redirect_to_generic_fallback(
100+
model, res_id, access_token=access_token, **kwargs,
101+
)
74102
try:
75103
# We need here to extend the "allowed_company_ids" to allow a redirection
76104
# to any record that the user can access, regardless of currently visible
@@ -95,36 +123,38 @@ def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs):
95123
record_sudo.with_user(uid).with_context(allowed_company_ids=cids).check_access('read')
96124
request.future_response.set_cookie('cids', '-'.join([str(cid) for cid in cids]))
97125
except AccessError:
98-
return cls._redirect_to_messaging()
126+
return cls._redirect_to_generic_fallback(
127+
model, res_id, access_token=access_token, **kwargs,
128+
)
99129
else:
100130
record_action = record_sudo._get_access_action(access_uid=uid)
101131
else:
102132
record_action = record_sudo._get_access_action()
103-
if suggested_company:
104-
cids = [suggested_company.id]
133+
# we have an act_url (probably a portal link): we need to retry being logged to check access
105134
if record_action['type'] == 'ir.actions.act_url' and record_action.get('target_type') != 'public':
106-
url_params = {
107-
'model': model,
108-
'id': res_id,
109-
'active_id': res_id,
110-
'action': record_action.get('id'),
111-
}
112-
if cids:
113-
request.future_response.set_cookie('cids', '-'.join([str(cid) for cid in cids]))
114-
view_id = record_sudo.get_formview_id()
115-
if view_id:
116-
url_params['view_id'] = view_id
117-
url = '/web/login?redirect=#%s' % url_encode(url_params)
118-
return request.redirect(url)
135+
return cls._redirect_to_login_with_mail_view(
136+
model, res_id, access_token=access_token, **kwargs,
137+
)
119138

120139
record_action.pop('target_type', None)
121140
# the record has an URL redirection: use it directly
122141
if record_action['type'] == 'ir.actions.act_url':
123142
return request.redirect(record_action['url'])
124-
# other choice: act_window (no support of anything else currently)
125-
elif not record_action['type'] == 'ir.actions.act_window':
143+
# anything else than an act_window is not supported
144+
elif record_action['type'] != 'ir.actions.act_window':
126145
return cls._redirect_to_messaging()
127146

147+
# backend act_window: when not logged, unless really readable as public,
148+
# user is going to be redirected to login -> keep mail/view as redirect
149+
# in that case. In case of readable record, we consider this might be
150+
# a customization and we do not change the behavior in stable
151+
if uid is None or request.env.user._is_public():
152+
has_access = record_sudo.with_user(request.env.user).has_access('read')
153+
if not has_access:
154+
return cls._redirect_to_login_with_mail_view(
155+
model, res_id, access_token=access_token, **kwargs,
156+
)
157+
128158
url_params = {}
129159
menu_id = request.env['ir.ui.menu']._get_best_backend_root_menu_id_for_model(model)
130160
if menu_id:
@@ -134,12 +164,12 @@ def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs):
134164
url_params['view_id'] = view_id
135165
if cids:
136166
request.future_response.set_cookie('cids', '-'.join([str(cid) for cid in cids]))
137-
167+
138168
# @see commit c63d14a0485a553b74a8457aee158384e9ae6d3f
139169
# @see router.js: heuristics to discrimate a model name from an action path
140170
# is the presence of dots, or the prefix m- for models
141171
model_in_url = model if "." in model else "m-" + model
142-
url = f'/odoo/{model_in_url}/{res_id}?{url_encode(url_params)}'
172+
url = f'/odoo/{model_in_url}/{res_id}?{url_encode(url_params, sort=True)}'
143173
return request.redirect(url)
144174

145175
@http.route('/mail/view', type='http', auth='public')

addons/mail/models/mail_thread.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4102,31 +4102,17 @@ def _notify_get_recipients_classify(self, message, recipients_data,
41024102

41034103
def _notify_get_action_link(self, link_type, **kwargs):
41044104
""" Prepare link to an action: view document, follow document, ... """
4105-
params = {
4106-
'model': kwargs.get('model', self._name),
4107-
'res_id': kwargs.get('res_id', self.ids and self.ids[0] or False),
4108-
}
4109-
# keep only accepted parameters:
4110-
# - action (deprecated), token (assign), access_token (view)
4111-
# - auth_signup: auth_signup_token and auth_login
4112-
# - portal: pid, hash
4113-
params.update(dict(
4114-
(key, value)
4115-
for key, value in kwargs.items()
4116-
if key in ('action', 'token', 'access_token', 'auth_signup_token',
4117-
'auth_login', 'pid', 'hash')
4118-
))
4105+
params = self._get_action_link_params(link_type, **kwargs)
41194106

41204107
if link_type in ['view', 'assign', 'follow', 'unfollow']:
41214108
base_link = '/mail/%s' % link_type
41224109
elif link_type == 'controller':
41234110
controller = kwargs.get('controller')
4124-
params.pop('model')
41254111
base_link = '%s' % controller
41264112
else:
41274113
return ''
41284114

4129-
if link_type not in ['view']:
4115+
if link_type != 'view':
41304116
token = self._encode_link(base_link, params)
41314117
params['token'] = token
41324118

@@ -4146,6 +4132,28 @@ def _encode_link(self, base_link, params):
41464132
hm = hmac.new(secret.encode('utf-8'), token.encode('utf-8'), hashlib.sha1).hexdigest()
41474133
return hm
41484134

4135+
def _get_action_link_params(self, link_type, **kwargs):
4136+
""" Parameters management for '_notify_get_action_link' """
4137+
params = {
4138+
'model': kwargs.get('model', self._name),
4139+
'res_id': kwargs.get('res_id', self.ids[0] if self else False),
4140+
}
4141+
# keep only accepted parameters:
4142+
# - action (deprecated), token (assign), access_token (view)
4143+
# - auth_signup: auth_signup_token and auth_login
4144+
# - portal: pid, hash
4145+
params.update({
4146+
key: value
4147+
for key, value in kwargs.items()
4148+
if key in ('action', 'token', 'access_token', 'auth_signup_token',
4149+
'auth_login', 'pid', 'hash')
4150+
})
4151+
if link_type == 'controller':
4152+
params.pop('model')
4153+
elif link_type not in ['view', 'assign', 'follow', 'unfollow']:
4154+
return {}
4155+
return params
4156+
41494157
@api.model
41504158
def _extract_model_and_id(self, msg_vals):
41514159
"""

addons/mail/tests/common.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,6 @@ def _create_template(cls, model, template_values=None):
10201020
cls.email_template = cls.env['mail.template'].create(create_values)
10211021
return cls.email_template
10221022

1023-
10241023
def _generate_notify_recipients(self, partners, record=None):
10251024
""" Tool method to generate recipients data according to structure used
10261025
in notification methods. Purpose is to allow testing of internals of
@@ -1041,6 +1040,29 @@ def _generate_notify_recipients(self, partners, record=None):
10411040
} for partner in partners
10421041
]
10431042

1043+
def _get_mail_composer_web_context(self, records, add_web=True, **values):
1044+
""" Helper to generate composer context. Will make tests a bit less
1045+
verbose.
1046+
1047+
:param bool add_web: add web context, generally making noise especially in
1048+
mass mail mode (active_id/ids both present in context)
1049+
"""
1050+
base_context = {
1051+
'default_model': records._name,
1052+
'default_res_ids': records.ids,
1053+
}
1054+
if len(records) == 1:
1055+
base_context['default_composition_mode'] = 'comment'
1056+
else:
1057+
base_context['default_composition_mode'] = 'mass_mail'
1058+
if add_web:
1059+
base_context['active_model'] = records._name
1060+
base_context['active_id'] = records[0].id
1061+
base_context['active_ids'] = records.ids
1062+
if values:
1063+
base_context.update(**values)
1064+
return base_context
1065+
10441066
# ------------------------------------------------------------
10451067
# MAIL ASSERTS WRAPPERS
10461068
# ------------------------------------------------------------

addons/portal/controllers/mail.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ def portal_message_update_is_internal(self, message_id, is_internal):
121121

122122
class MailController(mail.MailController):
123123

124+
@classmethod
125+
def _redirect_to_generic_fallback(cls, model, res_id, access_token=None, **kwargs):
126+
# Generic fallback for a share user is the customer portal
127+
if request.session.uid and request.env.user.share:
128+
return request.redirect('/my')
129+
return super()._redirect_to_generic_fallback(model, res_id, access_token=access_token, **kwargs)
130+
124131
@classmethod
125132
def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs):
126133
""" If the current user doesn't have access to the document, but provided
@@ -156,7 +163,7 @@ def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs):
156163
url = urls.url_parse(url)
157164
url_params = url.decode_query()
158165
url_params.update([("pid", pid), ("hash", hash)])
159-
url = url.replace(query=urls.url_encode(url_params)).to_url()
166+
url = url.replace(query=urls.url_encode(url_params, sort=True)).to_url()
160167
return request.redirect(url)
161168
return super(MailController, cls)._redirect_to_record(model, res_id, access_token=access_token, **kwargs)
162169

addons/test_mail/tests/test_mail_composer.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -118,21 +118,7 @@ def _get_web_context(self, records, add_web=True, **values):
118118
:param add_web: add web context, generally making noise especially in
119119
mass mail mode (active_id/ids both present in context)
120120
"""
121-
base_context = {
122-
'default_model': records._name,
123-
'default_res_ids': records.ids,
124-
}
125-
if len(records) == 1:
126-
base_context['default_composition_mode'] = 'comment'
127-
else:
128-
base_context['default_composition_mode'] = 'mass_mail'
129-
if add_web:
130-
base_context['active_model'] = records._name
131-
base_context['active_id'] = records[0].id
132-
base_context['active_ids'] = records.ids
133-
if values:
134-
base_context.update(**values)
135-
return base_context
121+
return self._get_mail_composer_web_context(records, add_web=add_web, **values)
136122

137123

138124
@tagged('mail_composer')

0 commit comments

Comments
 (0)