require 'active_record' require 'active_record/connection_adapters/postgresql/oid/json' require_relative './carto_json_serializer' require_dependency 'carto/table_utils' require_dependency 'carto/query_rewriter' module Carto module LayerTableDependencies private def affected_tables return [] unless maps.first.present? && options.present? node_id = options.symbolize_keys[:source] if node_id.present? visualization_id = map.visualization.id node = AnalysisNode.find_by_natural_id(visualization_id, node_id) return [] unless node dependencies = node.source_descendants.map do |source_node| tables_by_query = tables_from_query(source_node.params[:query]) table_name = source_node.options[:table_name] tables_by_name = table_name ? tables_from_names([table_name], user) : [] tables_by_query.present? ? tables_by_query : tables_by_name end dependencies.flatten.compact.uniq else tables_by_query = tables_from_query_option dependencies = tables_by_query.present? ? tables_by_query : tables_from_table_name_option dependencies.compact.uniq end end def tables_from_query_option tables_from_query(query) end def tables_from_query(query) query.present? ? tables_from_names(affected_table_names(query), user) : [] rescue StandardError => e # INFO: this covers changes that CartoDB can't track, so we must handle it gracefully. # For example, if layer SQL contains wrong SQL (uses a table that doesn't exist, or uses an invalid operator). # This warning level is checked in tests to ensure that embed view does not need user DB connection, # so we need to keep it (or change the tests accordingly) log_warning(message: 'Could not retrieve tables from query', exception: e, current_user: user, layer: attributes) [] end def tables_from_table_name_option return [] if options.empty? sym_options = options.symbolize_keys user_name = sym_options[:user_name] table_name = sym_options[:table_name] schema_prefix = user_name.present? && table_name.present? && !table_name.include?('.') ? %{"#{user_name}".} : '' tables_from_names(["#{schema_prefix}#{table_name}"], user) end end class Layer < ActiveRecord::Base include Carto::TableUtils include LayerTableDependencies include Carto::QueryRewriter attribute :options, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json.new attribute :infowindow, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json.new attribute :tooltip, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json.new serialize :options, CartoJsonSerializer serialize :infowindow, CartoJsonSerializer serialize :tooltip, CartoJsonSerializer has_many :layers_maps, dependent: :destroy has_many :maps, through: :layers_maps, after_add: :after_added_to_map has_many :layers_user, dependent: :destroy has_many :users, through: :layers_user, after_add: :set_default_order has_many :layers_user_tables, dependent: :destroy has_many :user_tables, through: :layers_user_tables, class_name: Carto::UserTable has_many :widgets, -> { order(:order) }, class_name: Carto::Widget has_many :legends, -> { order(:created_at) }, class_name: Carto::Legend, dependent: :destroy has_many :layer_node_styles before_destroy :ensure_not_viewer before_save :lock_user_tables after_save :invalidate_maps, :update_layer_node_style after_save :register_table_dependencies, if: :data_layer? ALLOWED_KINDS = %w{carto tiled background gmapsbase torque wms}.freeze validates :kind, inclusion: { in: ALLOWED_KINDS } validate :validate_not_viewer TEMPLATES_MAP = { 'table/views/infowindow_light' => 'infowindow_light', 'table/views/infowindow_dark' => 'infowindow_dark', 'table/views/infowindow_light_header_blue' => 'infowindow_light_header_blue', 'table/views/infowindow_light_header_yellow' => 'infowindow_light_header_yellow', 'table/views/infowindow_light_header_orange' => 'infowindow_light_header_orange', 'table/views/infowindow_light_header_green' => 'infowindow_light_header_green', 'table/views/infowindow_header_with_image' => 'infowindow_header_with_image' }.freeze def set_default_order(parent) # Reload maps upon adding this layer to a map (AR doesn't do this automatically) maps.reload if persisted? return unless order.nil? max_order = parent.layers.map(&:order).compact.max || -1 self.order = max_order + 1 save if persisted? end # Sequel model compatibility (for TableBlender) # TODO: Remove this after `::UserTable` deletion, and inline into TableBlender def add_map(map) map.layers << self end def user_tables_readable_by(user) user_tables.select { |ut| ut.readable_by?(user) } end def data_readable_by?(user) user_tables.all? { |ut| ut.readable_by?(user) } end def legend @legend ||= options['legend'] end def qualified_table_name(schema_owner_user = nil) table_name = options['table_name'] if table_name.present? && table_name.include?('.') table_name else schema_prefix = schema_owner_user.nil? ? '' : "#{schema_owner_user.sql_safe_database_schema}." "#{schema_prefix}#{safe_table_name_quoting(options['table_name'])}" end end # INFO: for vizjson v3 this is not used, see VizJSON3LayerPresenter#to_vizjson_v3 def infowindow_template_path if infowindow.present? && infowindow['template_name'].present? template_name = TEMPLATES_MAP.fetch(infowindow['template_name'], infowindow['template_name']) Rails.root.join("lib/assets/javascripts/cartodb/table/views/infowindow/templates/#{template_name}.jst.mustache") end end # INFO: for vizjson v3 this is not used, see VizJSON3LayerPresenter#to_vizjson_v3 def tooltip_template_path if tooltip.present? && tooltip['template_name'].present? template_name = TEMPLATES_MAP.fetch(tooltip['template_name'], tooltip['template_name']) Rails.root.join("lib/assets/javascripts/cartodb/table/views/tooltip/templates/#{template_name}.jst.mustache") end end def basemap? gmapsbase? || tiled? end def base_layer? tiled? || background? || gmapsbase? || wms? end def torque? kind == 'torque' end def data_layer? !base_layer? end def user_layer? tiled? || background? || gmapsbase? || wms? end def named_map_layer? tiled? || background? || gmapsbase? || wms? || carto? end def carto? kind == 'carto' end def tiled? kind == 'tiled' end def background? kind == 'background' end def gmapsbase? kind == 'gmapsbase' end def wms? kind == 'wms' end def supports_labels_layer? basemap? && options["labels"] && options["labels"]["urlTemplate"] end def map maps[0] end def visualization map.visualization if map end def user @user ||= map.nil? ? nil : map.user end def default_query(user = nil, database_schema = nil) sym_options = options.symbolize_keys query = sym_options[:query] if query.present? query else user_username = user.nil? ? nil : user.username user_name = sym_options[:user_name] || user_username table_name = sym_options[:table_name] qualify = (user && user.organization_user?) || user_username != user_name if table_name.present? && !table_name.include?('.') && user_name.present? && qualify "SELECT * FROM #{safe_table_name_quoting(user_name)}.#{safe_table_name_quoting(table_name)}" elsif database_schema.present? "SELECT * FROM #{safe_table_name_quoting(database_schema)}.#{safe_table_name_quoting(table_name)}" else "SELECT * FROM #{qualified_table_name}" end end end def register_table_dependencies if data_layer? if persisted? user_tables.reload maps.reload end self.user_tables = affected_tables end end def fix_layer_user_information(old_username, new_user, renamed_tables) new_username = new_user.username if options.key?(:user_name) old_username = options[:user_name] || old_username options[:user_name] = new_username end if options.key?(:table_name) old_table_name = options[:table_name] options[:table_name] = renamed_tables.fetch(old_table_name, old_table_name) end # query_history is not modified as a safety measure for cases where this naive replacement doesn't work query = options[:query] options[:query] = rewrite_query(query, old_username, new_user, renamed_tables) if query.present? end def force_notify_change map.force_notify_map_change if map end def custom? CUSTOM_CATEGORIES.include?(category) end def category options && options['category'] end def rename_table(current_table_name, new_table_name) return self unless data_layer? target_keys = %w{table_name tile_style query} targets = options.select { |key, _| target_keys.include?(key) } renamed = targets.map do |key, value| [key, rename_in(value, current_table_name, new_table_name)] end self.options = options.merge(renamed.to_h) self end def uses_private_tables? user_tables.any?(&:private?) end def after_added_to_map(map) # This avoids unnecessary operations for in-memory logic. Example: Mapcap recreation. See #12473. return unless map.persisted? set_default_order(map) register_table_dependencies end def depends_on?(user_table) layers_user_tables.map(&:user_table_id).include?(user_table.id) end def source_id options.symbolize_keys[:source] end def qualify_for_organization(owner_username) options['query'] = qualify_query(query, options['table_name'], owner_username) if query end private # The table dependencies will only be updated after the layer. However, when deleting them, they need to be deleted # before the model. This can cause deadlocks with simultaneous request to update and delete the model. # This request a explicit lock to PostgreSQL so the tables are always accessed in the same order. #11443 def lock_user_tables user_tables.lock.all if persisted? end def rename_in(target, anchor, substitution) return if target.blank? regex = /(\A|\W+)(#{anchor})(\W+|\z)/ target.gsub(regex) { |match| match.gsub(anchor, substitution) } end CUSTOM_CATEGORIES = %w{Custom NASA TileJSON Mapbox WMS}.freeze def tables_from_names(table_names, user) ::Table.get_all_user_tables_by_names(table_names, user) end def affected_table_names(query) return [] unless query.present? query_tables = user.in_database.execute("SELECT unnest(CDB_QueryTablesText(#{user.in_database.quote(query)}))") query_tables.column_values(0).uniq end def query options.symbolize_keys[:query] end def invalidate_maps maps.each(&:notify_map_change) end def ensure_not_viewer raise CartoDB::InvalidMember.new(user: "Viewer users can't destroy layers") if user && user.viewer end def validate_not_viewer errors.add(:maps, "Viewer users can't edit layers") if maps.any? { |m| m.user && m.user.viewer } end def update_layer_node_style style = current_layer_node_style if style style.update_from_layer(self) style.save end end def current_layer_node_style return nil unless source_id layer_node_styles.where(source_id: source_id).first || LayerNodeStyle.new(layer: self, source_id: source_id) end end end