From e9af623f8e8692de231f33f671715df38859bec3 Mon Sep 17 00:00:00 2001 From: jevin Date: Mon, 21 Dec 2020 09:29:55 -0700 Subject: [PATCH 1/2] [FIX] odoo: security patches --- addons/mail/i18n/mail.pot | 32 ++++++++++- addons/mail/models/mail_channel.py | 46 ++++++++++++++- addons/mail/models/mail_followers.py | 18 +++++- addons/mail/models/mail_notification.py | 16 +++++- addons/mail/models/mail_template.py | 2 +- addons/mail/models/mail_thread.py | 2 +- addons/mail/static/src/js/discuss.js | 10 ++-- .../src/js/models/threads/abstract_thread.js | 2 +- .../static/src/js/models/threads/dm_chat.js | 2 +- .../static/src/js/models/threads/livechat.js | 2 +- .../js/services/mail_notification_manager.js | 8 ++- addons/mail/static/src/xml/discuss.xml | 2 +- addons/mail/static/tests/discuss_tests.js | 2 +- addons/portal/controllers/portal.py | 2 +- addons/portal/wizard/portal_wizard.py | 9 +++ addons/test_mail/tests/test_performance.py | 16 ++++-- addons/web/controllers/main.py | 7 ++- .../js/views/calendar/calendar_controller.js | 2 +- odoo/addons/base/models/ir_actions.py | 14 +---- odoo/addons/base/models/qweb.py | 3 +- .../base/wizard/base_import_language.py | 3 + odoo/tools/misc.py | 57 +++++++++++++++---- odoo/tools/safe_eval.py | 7 ++- 23 files changed, 205 insertions(+), 59 deletions(-) diff --git a/addons/mail/i18n/mail.pot b/addons/mail/i18n/mail.pot index c6b2c73173841..9b58fb67720f0 100644 --- a/addons/mail/i18n/mail.pot +++ b/addons/mail/i18n/mail.pot @@ -2547,6 +2547,12 @@ msgstr "" msgid "Is Read" msgstr "" +#. module: mail +#: code:addons/mail/models/mail_notification.py:0 +#, python-format +msgid "Can not update the message or recipient of a notification." +msgstr "" + #. module: mail #: model:ir.model.fields,field_description:mail.field_mail_channel__is_subscribed msgid "Is Subscribed" @@ -4829,6 +4835,12 @@ msgstr "" msgid "The owner of records created upon receiving emails on this alias. If this field is not set the system will attempt to find the right owner based on the sender (From) address, or will use the Administrator account if no system user is found for that address." msgstr "" +#. module: mail +#: code:addons/mail/models/mail_channel.py:0 +#, python-format +msgid "The partner can not join this channel" +msgstr "" + #. module: mail #: code:addons/mail/models/mail_activity.py:243 #: code:addons/mail/models/mail_message.py:704 @@ -5427,6 +5439,24 @@ msgstr "" msgid "You can mark any message as 'starred', and it shows up in this mailbox." msgstr "" +#. module: mail +#: cod:addons/mail/models/mail_channel.py:0 +#, python-format +msgid "You can not remove this partner from this channel" +msgstr "" + +#. module: mail +#: code:addons/mail/models/mail_channel.py:0 +#, python-format +msgid "You can not write on the record of other users" +msgstr "" + +#. module: mail +#: code:addons/mail/models/mail_channel.py:0 +#, python-format +msgid "You can not write on this field" +msgstr "" + #. module: mail #: code:addons/mail/models/res_users.py:87 #, python-format @@ -5455,7 +5485,7 @@ msgstr "" #. openerp-web #: code:addons/mail/static/src/js/services/mail_notification_manager.js:198 #, python-format -msgid "You have been invited to: " +msgid "You have been invited to: %s" msgstr "" #. module: mail diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 13cbb4644a1dc..e27cbfa3553dc 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -9,7 +9,7 @@ from uuid import uuid4 from odoo import _, api, fields, models, modules, tools -from odoo.exceptions import UserError, ValidationError +from odoo.exceptions import AccessError, UserError, ValidationError from odoo.osv import expression from odoo.tools import ormcache, pycompat from odoo.tools.safe_eval import safe_eval @@ -34,6 +34,27 @@ class ChannelPartner(models.Model): is_minimized = fields.Boolean("Conversation is minimized") is_pinned = fields.Boolean("Is pinned on the interface", default=True) + @api.model + def create(self, vals): + if 'channel_id' in vals and not self.env.user._is_admin(): + channel_id = self.env['mail.channel'].browse(vals['channel_id']) + if not channel_id._can_invite(vals.get('partner_id')): + raise AccessError(_('The partner can not join this channel')) + return super(ChannelPartner, self).create(vals) + + def write(self, vals): + if not self.env.user._is_admin(): + if {'channel_id', 'partner_id', 'partner_email'} & set(vals): + raise AccessError(_('You can not write on this field')) + elif self.mapped('partner_id') != self.env.user.partner_id: + raise AccessError(_('You can not write on the record of other users')) + return super(ChannelPartner, self).write(vals) + + def unlink(self): + if not self.env.user._is_admin() and not all(record.channel_id.is_member for record in self): + raise AccessError(_('You can not remove this partner from this channel')) + return super(ChannelPartner, self).unlink() + class Moderation(models.Model): _name = 'mail.moderation' @@ -213,6 +234,10 @@ def create(self, vals): vals['image'] = defaults['image'] tools.image_resize_images(vals) + + # always add the current user to the channel + vals['channel_partner_ids'] = vals.get('channel_partner_ids', []) + [(4, self.env.user.partner_id.id)] + # Create channel and alias channel = super(Channel, self.with_context( alias_model_name=self._name, alias_parent_model_name=self._name, mail_create_nolog=True, mail_create_nosubscribe=True) @@ -384,7 +409,7 @@ def message_post(self, message_type='notification', **kwargs): if moderation_status == 'rejected': return self.env['mail.message'] - self.filtered(lambda channel: channel.channel_type == 'chat').mapped('channel_last_seen_partner_ids').write({'is_pinned': True}) + self.filtered(lambda channel: channel.channel_type == 'chat').mapped('channel_last_seen_partner_ids').sudo().write({'is_pinned': True}) message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(message_type=message_type, moderation_status=moderation_status, **kwargs) @@ -732,6 +757,22 @@ def channel_invite(self, partner_ids): # broadcast the channel header to the added partner self._broadcast(partner_ids) + def _can_invite(self, partner_id): + """Return True if the current user can invite the partner to the channel.""" + self.ensure_one() + sudo_self = self.sudo() + if sudo_self.public == 'public': + return True + if sudo_self.public == 'private': + return self.is_member + + # get the user related to the invited partner + partner = self.env['res.partner'].browse(partner_id).exists() + invited_user_id = partner.user_ids[:1] + if invited_user_id: + return (self.env.user | invited_user_id) <= sudo_self.group_public_id.users + return False + @api.multi def notify_typing(self, is_typing, is_website_user=False): """ Broadcast the typing notification to channel members @@ -824,7 +865,6 @@ def channel_create(self, name, privacy='public'): 'name': name, 'public': privacy, 'email_send': False, - 'channel_partner_ids': [(4, self.env.user.partner_id.id)] }) notification = _('
created #%s
') % (new_channel.id, new_channel.name,) new_channel.message_post(body=notification, message_type="notification", subtype="mail.mt_comment") diff --git a/addons/mail/models/mail_followers.py b/addons/mail/models/mail_followers.py index f4f16c2e9f01b..e1da60ef19f4b 100644 --- a/addons/mail/models/mail_followers.py +++ b/addons/mail/models/mail_followers.py @@ -48,7 +48,7 @@ def _invalidate_documents(self): @api.model_create_multi def create(self, vals_list): - res = super(Followers, self).create(vals_list) + res = super(Followers, self).create(vals_list)._check_rights() res._invalidate_documents() return res @@ -57,6 +57,7 @@ def write(self, vals): if 'res_model' in vals or 'res_id' in vals: self._invalidate_documents() res = super(Followers, self).write(vals) + self._check_rights() if any(x in vals for x in ['res_model', 'res_id', 'partner_id']): self._invalidate_documents() return res @@ -66,6 +67,21 @@ def unlink(self): self._invalidate_documents() return super(Followers, self).unlink() + def _check_rights(self): + user_partner = self.env.user.partner_id + for record in self: + obj = self.env[record.res_model].browse(record.res_id) + if record.channel_id or record.partner_id != user_partner: + obj.check_access_rights('write') + obj.check_access_rule('write') + subject = record.channel_id or record.partner_id + subject.check_access_rights('read') + subject.check_access_rule('read ') + else: + obj.check_access_rights('read') + obj.check_access_rule('read') + return self + _sql_constraints = [ ('mail_followers_res_partner_res_model_id_uniq', 'unique(res_model,res_id,partner_id)', 'Error, a partner cannot follow twice the same object.'), ('mail_followers_res_channel_res_model_id_uniq', 'unique(res_model,res_id,channel_id)', 'Error, a channel cannot follow twice the same object.'), diff --git a/addons/mail/models/mail_notification.py b/addons/mail/models/mail_notification.py index b687633a86d4c..3b67095713d95 100644 --- a/addons/mail/models/mail_notification.py +++ b/addons/mail/models/mail_notification.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from odoo import api, fields, models +from odoo.exceptions import AccessError from odoo.tools.translate import _ @@ -43,6 +44,19 @@ def init(self): if not self._cr.fetchone(): self._cr.execute('CREATE INDEX mail_notification_res_partner_id_is_read_email_status_mail_message_id ON mail_message_res_partner_needaction_rel (res_partner_id, is_read, email_status, mail_message_id)') + @api.model + def create(self, vals): + msg = self.env['mail.message'].browse(vals['mail_message_id']) + msg.check_access_rights('read') + msg.check_access_rule('read') + return super(Notification, self).create(vals) + + @api.multi + def write(self, vals): + if ('mail_message_id' in vals or 'res_partner_id' in vals) and not self.env.user._is_admin(): + raise AccessError(_("Can not update the message or recipient of a notification.")) + return super(Notification, self).write(vals) + @api.multi def format_failure_reason(self): self.ensure_one() @@ -50,5 +64,3 @@ def format_failure_reason(self): return dict(type(self).failure_type.selection).get(self.failure_type, _('No Error')) else: return _("Unknown error") + ": %s" % (self.failure_reason or '') - - diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index 4f3689ba99b15..e6bfd851b5618 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -98,7 +98,7 @@ def format_amount(env, amount, currency): 'str': str, 'quote': urls.url_quote, 'urlencode': urls.url_encode, - 'datetime': datetime, + 'datetime': tools.wrap_module(datetime, []), 'len': len, 'abs': abs, 'min': min, diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 0f8f290b8f4ac..f2d9b82b2e5f7 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1316,7 +1316,7 @@ def message_route_process(self, message, message_dict, routes): # disabled subscriptions during message_new/update to avoid having the system user running the # email gateway become a follower of all inbound messages - MessageModel = Model.sudo(user_id).with_context(mail_create_nosubscribe=True, mail_create_nolog=True) + MessageModel = Model.sudo(self.env.uid == 1 and user_id or None).with_context(mail_create_nosubscribe=True, mail_create_nolog=True) if thread_id and hasattr(MessageModel, 'message_update'): thread = MessageModel.browse(thread_id) thread.message_update(message_dict) diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js index 2cb9fe749d776..f05b4c58bd74c 100644 --- a/addons/mail/static/src/js/discuss.js +++ b/addons/mail/static/src/js/discuss.js @@ -559,12 +559,12 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { if (type === 'public') { $input.autocomplete({ source: function (request, response) { - self._lastSearchVal = _.escape(request.term); + self._lastSearchVal = request.term; self._searchChannel(self._lastSearchVal).done(function (result){ result.push({ label: _.str.sprintf( '' + _t("Create %s") + '', - '"#' + self._lastSearchVal + '"' + '"#' + _.escape(self._lastSearchVal) + '"' ), value: '_create', }); @@ -587,7 +587,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { }); } else if (type === 'private') { $input.on('keyup', this, function (ev) { - var name = _.escape($(ev.target).val()); + var name = $(ev.target).val(); if (ev.which === $.ui.keyCode.ENTER && name) { self.call('mail_service', 'createChannel', name, 'private'); } @@ -595,7 +595,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { } else if (type === 'dm_chat') { $input.autocomplete({ source: function (request, response) { - self._lastSearchVal = _.escape(request.term); + self._lastSearchVal = request.term; self.call('mail_service', 'searchPartner', self._lastSearchVal, 10).done(response); }, select: function (ev, ui) { @@ -1186,7 +1186,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { * @private */ _onInviteButtonClicked: function () { - var title = _.str.sprintf(_t("Invite people to #%s"), this._thread.getName()); + var title = _.str.sprintf(_t("Invite people to #%s"), _.escape(this._thread.getName())); new PartnerInviteDialog(this, title, this._thread.getID()).open(); }, /** diff --git a/addons/mail/static/src/js/models/threads/abstract_thread.js b/addons/mail/static/src/js/models/threads/abstract_thread.js index 829f54581dac5..5c331f0512a27 100644 --- a/addons/mail/static/src/js/models/threads/abstract_thread.js +++ b/addons/mail/static/src/js/models/threads/abstract_thread.js @@ -15,7 +15,7 @@ var AbstractThread = Class.extend(Mixins.EventDispatcherMixin, { * @param {Object} params * @param {Object} params.data * @param {integer|string} params.data.id the ID of this thread - * @param {string} params.data.name the name of this thread + * @param {string} params.data.name the server name of this thread * @param {string} [params.data.status=''] the status of this thread * @param {Object} params.parent Object with the event-dispatcher mixin * (@see {web.mixins.EventDispatcherMixin}) diff --git a/addons/mail/static/src/js/models/threads/dm_chat.js b/addons/mail/static/src/js/models/threads/dm_chat.js index 767a6e32edd07..baebc957b9b5f 100644 --- a/addons/mail/static/src/js/models/threads/dm_chat.js +++ b/addons/mail/static/src/js/models/threads/dm_chat.js @@ -15,7 +15,7 @@ var DMChat = TwoUserChannel.extend({ * @param {Object[]} params.data.direct_partner * @param {integer} params.data.direct_partner[0].id * @param {string} params.data.direct_partner[0].im_status - * @param {string} params.data.direct_partner[0].name + * @param {string} params.data.direct_partner[0].name server name of partner */ init: function (params) { this._super.apply(this, arguments); diff --git a/addons/mail/static/src/js/models/threads/livechat.js b/addons/mail/static/src/js/models/threads/livechat.js index f4cb58ebad363..6de228c1315bb 100644 --- a/addons/mail/static/src/js/models/threads/livechat.js +++ b/addons/mail/static/src/js/models/threads/livechat.js @@ -17,7 +17,7 @@ var Livechat = TwoUserChannel.extend({ * @override * @param {Object} params * @param {Object} params.data - * @param {string} params.data.anonymous_name name of the website user + * @param {string} params.data.anonymous_name server name of the website user */ init: function (params) { this._super.apply(this, arguments); diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js index 8b698b3e04e8f..4c5242a9366fe 100644 --- a/addons/mail/static/src/js/services/mail_notification_manager.js +++ b/addons/mail/static/src/js/services/mail_notification_manager.js @@ -178,9 +178,11 @@ MailManager.include({ * * @private * @param {Object} channelData + * @param {string} channelData.channel_type * @param {integer} channelData.id * @param {string} [channelData.info] * @param {boolean} channelData.is_minimized + * @param {string} channelData.name server name of channel * @param {string} channelData.state */ _handlePartnerChannelNotification: function (channelData) { @@ -196,7 +198,7 @@ MailManager.include({ ) { this.do_notify( _t("Invitation"), - _t("You have been invited to: ") + channelData.name); + _.str.sprintf(_t("You have been invited to: %s"), _.escape(channelData.name))); } } var channel = this.getChannel(channelData.id); @@ -438,12 +440,12 @@ MailManager.include({ if (_.contains(['public', 'private'], channel.getType())) { message = _.str.sprintf( _t("You unsubscribed from %s."), - channel.getName() + _.escape(channel.getName()) ); } else { message = _.str.sprintf( _t("You unpinned your conversation with %s."), - channel.getName() + _.escape(channel.getName()) ); } this._removeChannel(channel); diff --git a/addons/mail/static/src/xml/discuss.xml b/addons/mail/static/src/xml/discuss.xml index 605dcb53956c4..b36417530dca1 100644 --- a/addons/mail/static/src/xml/discuss.xml +++ b/addons/mail/static/src/xml/discuss.xml @@ -137,7 +137,7 @@ # - + ' % (p.display_name, p.email) for p in partners_error_user]))) + if partners_error_internal_user: + error_msg.append("%s\n- %s" % (_("Some contacts are already internal users:"), + '\n- '.join(partners_error_internal_user.mapped('email')))) if error_msg: error_msg.append(_("To resolve this error, you can: \n" "- Correct the emails of the relevant contacts\n" "- Grant access only to contacts with unique emails")) + error_msg[-1] += _("\n- Switch the internal users to portal manually") return error_msg @api.multi diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index f96a9f0f97481..2f72b2b1d28e3 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -58,18 +58,22 @@ def test_write_mail(self): records = self.env['test_performance.mail'].search([]) self.assertEqual(len(records), 5) - with self.assertQueryCount(__system__=3, demo=3): # test_mail only: 3 - 3 + with self.assertQueryCount(__system__=3, demo=4): # test_mail only: 3 - 4 records.write({'name': 'X'}) @users('__system__', 'demo') @warmup def test_write_mail_with_recomputation(self): """ Write records inheriting from 'mail.thread' (with recomputation). """ + import logging + logging.getLogger(__name__).info("================================================") records = self.env['test_performance.mail'].search([]) self.assertEqual(len(records), 5) - with self.assertQueryCount(__system__=5, demo=5): # test_mail only: 5 - 5 + with self.assertQueryCount(__system__=5, demo=6): # test_mail only: 5 - 6 + records._cr.sql_log = True records.write({'value': 42}) + records._cr.sql_log = False @users('__system__', 'demo') @warmup @@ -98,13 +102,13 @@ def test_create_mail(self): @warmup def test_create_mail_with_tracking(self): """ Create records inheriting from 'mail.thread' (with field tracking). """ - with self.assertQueryCount(__system__=13, demo=13): # test_mail only: 13 - 13 + with self.assertQueryCount(__system__=13, demo=14): # test_mail only: 13 - 14 self.env['test_performance.mail'].create({'name': 'X'}) @users('__system__', 'emp') @warmup def test_create_mail_simple(self): - with self.assertQueryCount(__system__=8, emp=8): # test_mail only: 8 - 8 + with self.assertQueryCount(__system__=8, emp=9): # test_mail only: 8 - 9 self.env['mail.test.simple'].create({'name': 'Test'}) @users('__system__', 'emp') @@ -160,7 +164,7 @@ def setUp(self): def test_adv_activity(self): model = self.env['mail.test.activity'] - with self.assertQueryCount(__system__=9, emp=8): # test_mail only: 9 - 8 + with self.assertQueryCount(__system__=9, emp=9): # test_mail only: 9 - 9 model.create({'name': 'Test'}) @users('__system__', 'emp') @@ -265,7 +269,7 @@ def test_message_post_no_notification(self): def test_message_post_one_email_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(__system__=51, emp=71): # com runbot: 51 - 71 // test_mail only: 48 - 68 + with self.assertQueryCount(__system__=53, emp=72): # com runbot: 53 - 72 // test_mail only: 48 - 68 record.message_post( body='

