[FIX] event: update static maps api

Google now requires an API key for the static maps API. As API calls cost money, and we cannot restrict the domain of the caller with the API key in emails, as the domain may vary vastly, the signing of static API URLs is implemented in the existing 'google_map_img' of partners.

This in turn can be used to get a url for an existing location. While preventing anyone from stealing the API key for their own purposes

task - 3079113

closes odoo/odoo#107200

Signed-off-by: Stéphane Debauche (std) <std@odoo.com>
This commit is contained in:
Odoo's Mergebot
2023-01-20 19:56:53 +01:00
committed by GitHub
6 changed files with 144 additions and 16 deletions
+9 -3
View File
@@ -225,7 +225,10 @@
<div>
<i class="fa fa-map-marker"/>
<a t-attf-href="https://maps.google.com/maps?q={{ location }}" target="new">
See location on Google Maps
<img t-if="event_address.static_map_url and event_address.static_map_url_is_valid"
t-att-src="event_address.static_map_url"
style="vertical-align:bottom; width: 100%;" alt="Google Maps"/>
<t t-else="">See location on Google Maps</t>
</a>
</div>
</td></tr></table>
@@ -442,11 +445,14 @@
<tr><td valign="top" style="font-size: 14px;">
<!-- GOOGLE MAPS LINK -->
<table t-if="event_address" style="width:100%;"><tr><td>
<table t-if="event_address and location" style="width:100%;"><tr><td>
<div>
<i class="fa fa-map-marker"/>
<a t-attf-href="https://maps.google.com/maps?q={{ location }}" target="new">
See location on Google Maps
<img t-if="event_address.static_map_url and event_address.static_map_url_is_valid"
t-attf-src="{{ event_address.static_map_url }}"
style="vertical-align:bottom; width: 100%;" alt="Google Maps"/>
<span t-else="">See location on Google Maps</span>
</a>
</div>
</td></tr></table>
+47 -1
View File
@@ -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
+65 -1
View File
@@ -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)
@@ -52,6 +52,25 @@
</block>
</app>
</xpath>
<xpath expr="//setting[@id='email-outlook-setting']" position="after">
<setting string="Google Maps" help="Insert dynamic Google Maps in your email templates"
documentation="https://developers.google.com/maps/documentation/maps-static/get-api-key">
<field name="use_google_maps_static_api"/>
<div attrs="{'invisible': [('use_google_maps_static_api', '=', False)]}">
<div class="content-group mt16">
<label for="google_maps_static_api_key" class="o_form_label col-lg-3 o_light_label"/>
<field name="google_maps_static_api_key" string="Key"
attrs="{'required': [('use_google_maps_static_api','=',True)]}"/>
</div>
<div class="content-group">
<label for="google_maps_static_api_secret" class="o_form_label col-lg-3 o_light_label"/>
<field name="google_maps_static_api_secret" string="Secret"
attrs="{'required': [('use_google_maps_static_api','=',True)]}"/>
</div>
</div>
</setting>
</xpath>
</field>
</record>
@@ -42,7 +42,10 @@
<div class="mt16 text-warning"><strong>Save</strong> this page and come back here to set up the feature.</div>
</div>
</setting>
<setting string="Use an Outlook Server" help="Send and receive emails through your Outlook account." documentation="https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app" attrs="{'invisible': [('external_email_server_default', '=', False)]}">
<setting id="email-outlook-setting" string="Use an Outlook Server"
help="Send and receive emails through your Outlook account."
documentation="https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app"
attrs="{'invisible': [('external_email_server_default', '=', False)]}">
<field name="module_microsoft_outlook"/>
<div class="content-group" id="msg_module_microsoft_outlook"
attrs="{'invisible': [('module_microsoft_outlook','=',False)]}">
-10
View File
@@ -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'&lt;%', u'<%')
cleaned = cleaned.replace(u'%&gt;', u'%>')
# html considerations so real html content match database value
cleaned = cleaned.replace(u'\xa0', u'&nbsp;')
except etree.ParserError as e: