[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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]}">
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user