diff --git a/addons/event/data/mail_template_data.xml b/addons/event/data/mail_template_data.xml index 03d541a588d..392b637d14d 100644 --- a/addons/event/data/mail_template_data.xml +++ b/addons/event/data/mail_template_data.xml @@ -225,7 +225,10 @@
- See location on Google Maps + Google Maps + See location on Google Maps
@@ -442,11 +445,14 @@ -
+
diff --git a/addons/event/models/res_config_settings.py b/addons/event/models/res_config_settings.py index 5105365bdc6..38907e18fb1 100644 --- a/addons/event/models/res_config_settings.py +++ b/addons/event/models/res_config_settings.py @@ -1,12 +1,23 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +import base64 +import binascii +from odoo import _, api, exceptions, fields, models class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' + def _default_use_google_maps_static_api(self): + api_key = self.env['ir.config_parameter'].sudo().get_param('google_maps.signed_static_api_key') + api_secret = self.env['ir.config_parameter'].sudo().get_param('google_maps.signed_static_api_secret') + return bool(api_key and api_secret) + + google_maps_static_api_key = fields.Char("Google Maps API key", compute="_compute_maps_static_api_key", + readonly=False, store=True, config_parameter='google_maps.signed_static_api_key') + google_maps_static_api_secret = fields.Char("Google Maps API secret", compute="_compute_maps_static_api_secret", + readonly=False, store=True, config_parameter='google_maps.signed_static_api_secret') module_event_sale = fields.Boolean("Tickets") module_website_event_meet = fields.Boolean("Discussion Rooms") module_website_event_track = fields.Boolean("Tracks and Agenda") @@ -17,6 +28,21 @@ class ResConfigSettings(models.TransientModel): module_event_barcode = fields.Boolean("Barcode") module_website_event_sale = fields.Boolean("Online Ticketing") module_event_booth = fields.Boolean("Booth Management") + use_google_maps_static_api = fields.Boolean("Google Maps static API", default=_default_use_google_maps_static_api) + + @api.depends('use_google_maps_static_api') + def _compute_maps_static_api_key(self): + """Clear API key on disabling google maps.""" + for config in self: + if not config.use_google_maps_static_api: + config.google_maps_static_api_key = '' + + @api.depends('use_google_maps_static_api') + def _compute_maps_static_api_secret(self): + """Clear API secret on disabling google maps.""" + for config in self: + if not config.use_google_maps_static_api: + config.google_maps_static_api_secret = '' @api.onchange('module_website_event_track') def _onchange_module_website_event_track(self): @@ -27,3 +53,23 @@ class ResConfigSettings(models.TransientModel): if not config.module_website_event_track: config.module_website_event_track_live = False config.module_website_event_track_quiz = False + + def _check_google_maps_static_api_secret(self): + for config in self: + if config.google_maps_static_api_secret: + try: + base64.urlsafe_b64decode(config.google_maps_static_api_secret) + except binascii.Error: + raise exceptions.UserError(_("Please enter a valid base64 secret")) + + @api.model_create_multi + def create(self, vals_list): + configs = super().create(vals_list) + configs._check_google_maps_static_api_secret() + return configs + + def write(self, vals): + configs = super().write(vals) + if vals.get('google_maps_static_api_secret'): + configs._check_google_maps_static_api_secret() + return configs diff --git a/addons/event/models/res_partner.py b/addons/event/models/res_partner.py index f82b58ae502..58a59efc063 100644 --- a/addons/event/models/res_partner.py +++ b/addons/event/models/res_partner.py @@ -1,7 +1,14 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +import base64 +import binascii +import hmac + +import requests +import werkzeug.urls + +from odoo import api, fields, models class ResPartner(models.Model): @@ -9,14 +16,71 @@ class ResPartner(models.Model): event_count = fields.Integer( '# Events', compute='_compute_event_count', groups='event.group_event_registration_desk') + static_map_url = fields.Char(compute="_compute_static_map_url") + static_map_url_is_valid = fields.Boolean(compute="_compute_static_map_url_is_valid") def _compute_event_count(self): self.event_count = 0 for partner in self: partner.event_count = self.env['event.event'].search_count([('registration_ids.partner_id', 'child_of', partner.ids)]) + @api.depends('zip', 'city', 'country_id', 'street') + def _compute_static_map_url(self): + for partner in self: + partner.static_map_url = partner._google_map_signed_img(zoom=13, width=598, height=200) + + def _compute_static_map_url_is_valid(self): + """Compute whether the link is valid. + + This should only remain valid for a relatively short time. + Here, for the duration it is in cache. + """ + session = requests.Session() + for partner in self: + url = self.static_map_url + if not url: + partner.static_map_url_is_valid = False + continue + + is_valid = False + # If the response isn't strictly successful, assume invalid url + try: + res = session.get(url, timeout=2) + if res.ok and not res.headers.get('X-Staticmap-API-Warning'): + is_valid = True + except requests.exceptions.RequestException: + pass + + partner.static_map_url_is_valid = is_valid + def action_event_view(self): action = self.env["ir.actions.actions"]._for_xml_id("event.action_event_view") action['context'] = {} action['domain'] = [('registration_ids.partner_id', 'child_of', self.ids)] return action + + def _google_map_signed_img(self, zoom=13, width=298, height=298): + """Create a signed static image URL for the location of this partner.""" + GOOGLE_MAPS_STATIC_API_KEY = self.env['ir.config_parameter'].sudo().get_param('google_maps.signed_static_api_key') + GOOGLE_MAPS_STATIC_API_SECRET = self.env['ir.config_parameter'].sudo().get_param('google_maps.signed_static_api_secret') + if not GOOGLE_MAPS_STATIC_API_KEY or not GOOGLE_MAPS_STATIC_API_SECRET: + return None + # generate signature as per https://developers.google.com/maps/documentation/maps-static/digital-signature#server-side-signing + location_string = f"{self.street}, {self.city} {self.zip}, {self.country_id and self.country_id.display_name or ''}" + params = { + 'center': location_string, + 'markers': f'size:mid|{location_string}', + 'size': f"{width}x{height}", + 'zoom': zoom, + 'sensor': "false", + 'key': GOOGLE_MAPS_STATIC_API_KEY, + } + unsigned_path = '/maps/api/staticmap?' + werkzeug.urls.url_encode(params) + try: + api_secret_bytes = base64.urlsafe_b64decode(GOOGLE_MAPS_STATIC_API_SECRET + "====") + except binascii.Error: + return None + url_signature_bytes = hmac.digest(api_secret_bytes, unsigned_path.encode(), 'sha1') + params['signature'] = base64.urlsafe_b64encode(url_signature_bytes) + + return 'https://maps.googleapis.com/maps/api/staticmap?' + werkzeug.urls.url_encode(params) diff --git a/addons/event/views/res_config_settings_views.xml b/addons/event/views/res_config_settings_views.xml index f26b6c7796c..5b734a72bca 100644 --- a/addons/event/views/res_config_settings_views.xml +++ b/addons/event/views/res_config_settings_views.xml @@ -52,6 +52,25 @@ + + + + +
+
+
+
+
+
+
+
diff --git a/addons/mail/views/res_config_settings_views.xml b/addons/mail/views/res_config_settings_views.xml index 291c3c95cb7..b805040ed73 100644 --- a/addons/mail/views/res_config_settings_views.xml +++ b/addons/mail/views/res_config_settings_views.xml @@ -42,7 +42,10 @@
Save this page and come back here to set up the feature.
- +
diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py index 5271c302adc..0f6a8c86b30 100644 --- a/odoo/tools/mail.py +++ b/odoo/tools/mail.py @@ -223,16 +223,6 @@ def html_sanitize(src, silent=True, sanitize_tags=True, sanitize_attributes=Fals cleaner = _Cleaner(**kwargs) cleaned = cleaner.clean_html(src) assert isinstance(cleaned, str) - # MAKO compatibility: $, { and } inside quotes are escaped, preventing correct mako execution - cleaned = cleaned.replace(u'%24', u'$') - cleaned = cleaned.replace(u'%7B', u'{') - cleaned = cleaned.replace(u'%7D', u'}') - cleaned = cleaned.replace(u'%20', u' ') - cleaned = cleaned.replace(u'%5B', u'[') - cleaned = cleaned.replace(u'%5D', u']') - cleaned = cleaned.replace(u'%7C', u'|') - cleaned = cleaned.replace(u'<%', u'<%') - cleaned = cleaned.replace(u'%>', u'%>') # html considerations so real html content match database value cleaned = cleaned.replace(u'\xa0', u' ') except etree.ParserError as e: