[MERGE] Forward-port saas-11 up to eb6b256f99

This commit is contained in:
Olivier Dony
2016-10-04 01:20:17 +02:00
13 changed files with 241 additions and 144 deletions
@@ -44,6 +44,10 @@ class ir_module_module(osv.osv):
for kind in ['data', 'init_xml', 'update_xml']:
for filename in terp[kind]:
ext = os.path.splitext(filename)[1].lower()
if ext not in ('.xml', '.csv', '.sql'):
_logger.info("module %s: skip unsupported file %s", module, filename)
continue
_logger.info("module %s: loading %s", module, filename)
noupdate = False
if filename.endswith('.csv') and kind in ('init', 'init_xml'):
+1
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import controllers
import models
import report
import wizard
+2
View File
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*
import main
+38
View File
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.addons.mail.controllers.main import MailController
from openerp import http
class CrmController(http.Controller):
@http.route('/lead/case_mark_won', type='http', auth='user', methods=['GET'])
def crm_lead_case_mark_won(self, res_id, token):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('crm.lead', int(res_id), token)
if comparison and record:
try:
record.case_mark_won()
except Exception:
return MailController._redirect_to_messaging()
return redirect
@http.route('/lead/case_mark_lost', type='http', auth='user', methods=['GET'])
def crm_lead_case_mark_lost(self, res_id, token):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('crm.lead', int(res_id), token)
if comparison and record:
try:
record.case_mark_lost()
except Exception:
return MailController._redirect_to_messaging()
return redirect
@http.route('/lead/convert', type='http', auth='user', methods=['GET'])
def crm_lead_convert(self, res_id, token):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('crm.lead', int(res_id), token)
if comparison and record:
try:
record.convert_opportunity(record.partner_id.id)
except Exception:
return MailController._redirect_to_messaging()
return redirect
+6 -3
View File
@@ -1062,9 +1062,12 @@ class Lead(FormatAddress, models.Model):
result = super(Lead, self)._notification_get_recipient_groups(message, recipients)
lead = self[0]
won_action = self._notification_link_helper('method', method='action_set_won')
lost_action = self._notification_link_helper('method', method='action_set_lost')
convert_action = self._notification_link_helper('method', method='convert_opportunity', partner_id=lead.partner_id.id)
won_action = self._notification_link_helper('controller', controller='/lead/case_mark_won')
lost_action = self._notification_link_helper('controller', controller='/lead/case_mark_lost')
convert_action = self._notification_link_helper('controller', controller='/lead/convert')
if lead.type == 'lead':
result['group_sale_salesman'] = {
+1
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import controllers
import models
import report
import wizard
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*
import main
+28
View File
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.addons.mail.controllers.main import MailController
from openerp import http
class HrHolidaysController(http.Controller):
@http.route('/hr_holidays/validate', type='http', auth='user', methods=['GET'])
def hr_holidays_validate(self, res_id, token):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('hr.holidays', int(res_id), token)
if comparison and record:
try:
record.signal_workflow('validate')
except Exception:
return MailController._redirect_to_messaging()
return redirect
@http.route('/hr_holidays/refuse', type='http', auth='user', methods=['GET'])
def hr_holidays_refuse(self, res_id, token):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('hr.holidays', int(res_id), token)
if comparison and record:
try:
record.signal_workflow('refuse')
except Exception:
return MailController._redirect_to_messaging()
return redirect
+4 -2
View File
@@ -535,8 +535,10 @@ class Holidays(models.Model):
def _notification_get_recipient_groups(self, message, recipients):
result = super(Holidays, self)._notification_get_recipient_groups(message, recipients)
app_action = '/mail/workflow?%s' % url_encode({'model': self._name, 'res_id': self.id, 'signal': 'validate'})
ref_action = '/mail/workflow?%s' % url_encode({'model': self._name, 'res_id': self.id, 'signal': 'refuse'})
app_action = self._notification_link_helper('controller', controller='/hr_holidays/validate')
ref_action = self._notification_link_helper('controller', controller='/hr_holidays/refuse')
actions = []
if self.state == 'confirm':
+105 -100
View File
@@ -1,5 +1,6 @@
import base64
import json
import logging
from operator import itemgetter
import psycopg2
import werkzeug
@@ -8,20 +9,90 @@ from werkzeug import url_encode
import openerp
from openerp import SUPERUSER_ID
from openerp import http
from openerp import _
from openerp.exceptions import AccessError
from openerp.http import request
from openerp.tools import consteq
from openerp.addons.web.controllers.main import binary_content
_logger = logging.getLogger(__name__)
class MailController(http.Controller):
_cp_path = '/mail'
def _redirect_to_messaging(self):
@classmethod
def _redirect_to_messaging(cls):
messaging_action = request.env['mail.thread']._get_inbox_action_xml_id()
url = '/web#%s' % url_encode({'action': messaging_action})
return werkzeug.utils.redirect(url)
@classmethod
def _check_token(cls, token):
base_link = request.httprequest.path
params = dict(request.params)
params.pop('token', '')
valid_token = request.env['mail.thread']._generate_notification_token(base_link, params)
return consteq(valid_token, str(token))
@classmethod
def _check_token_and_record_or_redirect(cls, model, res_id, token):
comparison = cls._check_token(token)
if not comparison:
_logger.warning(_('Invalid token in route %s') % request.httprequest.url)
return comparison, None, cls._redirect_to_messaging()
try:
record = request.env[model].browse(res_id).exists()
except Exception:
record = None
redirect = cls._redirect_to_messaging()
else:
redirect = cls._redirect_to_record(model, res_id)
return comparison, record, redirect
@classmethod
def _redirect_to_record(cls, model, res_id):
uid = request.session.uid
# no model / res_id, meaning no possible record -> redirect to login
if not model or not res_id or model not in request.env:
return cls._redirect_to_messaging()
# find the access action using sudo to have the details about the access link
RecordModel = request.env[model]
record_sudo = RecordModel.sudo().browse(res_id).exists()
if not record_sudo:
# record does not seem to exist -> redirect to login
return cls._redirect_to_messaging()
record_action = record_sudo.get_access_action()
# the record has an URL redirection: use it directly
if record_action['type'] == 'ir.actions.act_url':
return werkzeug.utils.redirect(record_action['url'])
# other choice: act_window (no support of anything else currently)
elif not record_action['type'] == 'ir.actions.act_window':
return cls._redirect_to_messaging()
# the record has a window redirection: check access rights
if not RecordModel.sudo(uid).check_access_rights('read', raise_exception=False):
return cls._redirect_to_messaging()
try:
record_sudo.sudo(uid).check_access_rule('read')
except AccessError:
return cls._redirect_to_messaging()
url_params = {
'view_type': record_action['view_type'],
'model': model,
'id': res_id,
'active_id': res_id,
'view_id': record_sudo.get_formview_id(),
'action': record_action.get('id'),
}
url = '/web?#%s' % url_encode(url_params)
return werkzeug.utils.redirect(url)
@http.route('/mail/receive', type='json', auth='none')
def receive(self, req):
""" End-point to receive mail from an external SMTP server. """
@@ -93,8 +164,6 @@ class MailController(http.Controller):
- users without read access are redirected to the Messaging
- not logged users are redirected to the login page
"""
uid = request.session.uid
if message_id:
try:
message = request.env['mail.message'].sudo().browse(int(message_id)).exists()
@@ -108,112 +177,48 @@ class MailController(http.Controller):
elif res_id and isinstance(res_id, basestring):
res_id = int(res_id)
# no model / res_id, meaning no possible record -> redirect to login
if not model or not res_id or model not in request.env:
return self._redirect_to_messaging()
return self._redirect_to_record(model, res_id)
# find the access action using sudo to have the details about the access link
RecordModel = request.env[model].sudo(uid)
record = RecordModel.browse(res_id).exists()
if not record:
# record does not seem to exist -> redirect to login
return self._redirect_to_messaging()
record_action = record.get_access_action()
@http.route('/mail/follow', type='http', auth='user', methods=['GET'])
def mail_action_follow(self, model, res_id, token=None):
comparison, record, redirect = self._check_token_and_record_or_redirect(model, int(res_id), token)
if comparison and record:
try:
record.sudo().message_subscribe_users()
except Exception:
return self._redirect_to_messaging()
return redirect
# only URL redirections or window actions supported currently
if not record_action['type'] in ('ir.actions.act_url', 'ir.actions.act_window'):
return self._redirect_to_messaging()
# the record has an URL redirection: use it directly
if record_action['type'] == 'ir.actions.act_url':
return werkzeug.utils.redirect(record_action['url'])
# the record has a window redirection: check access rights
if not RecordModel.check_access_rights('read', raise_exception=False):
return self._redirect_to_messaging()
try:
record.check_access_rule('read')
except AccessError:
return self._redirect_to_messaging()
# at this point user can read the document so no issue with get_formview_id
query = {}
url_params = {
'view_type': record_action['view_type'],
'model': model,
'id': res_id,
'active_id': res_id,
'view_id': record.get_formview_id(),
'action': record_action.get('id'),
}
url = '/web?%s#%s' % (url_encode(query), url_encode(url_params))
return werkzeug.utils.redirect(url)
@http.route('/mail/follow', type='http', auth='user')
def mail_action_follow(self, model, res_id):
if model not in request.env:
return self._redirect_to_messaging()
Model = request.env[model]
try:
Model.browse(res_id).message_subscribe_users()
except:
return self._redirect_to_messaging()
return werkzeug.utils.redirect('/mail/view?%s' % url_encode({'model': model, 'res_id': res_id}))
@http.route('/mail/unfollow', type='http', auth='user')
def mail_action_unfollow(self, model, res_id):
if model not in request.env:
return self._redirect_to_messaging()
Model = request.env[model]
try:
Model.browse(res_id).sudo().message_unsubscribe_users([request.uid])
except:
return self._redirect_to_messaging()
return werkzeug.utils.redirect('/mail/view?%s' % url_encode({'model': model, 'res_id': res_id}))
@http.route('/mail/unfollow', type='http', auth='user', methods=['GET'])
def mail_action_unfollow(self, model, res_id, token=None):
comparison, record, redirect = self._check_token_and_record_or_redirect(model, int(res_id), token)
if comparison and record:
try:
# TDE CHECKME: is sudo really necessary ?
record.sudo().message_unsubscribe_users([request.uid])
except Exception:
return self._redirect_to_messaging()
return redirect
@http.route('/mail/new', type='http', auth='user')
def mail_action_new(self, model, res_id, **kwargs):
def mail_action_new(self, model, res_id, action_id):
if model not in request.env:
return self._redirect_to_messaging()
params = {'view_type': 'form', 'model': model}
if kwargs.get('action_id'):
params['action'] = kwargs['action_id']
if action_id:
# Probably something to do
params['action'] = action_id
return werkzeug.utils.redirect('/web?#%s' % url_encode(params))
@http.route('/mail/method', type='http', auth='user')
def mail_action_method(self, model, res_id, method, **kwargs):
# only public methods / check exists
if method.strip().startswith('_') or model not in request.env:
return self._redirect_to_messaging()
Model = request.env[model]
try:
record = Model.browse(int(res_id)).exists()
getattr(record, method)(**json.loads(kwargs.get('params', {})))
except:
return self._redirect_to_messaging()
return werkzeug.utils.redirect('/mail/view?%s' % url_encode({'model': model, 'res_id': res_id}))
@http.route('/mail/assign', type='http', auth='user')
def mail_action_assign(self, model, res_id, **kwargs):
if model not in request.env:
return self._redirect_to_messaging()
Model = request.env[model]
try:
Model.browse(int(res_id)).exists().write({'user_id': request.uid})
except:
return self._redirect_to_messaging()
return werkzeug.utils.redirect('/mail/view?%s' % url_encode({'model': model, 'res_id': res_id}))
@http.route('/mail/workflow', type='http', auth='user')
def mail_action_workflow(self, model, res_id, signal, **kwargs):
if model not in request.env:
return self._redirect_to_messaging()
Model = request.env[model]
try:
Model.browse(int(res_id)).exists().signal_workflow(signal)
except:
return self._redirect_to_messaging()
return werkzeug.utils.redirect('/mail/view?%s' % url_encode({'model': model, 'res_id': res_id}))
@http.route('/mail/assign', type='http', auth='user', methods=['GET'])
def mail_action_assign(self, model, res_id, token=None):
comparison, record, redirect = self._check_token_and_record_or_redirect(model, int(res_id), token)
if comparison and record:
try:
record.write({'user_id': request.uid})
except Exception:
return self._redirect_to_messaging()
return redirect
@http.route('/mail/<string:res_model>/<int:res_id>/avatar/<int:partner_id>', type='http', auth='public')
def avatar(self, res_model, res_id, partner_id):
+29 -16
View File
@@ -4,6 +4,8 @@ import base64
import datetime
import dateutil
import email
import hashlib
import hmac
import json
import lxml
from lxml import etree
@@ -573,6 +575,13 @@ class MailThread(models.AbstractModel):
because portal users have a different Inbox action than classic users. """
return 'mail.mail_channel_action_client_chat'
@api.model
def _generate_notification_token(self, base_link, params):
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
token = '%s?%s' % (base_link, ' '.join('%s=%s' % (key, params[key]) for key in sorted(params.keys())))
hm = hmac.new(str(secret), token, hashlib.sha1).hexdigest()
return hm
@api.multi
def _notification_link_helper(self, link_type, **kwargs):
if kwargs.get('message_id'):
@@ -580,26 +589,30 @@ class MailThread(models.AbstractModel):
'message_id': kwargs.pop('message_id')
}
else:
self.ensure_one()
base_params = {
'model': self._name,
'res_id': self.ids[0],
'model': kwargs.get('model', self._name),
'res_id': kwargs.get('res_id', self.ids and self.ids[0] or False),
}
link = False
if link_type in ['view', 'assign', 'follow', 'unfollow']:
params = dict(base_params)
link = '/mail/view?%s' % url_encode(params)
elif link_type == 'workflow':
params = dict(base_params, signal=kwargs['signal'])
link = '/mail/workflow?%s' % url_encode(params)
elif link_type == 'method':
method = kwargs.pop('method')
params = dict(base_params, method=method, params=json.dumps(kwargs))
link = '/mail/method?%s' % url_encode(params)
base_link = '/mail/%s' % link_type
elif link_type == 'new':
params = dict(base_params, action_id=kwargs.get('action_id'))
link = '/mail/new?%s' % url_encode(params)
params = dict(base_params, action_id=kwargs.get('action_id', ''))
base_link = '/mail/new'
elif link_type == 'controller':
controller = kwargs.pop('controller')
params = dict(base_params)
params.pop('model')
base_link = '%s' % controller
else:
return ''
if link_type not in ['view', 'new']:
token = self._generate_notification_token(base_link, params)
params['token'] = token
link = '%s?%s' % (base_link, url_encode(params))
return link
@api.multi
@@ -673,8 +686,8 @@ class MailThread(models.AbstractModel):
'followers': self.env['res.partner'],
'not_followers': self.env['res.partner'],
'button_access': {'url': access_link, 'title': view_title},
'button_follow': {'url': '/mail/follow?%s' % url_encode({'model': message.model, 'res_id': message.res_id}), 'title': _('Follow')},
'button_unfollow': {'url': '/mail/unfollow?%s' % url_encode({'model': message.model, 'res_id': message.res_id}), 'title': _('Unfollow')},
'button_follow': {'url': self._notification_link_helper('follow', model=message.model, res_id=message.res_id), 'title': _('Follow')},
'button_unfollow': {'url': self._notification_link_helper('unfollow', model=message.model, res_id=message.res_id), 'title': _('Unfollow')},
'actions': list(),
}
group_data[category] = self.env['res.partner']
+1
View File
@@ -494,6 +494,7 @@ class Users(models.Model):
if not passwd:
# empty passwords disallowed for obvious security reasons
raise AccessDenied()
db = self.pool.db_name
if self.__uid_cache[db].get(uid) == passwd:
return
cr = self.pool.cursor()
+20 -23
View File
@@ -26,11 +26,8 @@ from openerp import SUPERUSER_ID
_logger = logging.getLogger(__name__)
# Import of XML records requires the unsafe eval as well,
# almost everywhere, which is ok because it supposedly comes
# from trusted data, but at least we make it obvious now.
unsafe_eval = eval
from .safe_eval import safe_eval as eval
from .safe_eval import safe_eval as s_eval
safe_eval = lambda expr, ctx={}: s_eval(expr, ctx, nocopy=True)
class ParseError(Exception):
def __init__(self, msg, text, filename, lineno):
@@ -98,7 +95,7 @@ def _eval_xml(self, node, env):
idref2 = {}
if f_search:
idref2 = _get_idref(self, env, f_model, self.idref)
q = unsafe_eval(f_search, idref2)
q = safe_eval(f_search, idref2)
ids = env[f_model].search(q).ids
if f_use != 'id':
ids = map(lambda x: x[f_use], env[f_model].browse(ids).read([f_use]))
@@ -115,7 +112,7 @@ def _eval_xml(self, node, env):
if a_eval:
idref2 = _get_idref(self, env, f_model, self.idref)
try:
return unsafe_eval(a_eval, idref2)
return safe_eval(a_eval, idref2)
except Exception:
logging.getLogger('openerp.tools.convert.init').error(
'Could not eval(%s) for %s in %s', a_eval, node.get('name'), env.context)
@@ -183,7 +180,7 @@ def _eval_xml(self, node, env):
# FIXME: should probably be exclusive
if a_eval:
self.idref['ref'] = self.id_get
args = unsafe_eval(a_eval, self.idref)
args = safe_eval(a_eval, self.idref)
for n in node:
return_val = _eval_xml(self, n, env)
if return_val is not None:
@@ -217,12 +214,12 @@ class xml_import(object):
for ctx in (data_node_context, node_context):
if ctx:
try:
ctx_res = unsafe_eval(ctx, eval_dict)
ctx_res = safe_eval(ctx, eval_dict)
if isinstance(context, dict):
context.update(ctx_res)
else:
context = ctx_res
except NameError:
except (ValueError, NameError):
# Some contexts contain references that are only valid at runtime at
# client-side, so in that case we keep the original context string
# as it is. We also log it, just in case.
@@ -261,7 +258,7 @@ form: module.record_id""" % (xml_id,)
if d_search:
idref = _get_idref(self, self.env, d_model, {})
try:
records = records.search(unsafe_eval(d_search, idref))
records = records.search(safe_eval(d_search, idref))
except ValueError:
_logger.warning('Skipping deletion for failed search `%r`', d_search, exc_info=True)
pass
@@ -293,14 +290,14 @@ form: module.record_id""" % (xml_id,)
if rec.get(field):
res[dest] = rec.get(field).encode('utf8')
if rec.get('auto'):
res['auto'] = eval(rec.get('auto','False'))
res['auto'] = safe_eval(rec.get('auto','False'))
if rec.get('sxw'):
sxw_content = file_open(rec.get('sxw')).read()
res['report_sxw_content'] = sxw_content
if rec.get('header'):
res['header'] = eval(rec.get('header','False'))
res['header'] = safe_eval(rec.get('header','False'))
res['multi'] = rec.get('multi') and eval(rec.get('multi','False'))
res['multi'] = rec.get('multi') and safe_eval(rec.get('multi','False'))
xml_id = rec.get('id','').encode('utf8')
self._test_xml_id(xml_id)
@@ -324,12 +321,12 @@ form: module.record_id""" % (xml_id,)
id = self.env['ir.model.data']._update("ir.actions.report.xml", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
self.idref[xml_id] = int(id)
if not rec.get('menu') or eval(rec.get('menu','False')):
if not rec.get('menu') or safe_eval(rec.get('menu','False')):
keyword = str(rec.get('keyword', 'client_print_multi'))
value = 'ir.actions.report.xml,'+str(id)
action = self.env['ir.values'].set_action(res['name'], keyword, res['model'], value)
self.env['ir.actions.report.xml'].browse(id).write({'ir_values_id': action.id})
elif self.mode=='update' and eval(rec.get('menu','False'))==False:
elif self.mode=='update' and safe_eval(rec.get('menu','False'))==False:
# Special check for report having attribute menu=False on update
value = 'ir.actions.report.xml,'+str(id)
self._remove_ir_values(res['name'], value, res['model'])
@@ -398,8 +395,8 @@ form: module.record_id""" % (xml_id,)
context = self.get_context(data_node, rec, eval_context)
try:
domain = unsafe_eval(domain, eval_context)
except NameError:
domain = safe_eval(domain, eval_context)
except (ValueError, NameError):
# Some domains contain references that are only valid at runtime at
# client-side, so in that case we keep the original domain string
# as it is. We also log it, just in case.
@@ -435,7 +432,7 @@ form: module.record_id""" % (xml_id,)
if rec.get('target'):
res['target'] = rec.get('target','')
if rec.get('multi'):
res['multi'] = eval(rec.get('multi', 'False'))
res['multi'] = safe_eval(rec.get('multi', 'False'))
id = self.env['ir.model.data']._update('ir.actions.act_window', self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
self.idref[xml_id] = int(id)
@@ -583,7 +580,7 @@ form: module.record_id""" % (xml_id,)
if rec_id:
records = env[rec_model].browse(self.id_get(rec_id))
elif rec_src:
q = unsafe_eval(rec_src, eval_dict)
q = safe_eval(rec_src, eval_dict)
records = env[rec_model].search(q)
if rec_src_count:
count = int(rec_src_count)
@@ -609,7 +606,7 @@ form: module.record_id""" % (xml_id,)
f_expr = test.get("expr",'').encode('utf-8')
env = self.env(user=uid, context=context)
expected_value = _eval_xml(self, test, env) or True
expression_value = unsafe_eval(f_expr, globals_dict)
expression_value = safe_eval(f_expr, globals_dict)
if expression_value != expected_value: # assertion failed
self.assertion_report.record_failure()
msg = 'assertion "%s" failed!\n' \
@@ -628,7 +625,7 @@ form: module.record_id""" % (xml_id,)
rec_id = rec.get("id",'').encode('ascii')
rec_context = rec.get("context", {})
if rec_context:
rec_context = unsafe_eval(rec_context)
rec_context = safe_eval(rec_context)
if self.xml_filename and rec_id:
rec_context['install_mode_data'] = dict(
@@ -676,7 +673,7 @@ form: module.record_id""" % (xml_id,)
f_val = False
if f_search:
q = unsafe_eval(f_search, self.idref)
q = safe_eval(f_search, self.idref)
assert f_model, 'Define an attribute model="..." in your .XML file !'
# browse the objects searched
s = self.env[f_model].search(q)