cartodb/app/models/layer/presenter.rb
2020-06-15 10:58:47 +08:00

303 lines
10 KiB
Ruby

require 'json'
require 'ejs'
require_dependency 'carto/table_utils'
module CartoDB
# Natural naming would be "Layer", but it collides with Layer class
module LayerModule
class Presenter
include Carto::TableUtils
EMPTY_CSS = '#dummy{}'
TORQUE_ATTRS = %w(
table_name
user_name
property
blendmode
resolution
countby
torque-duration
torque-steps
torque-blend-mode
query
source
tile_style
named_map
visible
)
INFOWINDOW_KEYS = %w(
fields template_name template alternative_names width maxHeight
)
def initialize(layer, options={}, configuration={}, decoration_data={})
@layer = layer
@options = options
@configuration = configuration
@decoration_data = decoration_data
end
def to_vizjson_v2
if base?(layer)
with_kind_as_type(layer.public_values).symbolize_keys
elsif torque?(layer)
as_torque
else
{
id: layer.id,
type: 'CartoDB',
infowindow: infowindow_data_v2,
tooltip: tooltip_data_v2,
legend: layer.legend,
order: layer.order,
visible: layer.public_values['options']['visible'],
options: options_data_v2
}
end
end
# Old layers_controller directly does layer.to_json, but to be uniform with new controller,
# always call through the presenter at least in tests
def to_json(*args)
layer.to_json(*args)
end
def to_vizjson_v1
return layer.public_values.symbolize_keys if base?(layer)
{
id: layer.id,
kind: 'CartoDB',
infowindow: infowindow_data_v1,
order: layer.order,
options: options_data_v1
}
end
def to_poro
# .merge left for backwards compatibility
poro = layer.public_values
return poro unless poro['options']
# INFO changed to support new presenter's way of sending owner
if @options[:user] && !poro['options']['user_name'].present?
user_name = @options[:user].username
schema_name = @options[:user].sql_safe_database_schema
elsif poro['options']['user_name'].present?
user_name = poro['options']['user_name']
schema_name = poro['options']['user_name']
end
if options[:viewer_user] && user_name && poro['options']['table_name']
# if the table_name already have a schema don't add another one
# this case happens when you share a layer already shared with you
if user_name != options[:viewer_user].username && !poro['options']['table_name'].include?('.')
poro['options']['table_name'] = safe_schema_and_table_quoting(schema_name, poro['options']['table_name'])
end
end
poro
end
private
attr_reader :layer, :options, :configuration
# Decorates the layer presentation with data if needed. nils on the decoration act as removing the field
def decorate_with_data(source_hash, decoration_data)
decoration_data.each { |key, value|
source_hash[key] = value
source_hash.delete_if { |k, v|
v.nil?
}
}
source_hash
end
def base?(layer)
['tiled', 'background', 'gmapsbase', 'wms'].include? layer.kind
end
def torque?(layer)
layer.kind == 'torque'
end
def with_kind_as_type(attributes)
decorate_with_data(attributes.merge(type: attributes.delete('kind')), @decoration_data)
end
def as_torque
api_templates_type = options.fetch(:https_request, false) ? 'private' : 'public'
layer_options = decorate_with_data(
# Make torque always have a SQL query too (as vizjson v2)
layer.options.merge({ 'query' => wrap(sql_from(layer.options), layer.options) }),
@decoration_data
)
{
id: layer.id,
type: 'torque',
order: layer.order,
legend: layer.legend,
options: {
stat_tag: options.fetch(:visualization_id),
maps_api_template: fix_torque_maps_api_template(ApplicationHelper.maps_api_template(api_templates_type)),
sql_api_template: ApplicationHelper.sql_api_template(api_templates_type),
# tiler_* is kept for backwards compatibility
tiler_protocol: (configuration[:tiler]["public"]["protocol"] rescue nil),
tiler_domain: (configuration[:tiler]["public"]["domain"] rescue nil),
tiler_port: (configuration[:tiler]["public"]["port"] rescue nil),
# sql_api_* is kept for backwards compatibility
sql_api_protocol: (configuration[:sql_api]["public"]["protocol"] rescue nil),
sql_api_domain: (configuration[:sql_api]["public"]["domain"] rescue nil),
sql_api_endpoint: (configuration[:sql_api]["public"]["endpoint"] rescue nil),
sql_api_port: (configuration[:sql_api]["public"]["port"] rescue nil),
layer_name: name_for(layer),
}.merge(
layer_options.select { |k| TORQUE_ATTRS.include? k })
}
end
def infowindow_data_v1
with_template(layer.infowindow, layer.infowindow_template_path)
rescue => e
CartoDB::Logger.error(exception: e)
throw e
end
def infowindow_data_v2
whitelisted_infowindow(with_template(layer.infowindow, layer.infowindow_template_path))
rescue => e
CartoDB::Logger.error(exception: e)
throw e
end
def tooltip_data_v2
whitelisted_infowindow(with_template(layer.tooltip, layer.tooltip_template_path))
rescue => e
CartoDB::Logger.error(exception: e)
throw e
end
def with_template(infowindow, path)
# Careful with this logic:
# - nil means absolutely no infowindow (e.g. a torque)
# - path = nil or template filled: either pre-filled or custom infowindow, nothing to do here
# - template and path not nil but template not filled: stay and fill
return nil if infowindow.nil?
template = infowindow['template']
return infowindow if (!template.nil? && !template.empty?) || path.nil?
infowindow.merge!(template: File.read(path))
infowindow
end
def options_data_v2
if options[:full]
decorate_with_data(layer.options, @decoration_data)
else
layer_options = layer.options
data = {
layer_name: name_for(layer),
cartocss: css_from(layer.options),
cartocss_version: layer.options.fetch('style_version'),
interactivity: layer.options.fetch('interactivity')
}
source = layer.options['source']
if options[:for_named_map] && source
data[:source] = { id: source }
else
data[:sql] = wrap(sql_from(layer.options), layer.options)
end
if layer_options['style_properties'] && (layer_options['style_properties']['autogenerated'] == false || layer_options['style_properties']['autogenerated'].nil?)
sql_wrap = layer_options['sql_wrap'] || layer_options['query_wrapper']
data['sql_wrap'] = sql_wrap if sql_wrap.present?
end
data = decorate_with_data(data, @decoration_data)
viewer = options[:viewer_user]
if viewer
unless data['user_name'] == viewer.username
data['table_name'] = "\"#{data['user_name']}\".#{data['table_name']}"
end
end
data
end
end
def name_for(layer)
layer_alias = layer.options.fetch('table_name_alias', nil)
table_name = layer.options.fetch('table_name')
return table_name unless layer_alias && !layer_alias.empty?
layer_alias
end
def options_data_v1
return layer.options if options[:full]
layer.options.select { |key, value| public_options.include?(key.to_s) }
end
def sql_from(options)
query = options.fetch('query', '')
return default_query_for(options) if query.nil? || query.empty?
query
end
def css_from(options)
style = options.include?('tile_style') ? options['tile_style'] : nil
(style.nil? || style.strip.empty?) ? EMPTY_CSS : options.fetch('tile_style')
end
def wrap(query, options)
wrapper = options.fetch('query_wrapper', nil)
return query if wrapper.nil? || wrapper.empty?
EJS.evaluate(wrapper, sql: query)
end
def default_query_for(layer_options)
if options[:viewer_user]
unless layer_options['user_name'] == options[:viewer_user].username
name = if layer_options['user_name'] && layer_options['user_name'].include?('-')
"\"#{layer_options['user_name']}\""
else
layer_options['user_name']
end
return "select * from #{name}.#{safe_table_name_quoting(layer_options['table_name'])}"
end
end
"select * from #{safe_table_name_quoting(layer_options.fetch('table_name'))}"
end
def public_options
return configuration if configuration.empty?
configuration.fetch(:layer_opts).fetch("public_opts")
end
def whitelisted_infowindow(infowindow)
infowindow.nil? ? nil : infowindow.select { |key, value|
INFOWINDOW_KEYS.include?(key) || INFOWINDOW_KEYS.include?(key.to_s)
}
end
def fix_torque_maps_api_template(maps_api_template)
if visualization_owner_is_table_owner?
maps_api_template
else
# This fixes #9017 avoding Torque request to table owner named map instead of visualization owner.
# See https://github.com/CartoDB/torque/blob/8a14fe546ac411829b5f52bb575526ad5ecb79f8/lib/torque/provider/windshaft.js#L361
maps_api_template.gsub('{user}', layer.user.username)
end
end
def visualization_owner_is_table_owner?
layer.options.nil? || layer.options['user_name'].nil? || layer.user.nil? ||
layer.options['user_name'] == layer.user.username
end
end
end
end