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 = _('
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 = {}