This repository has been archived by the owner on Feb 25, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 30
/
maps.py
425 lines (347 loc) · 16.8 KB
/
maps.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
#!/usr/bin/python
# Copyright 2012 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy
# of the License at: http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distrib-
# uted under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
# OR CONDITIONS OF ANY KIND, either express or implied. See the License for
# specific language governing permissions and limitations under the License.
"""Main handler for displaying maps."""
__author__ = '[email protected] (Pete Giencke)'
import itertools
import json
import re
import urllib
import urlparse
import base_handler
import config
import metadata
import metadata_fetch
import model
import perms
import users
import utils
from google.appengine.ext import db
MAPS_API_BASE_URL = '//maps.google.com/maps/api/js'
ITEMS_PER_PAGE = 50
METADATA_CACHE = metadata_fetch.METADATA_CACHE
class ClientConfig(db.Model):
"""Client configuration allowing customization of the UI.
The term client is used in the sense of Maps API Premier client, not browser
or user. ClientConfigs will be allocated to particular partners allowing
them the ability to change some aspects of the UI when embedding on their
site. Key name: client ID, as specified in the 'client' URL param.
"""
# List of referers who are allowed to use this client config
allowed_referer_domains = db.StringListProperty()
# Whether or not to hide the footer in the UI
hide_footer = db.BooleanProperty(default=False)
# Whether or not to hide the share button in the UI
hide_share_button = db.BooleanProperty(default=False)
# Whether or not to hide the "My Location" button in the UI
hide_my_location_button = db.BooleanProperty(default=False)
# Allow a callback parameter so an embedding page can receive and control
# the map
allow_embed_map_callback = db.BooleanProperty(default=False)
# Whether or not to show the login state and links to sign in and sign out
show_login = db.BooleanProperty(default=False)
# The web property ID to use for tracking with Google Analytics
# If unspecified, the default Crisis Map Analytics ID is assigned downstream.
analytics_id = db.StringProperty(default='')
# HTML to be inserted into head.
custom_head_html = db.StringProperty(default='')
# Whether or not to activate the editing UI
enable_editing = db.BooleanProperty(default=False)
# Which side to show the layers panel on ('left' or 'right')
panel_side = db.StringProperty(default='right')
# Whether to float the panel over the map (instead of docking it to the side)
panel_float = db.BooleanProperty(default=False)
# Whether to hide the Google+ sharing button in the Share box
hide_google_plus_button = db.BooleanProperty(default=False)
# Whether to hide the Facebook Like button in the Share box
hide_facebook_button = db.BooleanProperty(default=False)
# Whether to hide the Twitter sharing button in the Share box
hide_twitter_button = db.BooleanProperty(default=False)
# Whether to display minimal map controls (small zoom control, no
# scale control, no pegman).
minimal_map_controls = db.BooleanProperty(default=False)
# Whether to hide the map title and description from the panel.
hide_panel_header = db.BooleanProperty(default=False)
# Whether to show OpenStreetMap as a base map option to all users.
enable_osm_map_type = db.BooleanProperty(default=False)
# Whether to allow OpenStreetMap as a base map option in the editor.
enable_osm_map_type_editing = db.BooleanProperty(default=False)
# Whether to enable the layer filter in the panel.
enable_layer_filter = db.BooleanProperty(default=False)
# The API key to use for Google APIs (from the Google APIs Console).
google_api_key = db.StringProperty(default='')
# Whether or not to enable the tabbed UI.
use_tab_panel = db.BooleanProperty(default=False)
# Whether or not to show feature details in the tabbed panel.
use_details_tab = db.BooleanProperty(default=False)
# URL endpoint for UrlShortener API.
urlshortener_api_url = db.StringProperty(default='')
# Note: When adding future settings, the default value should reflect the
# behavior prior to the introduction of the new setting. To avoid confusion
# with None, the default value for Boolean settings should always be False.
@classmethod
def Create(cls, client_id, **kwargs):
"""Creates a ClientConfig entity for a given client_id.
Args:
client_id: A string, to use as the key for the model.
**kwargs: Values for the properties of the ClientConfig entity (see the
class definition for the list of available properties).
Returns:
A new ClientConfig entity.
"""
return cls(key_name=client_id, **kwargs)
def AsDict(self):
"""Converts this entity to a dict suitable for sending to the UI as JSON."""
return dict((k, getattr(self, k)) for k in self.properties()
if k != 'allowed_referer_domains')
def GetClientConfig(client_id, referer, dev_mode=False):
"""Returns the specified config if the client is permitted to use it.
If the client_id is empty or invalid, or the referer doesn't have permission
to use it, the 'default' configuration is returned.
Args:
client_id: A string or None, the client parameter from the URL.
referer: A string or None, the "Referer" header from the request.
dev_mode: If True, permit any config regardless of the "Referer" value.
Returns:
A dictionary containing the properties of the active ClientConfig.
"""
client_id = client_id or 'default'
client_config = ClientConfig.get_by_key_name(client_id)
if client_config is None:
return {}
if dev_mode or client_id == 'default':
return client_config.AsDict()
referer_host = urlparse.urlparse(referer or '').hostname
if referer_host:
for allowed_domain in client_config.allowed_referer_domains:
# referer_host is valid if it ends with allowed_domain and
# the preceding character does not exist or is a dot.
if (referer_host == allowed_domain or
referer_host.endswith('.' + allowed_domain)):
return client_config.AsDict()
return {}
def GetMapPickerItems(domain, root_path):
"""Fetches the list of maps to show in the map picker menu for a given domain.
Args:
domain: A string, the domain whose catalog to fetch.
root_path: The relative path to the Crisis Map site root.
Returns:
A list of {'title': ..., 'url': ...} dictionaries describing menu items
corresponding to the CatalogEntry entities for the specified domain.
"""
map_picker_items = []
# Add menu items for the CatalogEntry entities that are marked 'listed'.
if domain:
if domain == config.Get('primary_domain'):
map_picker_items = [
{'title': entry.title, 'url': root_path + '/' + entry.label}
for entry in list(model.CatalogEntry.GetListed(domain))]
else:
map_picker_items = [
{'title': entry.title,
'url': root_path + '/%s/%s' % (entry.domain, entry.label)}
for entry in list(model.CatalogEntry.GetListed(domain))]
# Return all the menu items sorted by title.
return sorted(map_picker_items, key=lambda m: m['title'])
def GetMapsApiClientId(host_port):
"""Determines the Maps API client ID to use."""
hostname = host_port.split(':')[0]
# "&client=google-crisis-response" only works for Google domains.
for domain in ['google.org', 'google.com']:
if hostname == domain or hostname.endswith('.' + domain):
return 'google-crisis-response'
# On localhost, development servers, etc., don't set a client ID, as it would
# cause Maps API to disable itself with an "unauthorized" error message.
return ''
def GetConfig(request, map_object=None, catalog_entry=None, xsrf_token=''):
dev_mode = request.get('dev') and users.IsDeveloper()
map_picker_items = GetMapPickerItems(
catalog_entry and catalog_entry.domain or
config.Get('primary_domain'), request.root_path)
# Fill the cm_config dictionary.
root = request.root_path
xsrf_qs = '?xsrf_token=' + xsrf_token # needed for all POST URLs
result = {
'dev_mode': dev_mode,
'langs': base_handler.ALL_LANGUAGES,
# Each endpoint that the JS client code uses gets an entry in config.
'js_root': root + '/.js',
'json_proxy_url': root + '/.jsonp',
'kmlify_url': request.host_url + root + '/.kmlify',
'login_url': users.GetLoginUrl(request.url),
'logout_url': users.GetLogoutUrl(request.url),
'map_picker_items': map_picker_items,
'protect_url': root + '/.protect',
'report_query_url': root + '/.api/reports',
'report_post_url': root + '/.api/reports' + xsrf_qs,
'vote_post_url': root + '/.api/votes' + xsrf_qs,
'static_content_url': root + '/.static',
'user_email': users.GetCurrent() and users.GetCurrent().email,
'wms_configure_url': root + '/.wms/configure',
'wms_tiles_url': root + '/.wms/tiles'
}
# Add settings from the selected client config, if any.
result.update(GetClientConfig(request.get('client'),
request.headers.get('referer'), dev_mode))
# Add the MapRoot data and other map-specific information.
if catalog_entry: # published map
map_root = result['map_root'] = catalog_entry.map_root
result['label'] = catalog_entry.label
result['publisher_name'] = catalog_entry.publisher_name
key = catalog_entry.map_version_key
elif map_object: # draft map
map_root = result['map_root'] = map_object.map_root
result['map_list_url'] = root + '/.maps'
result['diff_url'] = root + '/.diff/' + map_object.id + xsrf_qs
result['save_url'] = root + '/.api/maps/' + map_object.id + xsrf_qs
result['share_url'] = root + '/.share/' + map_object.id + xsrf_qs
result['api_maps_url'] = root + '/.api/maps'
result['legend_url'] = root + '/.legend'
result['wms_query_url'] = root + '/.wms/query'
result['enable_editing'] = map_object.CheckAccess(perms.Role.MAP_EDITOR)
result['draft_mode'] = True
key = map_object.current_version_key
# Parameters that depend on the MapRoot, for both published and draft maps.
ui_region = request.get('gl')
if map_object or catalog_entry:
result['lang'] = base_handler.SelectLanguageForRequest(request, map_root)
ui_region = map_root.get('region', ui_region)
cache_key, sources = metadata.CacheSourceAddresses(key, result['map_root'])
result['metadata'] = {s: METADATA_CACHE.Get(s) for s in sources}
result['metadata_url'] = root + '/.metadata?ck=' + cache_key
metadata.ActivateSources(sources)
# Construct the URL for the Maps JavaScript API.
api_url_params = {
'sensor': 'false',
'libraries': 'places,search,visualization,weather',
'client': GetMapsApiClientId(request.host),
'language': request.lang
}
if ui_region:
api_url_params['region'] = ui_region
result['maps_api_url'] = (MAPS_API_BASE_URL + '?' +
urllib.urlencode(api_url_params))
maproot_url = request.get('maproot_url', '')
if dev_mode or maproot_url.startswith(request.root_url + '/'):
# It's always okay to fetch MapRoot JSON from a URL if it's from this app.
# In developer mode only, allow MapRoot JSON from arbitrary URLs.
result['maproot_url'] = maproot_url
if dev_mode:
# In developer mode only, allow query params to override the result.
# Developers can also specify map_root directly as a query param.
for name in (
ClientConfig.properties().keys() + ['map_root', 'use_tab_panel']):
value = request.get(name)
if value:
result[name] = json.loads(value)
return result
class MapByLabel(base_handler.BaseHandler):
"""Handler for displaying a published map by its domain and label."""
embeddable = True
def Get(self, label, domain=None): # pylint: disable=g-bad-name
"""Displays a published map by its domain and publication label."""
domain = domain or config.Get('primary_domain') or ''
entry = model.CatalogEntry.Get(domain, label)
if not entry:
# Fall back to the map list for users that go to /crisismap/maps.
# TODO(kpy): Remove this when the UI has a way to get to the map list.
if label == 'maps':
return self.redirect('.maps')
raise base_handler.Error(404, 'Label %s/%s not found.' % (domain, label))
cm_config = GetConfig(self.request, catalog_entry=entry,
xsrf_token=self.xsrf_token)
map_root = cm_config.get('map_root', {})
# SECURITY NOTE: cm_config_json is assumed to be safe JSON, and head_html
# is assumed to be safe HTML; all other template variables are autoescaped.
# Below, we use cm_config.pop() for template variables that aren't part of
# the API understood by google.cm.Map() and don't need to stay in cm_config.
self.response.out.write(self.RenderTemplate('map.html', {
'maps_api_url': cm_config.pop('maps_api_url', ''),
'head_html': cm_config.pop('custom_head_html', ''),
'lang': cm_config['lang'],
'lang_lower': cm_config['lang'].lower().replace('-', '_'),
'json_proxy_url': cm_config['json_proxy_url'],
'maproot_url': cm_config.pop('maproot_url', ''),
'map_title': map_root.get('title', '') + ' | Google Crisis Map',
'map_description': ToPlainText(map_root.get('description')),
'map_url': self.request.path_url,
'map_image': map_root.get('thumbnail_url', ''),
'cm_config_json': base_handler.ToHtmlSafeJson(cm_config)
}))
def ToPlainText(desc):
"""Converts a map description to plain text for use in a <meta> tag."""
desc = desc or '' # accommodate None
block_tag = re.compile(r'<(p|div|br|li|td)[^>]*>', re.I)
slashes = re.compile(r'(\s| )+(/(\s| )+)+')
# Replace block tags with ' / ' and compress multiple occurrences of ' / '.
desc = re.sub(slashes, ' / ', re.sub(block_tag, ' / ', desc))
# Strip all other HTML tags.
return utils.StripHtmlTags(desc)
class MapById(base_handler.BaseHandler):
"""Handler for displaying a map by its map ID."""
embeddable = True
def Get(self, map_id, domain=None): # pylint: disable=g-bad-name
"""Displays a map in draft mode by its map ID."""
map_object = model.Map.Get(map_id)
if not map_object:
raise base_handler.Error(404, 'Map %r not found.' % map_id)
if not domain or domain != map_object.domain:
# The canonical URL for a map contains both the domain and the map ID.
url = '../%s/.maps/%s' % (map_object.domain, map_id)
if self.request.GET: # preserve query params on redirect
url += '?' + urllib.urlencode(self.request.GET.items())
return self.redirect(url)
cm_config = GetConfig(self.request, map_object=map_object,
xsrf_token=self.xsrf_token)
# SECURITY NOTE: cm_config_json is assumed to be safe JSON, and head_html
# is assumed to be safe HTML; all other template variables are autoescaped.
# TODO(kpy): Factor out the bits common to MapByLabel.Get and MapById.Get.
self.response.out.write(self.RenderTemplate('map.html', {
'maps_api_url': cm_config.pop('maps_api_url', ''),
'head_html': cm_config.pop('custom_head_html', ''),
'lang': cm_config['lang'],
'lang_lower': cm_config['lang'].lower().replace('-', '_'),
'cm_config_json': base_handler.ToHtmlSafeJson(cm_config)
}))
class MapList(base_handler.BaseHandler):
"""Handler for the user's list of maps."""
def Get(self, user, domain=None): # pylint: disable=g-bad-name
"""Produces the map listing page."""
title = 'Maps for all domains'
if domain:
title = 'Maps for %s' % domain
# Get ITEMS_PER_PAGE + 1 items so we know whether there is a next page.
skip = int(self.request.get('skip', '0'))
maps = list(itertools.islice(
model.Map.GetViewable(user, domain), skip, skip + ITEMS_PER_PAGE + 1))
more_items = len(maps) > ITEMS_PER_PAGE
maps = maps[:ITEMS_PER_PAGE]
# Attach to each Map a 'catalog_entries' attribute with a list of the
# CatalogEntry objects that link to that Map.
published = {}
for entry in model.CatalogEntry.GetAll():
published.setdefault(entry.map_id, []).append(entry)
for m in maps:
m.catalog_entries = sorted(
published.get(m.id, []), key=lambda e: (e.domain, e.label))
self.response.out.write(self.RenderTemplate('map_list.html', {
'title': title,
'maps': maps,
'first': skip + 1,
'last': skip + len(maps),
'more_items': more_items,
'prev_page_url':
self.request.path_url + '?skip=%d' % max(0, skip - ITEMS_PER_PAGE),
'next_page_url':
self.request.path_url + '?skip=%d' % (skip + ITEMS_PER_PAGE),
'catalog_domains': sorted(
perms.GetAccessibleDomains(user, perms.Role.CATALOG_EDITOR))
}))