Test Post Performances with an email ping

', partner_ids=self.customer.ids, diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index d83e4f36f885d..5eaf50f741fcd 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -73,6 +73,7 @@ db_monodb = http.db_monodb +def clean(name): return name.replace('\x3c', '') def serialize_exception(f): @functools.wraps(f) def wrap(*args, **kwargs): @@ -1142,7 +1143,7 @@ def upload(self, callback, ufile): ufile.content_type, base64.b64encode(data)] except Exception as e: args = [False, str(e)] - return out % (json.dumps(callback), json.dumps(args)) + return out % (json.dumps(clean(callback)), json.dumps(args)) @http.route('/web/binary/upload_attachment', type='http', auth="user") @serialize_exception @@ -1175,11 +1176,11 @@ def upload_attachment(self, callback, model, id, ufile): _logger.exception("Fail to upload attachment %s" % ufile.filename) else: args.append({ - 'filename': filename, + 'filename': clean(filename), 'mimetype': ufile.content_type, 'id': attachment.id }) - return out % (json.dumps(callback), json.dumps(args)) + return out % (json.dumps(clean(callback)), json.dumps(args)) @http.route([ '/web/binary/company_logo', diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js index b699879472ab1..2944d2898bb8e 100644 --- a/addons/web/static/src/js/views/calendar/calendar_controller.js +++ b/addons/web/static/src/js/views/calendar/calendar_controller.js @@ -320,7 +320,7 @@ var CalendarController = AbstractController.extend({ res_id: id || null, context: event.context || self.context, readonly: readonly, - title: _t("Open: ") + event.data.title, + title: _t("Open: ") + _.escape(event.data.title), on_saved: function () { if (event.data.on_save) { event.data.on_save(); diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py index 428fabd6dcf49..02cc5d31be85e 100644 --- a/odoo/addons/base/models/ir_actions.py +++ b/odoo/addons/base/models/ir_actions.py @@ -5,8 +5,7 @@ from odoo import api, fields, models, tools, SUPERUSER_ID, _ from odoo.exceptions import MissingError, UserError, ValidationError, AccessError from odoo.tools.safe_eval import safe_eval, test_python_expr -from odoo.tools import pycompat, wrap_module -from odoo.http import request +from odoo.tools import pycompat import base64 from collections import defaultdict @@ -14,20 +13,11 @@ import logging import time +import dateutil from pytz import timezone _logger = logging.getLogger(__name__) -# build dateutil helper, starting with the relevant *lazy* imports -import dateutil -import dateutil.parser -import dateutil.relativedelta -import dateutil.rrule -import dateutil.tz -mods = {'parser', 'relativedelta', 'rrule', 'tz'} -attribs = {atr for m in mods for atr in getattr(dateutil, m).__all__} -dateutil = wrap_module(dateutil, mods | attribs) - class IrActions(models.Model): _name = 'ir.actions.actions' diff --git a/odoo/addons/base/models/qweb.py b/odoo/addons/base/models/qweb.py index 51693c55cb1ee..7d2e353dd584f 100644 --- a/odoo/addons/base/models/qweb.py +++ b/odoo/addons/base/models/qweb.py @@ -16,7 +16,7 @@ import werkzeug from werkzeug.utils import escape as _escape -from odoo.tools import pycompat, freehash +from odoo.tools import pycompat, freehash, wrap_values try: import builtins @@ -340,6 +340,7 @@ def _compiled_fn(self, append, values): log = {'last_path_node': None} new = self.default_values() new.update(values) + wrap_values(new) try: return compiled(self, append, new, options, log) except (QWebException, TransactionRollbackError) as e: diff --git a/odoo/addons/base/wizard/base_import_language.py b/odoo/addons/base/wizard/base_import_language.py index aea906b8dc9e8..7db16ba939b04 100644 --- a/odoo/addons/base/wizard/base_import_language.py +++ b/odoo/addons/base/wizard/base_import_language.py @@ -29,6 +29,9 @@ class BaseLanguageImport(models.TransientModel): def import_lang(self): this = self[0] this = this.with_context(overwrite=this.overwrite) + + self.env['res.lang'].load_lang(lang=self.code, lang_name=self.name) + with TemporaryFile('wb+') as buf: try: buf.write(base64.decodestring(this.data)) diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index 762c07e535c09..be62bb257ec8d 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -42,7 +42,7 @@ from .config import config from .cache import * -from .parse_version import parse_version +from .parse_version import parse_version from . import pycompat import odoo @@ -767,7 +767,7 @@ def __repr__(self): return self class UnquoteEvalContext(defaultdict): - """Defaultdict-based evaluation context that returns + """Defaultdict-based evaluation context that returns an ``unquote`` string for any missing name used during the evaluation. Mostly useful for evaluating OpenERP domains/contexts that @@ -1223,6 +1223,19 @@ def _pickle_load(stream, encoding='ASCII', errors=False): pickle.dump = pickle_.dump pickle.dumps = pickle_.dumps +def wrap_values(d): + # apparently sometimes people pass raw records as eval context + # values + if not (d and isinstance(d, dict)): + return d + for k in d: + v = d[k] + if isinstance(v, types.ModuleType): + d[k] = wrap_module(v, None) + return d +import shutil +_missing = object() +_cache = dict.fromkeys([os, os.path, shutil, sys, subprocess]) def wrap_module(module, attr_list): """Helper for wrapping a package/module to expose selected attributes @@ -1231,14 +1244,38 @@ def wrap_module(module, attr_list): attributes and their own main attributes. No support for hiding attributes in case of name collision at different levels. """ - attr_list = set(attr_list) + wrapper = _cache.get(module) + if wrapper: + return wrapper + + attr_list = attr_list and set(attr_list) class WrappedModule(object): def __getattr__(self, attrib): - if attrib in attr_list: - target = getattr(module, attrib) - if isinstance(target, types.ModuleType): - return wrap_module(target, attr_list) - return target - raise AttributeError(attrib) + # respect whitelist if there is one + if attr_list is not None and attrib not in attr_list: + raise AttributeError(attrib) + + target = getattr(module, attrib) + if isinstance(target, types.ModuleType): + wrapper = _cache.get(target, _missing) + if wrapper is None: + raise AttributeError(attrib) + if wrapper is _missing: + target = wrap_module(target, attr_list) + else: + target = wrapper + setattr(self, attrib, target) + return target # module and attr_list are in the closure - return WrappedModule() + wrapper = WrappedModule() + _cache.setdefault(module, wrapper) + return wrapper + +# dateutil submodules are lazy so need to import them for them to "exist" +import dateutil +mods = ['parser', 'relativedelta', 'rrule', 'tz'] +for mod in mods: + __import__('dateutil.%s' % mod) +attribs = [attr for m in mods for attr in getattr(dateutil, m).__all__] +dateutil = wrap_module(dateutil, set(mods + attribs)) +datetime = wrap_module(datetime, ['date', 'datetime', 'time', 'timedelta', 'timezone', 'tzinfo', 'MAXYEAR', 'MINYEAR']) diff --git a/odoo/tools/safe_eval.py b/odoo/tools/safe_eval.py index ccb2e3e25b41a..d4934495d586b 100644 --- a/odoo/tools/safe_eval.py +++ b/odoo/tools/safe_eval.py @@ -24,9 +24,7 @@ import sys import werkzeug -from . import pycompat -from .misc import ustr -from . import pycompat +from . import ustr, pycompat, wrap_values import odoo @@ -337,6 +335,9 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal if locals_dict is not None: locals_dict = dict(locals_dict) + wrap_values(globals_dict) + wrap_values(locals_dict) + if globals_dict is None: globals_dict = {} From f64ad4c5bce56943f6e0c2a5d25c908d8e5e8a61 Mon Sep 17 00:00:00 2001 From: jevin Date: Mon, 21 Dec 2020 09:31:27 -0700 Subject: [PATCH 2/2] [FIX] mail: read string typo --- addons/mail/models/mail_followers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/models/mail_followers.py b/addons/mail/models/mail_followers.py index e1da60ef19f4b..4a455eb9a294e 100644 --- a/addons/mail/models/mail_followers.py +++ b/addons/mail/models/mail_followers.py @@ -76,7 +76,7 @@ def _check_rights(self): obj.check_access_rule('write') subject = record.channel_id or record.partner_id subject.check_access_rights('read') - subject.check_access_rule('read ') + subject.check_access_rule('read') else: obj.check_access_rights('read') obj.check_access_rule('read')