require 'active_record' require 'cartodb-common' require_relative '../visualization/stats' require_relative '../quota_checker' require_dependency 'carto/named_maps/api' require_dependency 'carto/helpers/auth_token_generator' require_dependency 'carto/uuidhelper' require_dependency 'carto/visualization_invalidation_service' require_dependency 'carto/visualization_backup_service' module Carto::VisualizationDependencies def fully_dependent_on?(user_table) derived? && layers_dependent_on(user_table).count == data_layers.count end def partially_dependent_on?(user_table) derived? && layers_dependent_on(user_table).count.between?(1, data_layers.count - 1) end def dependent_on?(user_table) derived? && layers_dependent_on(user_table).any? end private def layers_dependent_on(user_table) data_layers.select { |l| l.depends_on?(user_table) } end end class Carto::Visualization < ActiveRecord::Base include CacheHelper include Carto::UUIDHelper include Carto::AuthTokenGenerator include Carto::VisualizationDependencies include Carto::VisualizationBackupService TYPE_CANONICAL = 'table'.freeze TYPE_DERIVED = 'derived'.freeze TYPE_SLIDE = 'slide'.freeze TYPE_REMOTE = 'remote'.freeze TYPE_KUVIZ = 'kuviz'.freeze TYPE_APP = 'app'.freeze VALID_TYPES = [TYPE_CANONICAL, TYPE_DERIVED, TYPE_SLIDE, TYPE_REMOTE, TYPE_KUVIZ, TYPE_APP].freeze MAP_TYPES = [TYPE_DERIVED, TYPE_KUVIZ].freeze KIND_GEOM = 'geom'.freeze KIND_RASTER = 'raster'.freeze PRIVACY_PUBLIC = 'public'.freeze PRIVACY_PRIVATE = 'private'.freeze PRIVACY_LINK = 'link'.freeze PRIVACY_PROTECTED = 'password'.freeze PRIVACIES = [PRIVACY_LINK, PRIVACY_PROTECTED, PRIVACY_PUBLIC, PRIVACY_PRIVATE].freeze VERSION_BUILDER = 3 V2_VISUALIZATIONS_REDIS_KEY = 'vizjson2_visualizations'.freeze scope :remotes, -> { where(type: TYPE_REMOTE) } # INFO: disable ActiveRecord inheritance column self.inheritance_column = :_type belongs_to :user, -> { select(Carto::User::DEFAULT_SELECT) }, inverse_of: :visualizations belongs_to :full_user, -> { readonly(true) }, class_name: Carto::User, inverse_of: :visualizations, primary_key: :id, foreign_key: :user_id belongs_to :permission, inverse_of: :visualization, dependent: :destroy has_many :likes, foreign_key: :subject has_many :shared_entities, foreign_key: :entity_id, inverse_of: :visualization, dependent: :destroy has_one :external_source, class_name: Carto::ExternalSource, dependent: :destroy, inverse_of: :visualization has_many :unordered_children, class_name: Carto::Visualization, foreign_key: :parent_id has_many :overlays, -> { order(:order) }, dependent: :destroy, inverse_of: :visualization belongs_to :active_layer, class_name: Carto::Layer belongs_to :map, class_name: Carto::Map, inverse_of: :visualization, dependent: :destroy has_one :asset, class_name: Carto::Asset, inverse_of: :visualization, dependent: :destroy has_many :related_templates, class_name: Carto::Template, foreign_key: :source_visualization_id has_one :synchronization, class_name: Carto::Synchronization, dependent: :destroy has_many :external_sources, class_name: Carto::ExternalSource has_many :analyses, class_name: Carto::Analysis has_many :mapcaps, -> { order('created_at DESC') }, class_name: Carto::Mapcap, dependent: :destroy has_one :state, class_name: Carto::State, autosave: true has_many :snapshots, class_name: Carto::Snapshot, dependent: :destroy validates :name, :privacy, :type, :user_id, :version, presence: true validates :privacy, inclusion: { in: PRIVACIES } validates :type, inclusion: { in: VALID_TYPES } validates :name, uniqueness: { scope: [:user_id, :type] }, if: -> { kuviz? || app? } validate :validate_password_presence validate :validate_privacy_changes validate :validate_user_not_viewer, on: :create before_validation :set_default_version, :set_register_table_only before_create :set_random_id, :set_default_permission before_save :remove_password_if_unprotected after_save :propagate_attribution_change after_save :propagate_privacy_and_name_to, if: :table before_destroy :backup_visualization after_commit :perform_invalidations attr_accessor :register_table_only def set_register_table_only self.register_table_only = false # This is a callback, returning `true` avoids halting because of assignment `false` return value true end def set_default_version self.version ||= user.try(:new_visualizations_version) end DELETED_COLUMNS = ['state_id', 'url_options'].freeze def self.columns super.reject { |c| DELETED_COLUMNS.include?(c.name) } end def size # Only canonical visualizations (Datasets) have a related table and then count against disk quota, # but we want to not break and even allow ordering by size multiple types if user_table user_table.size elsif remote? && external_source external_source.size else 0 end end def tags tags = super tags == nil ? [] : tags end def tags=(tags) tags.reject!(&:blank?) if tags super(tags) end def user_table map.user_table if map end def table @table ||= user_table.try(:service) end def layers_with_data_readable_by(user) return [] unless map map.layers.select { |l| l.data_readable_by?(user) } end def related_tables @related_tables ||= get_related_tables end def related_tables_readable_by(user) layers_with_data_readable_by(user).map { |l| l.user_tables_readable_by(user) }.flatten.uniq end def related_canonical_visualizations @related_canonical_visualizations ||= get_related_canonical_visualizations end def stats @stats ||= CartoDB::Visualization::Stats.new(self).to_poro end def transition_options @transition_options ||= (slide_transition_options.nil? ? {} : JSON.parse(slide_transition_options).symbolize_keys) end def transition_options=(value) self.slide_transition_options = ::JSON.dump(value.nil? ? DEFAULT_OPTIONS_VALUE : value) end def children ordered = [] children_vis = self.unordered_children if children_vis.count > 0 ordered << children_vis.select { |vis| vis.prev_id.nil? }.first while !ordered.last.next_id.nil? target = ordered.last.next_id unless target.nil? ordered << children_vis.select { |vis| vis.id == target }.first end end end ordered end # TODO: refactor next methods, all have similar naming but some receive user and some others user_id def liked_by?(user) likes_by_user(user).any? end def likes_by_user(user) likes.where(actor: user.id) end def add_like_from(user) unless has_read_permission?(user) raise UnauthorizedLikeError end likes.create!(actor: user.id) self rescue ActiveRecord::RecordNotUnique raise AlreadyLikedError end def remove_like_from(user) unless has_read_permission?(user) raise UnauthorizedLikeError end item = likes.where(actor: user.id) item.first.destroy unless item.first.nil? self end def send_like_email(current_viewer, vis_preview_image) if self.type == Carto::Visualization::TYPE_CANONICAL ::Resque.enqueue(::Resque::UserJobs::Mail::TableLiked, self.id, current_viewer.id, vis_preview_image) elsif self.type == Carto::Visualization::TYPE_DERIVED ::Resque.enqueue(::Resque::UserJobs::Mail::MapLiked, self.id, current_viewer.id, vis_preview_image) end end def is_viewable_by_user?(user) is_publically_accesible? || has_read_permission?(user) end def is_accesible_by_user?(user) is_viewable_by_user?(user) || password_protected? end def is_accessible_with_password?(user, password) is_viewable_by_user?(user) || password_valid?(password) end def is_publically_accesible? (public? || public_with_link?) && published? end def writable_by?(user) (user_id == user.id && !user.viewer?) || has_write_permission?(user) end def varnish_key "#{user.database_name}:#{sorted_related_table_names},#{id}" end def surrogate_key get_surrogate_key(CartoDB::SURROGATE_NAMESPACE_VISUALIZATION, id) end def qualified_name(viewer_user = nil) if viewer_user.nil? || owner?(viewer_user) name else "#{user.sql_safe_database_schema}.#{name}" end end # Despite storing always a named map, no need to retrieve it for "public" visualizations def retrieve_named_map? password_protected? || has_private_tables? end def has_password? !password_salt.nil? && !encrypted_password.nil? end def type_slide? type == TYPE_SLIDE end def kind_raster? kind == KIND_RASTER end def canonical? type == TYPE_CANONICAL end # TODO: remove. Kept for backwards compatibility with ::Permission model def table? type == TYPE_CANONICAL end def map? kuviz? || derived? end def derived? type == TYPE_DERIVED end def remote? type == TYPE_REMOTE end def kuviz? type == TYPE_KUVIZ end def app? type == TYPE_APP end def layers map ? map.layers : [] end def data_layers map ? map.data_layers : [] end def carto_layers map ? map.carto_layers : [] end def user_layers map ? map.user_layers : [] end def torque_layers map ? map.torque_layers : [] end def other_layers map ? map.other_layers : [] end def base_layers map ? map.base_layers : [] end def named_map_layers map ? map.named_map_layers : [] end def password_valid?(password) password_protected? && Carto::Common::EncryptionService.verify(password: password, secure_password: encrypted_password, salt: password_salt, secret: Cartodb.config[:password_secret]) end def organization? privacy == PRIVACY_PRIVATE && !permission.acl.empty? end def password_protected? privacy == PRIVACY_PROTECTED end def private? is_privacy_private? && !organization? end def is_privacy_private? privacy == PRIVACY_PRIVATE end def public? privacy == PRIVACY_PUBLIC end def public_with_link? self.privacy == PRIVACY_LINK end def editable? !(kind_raster? || type_slide?) end def get_auth_tokens [get_auth_token] end def mapviews @mapviews ||= CartoDB::Visualization::Stats.mapviews(stats) end def total_mapviews @total_mapviews ||= CartoDB::Visualization::Stats.new(self, nil).total_mapviews end def geometry_types @geometry_types ||= user_table.geometry_types if user_table end def table_service user_table.try(:service) end def has_read_permission?(user) user && (owner?(user) || (permission && permission.user_has_read_permission?(user))) end alias :can_view_private_info? :has_read_permission? def estimated_row_count user_table.try(:row_count) end def actual_row_count user_table.try(:actual_row_count) end def license_info if !license.nil? Carto::License.find(license.to_sym) end end def can_be_cached? !is_privacy_private? end def widgets # Preload widgets for all layers ActiveRecord::Associations::Preloader.new.preload(layers, :widgets) layers.map(&:widgets).flatten end def analysis_widgets widgets.select { |w| w.source_id.present? } end def attributions_from_derived_visualizations related_canonical_visualizations.map(&:attributions).reject(&:blank?) end def delete_from_table destroy if persisted? end def allowed_auth_tokens entities = [user] + permission.entities_with_read_permission entities.map(&:get_auth_token) end # - v2 (Editor): not private # - v3 (Builder): not derived or not private, mapcapped # This Ruby code should match the SQL code at Carto::VisualizationQueryBuilder#build section for @only_published. def published? !is_privacy_private? && (!builder? || !derived? || mapcapped?) end def builder? version == VERSION_BUILDER end MAX_MAPCAPS_PER_VISUALIZATION = 1 def create_mapcap! unless mapcaps.count < MAX_MAPCAPS_PER_VISUALIZATION mapcaps.last.destroy end auto_generate_indices_for_all_layers mapcaps.create! end def mapcapped? latest_mapcap.present? end def latest_mapcap mapcaps.first end def uses_builder_features? builder? || analyses.any? || widgets.any? || mapcapped? end def add_source_analyses return unless analyses.empty? data_layers.each_with_index do |layer, index| analysis = Carto::Analysis.source_analysis_for_layer(layer, index) if analysis.save layer.options[:source] = analysis.natural_id layer.options[:letter] = analysis.natural_id.first layer.save else log_warning(message: "Couldn't add source analysis for layer", current_user: user, layer: layer.attributes) end end end def ids_json layers_for_hash = layers.map do |layer| { layer_id: layer.id, widgets: layer.widgets.map(&:id) } end { visualization_id: id, map_id: map.id, layers: layers_for_hash } end def populate_ids(ids_json) self.id = ids_json[:visualization_id] map.id = ids_json[:map_id] map.layers.each_with_index do |layer, index| stored_layer_ids = ids_json[:layers][index] stored_layer_id = stored_layer_ids[:layer_id] layer.id = stored_layer_id layer.maps = [map] layer.widgets.each_with_index do |widget, widget_index| widget.id = stored_layer_ids[:widgets][widget_index] widget.layer_id = stored_layer_id end end end def for_presentation mapcapped? ? latest_mapcap.regenerate_visualization : self end # TODO: we should make visualization privacy/security methods aware of mapcaps and make those # deal with all the different the cases internally. # See https://github.com/CartoDB/cartodb/pull/9678 def non_mapcapped persisted? ? self : Carto::Visualization.find(id) end def mark_as_vizjson2 $tables_metadata.SADD(V2_VISUALIZATIONS_REDIS_KEY, id) end def uses_vizjson2? $tables_metadata.SISMEMBER(V2_VISUALIZATIONS_REDIS_KEY, id) > 0 end def open_in_editor? !builder? && uses_vizjson2? end def can_be_automatically_migrated_to_v3? overlays.builder_incompatible.none? end def state super ? super : build_state end def can_be_private? derived? ? user.try(:private_maps_enabled) : user.try(:private_tables_enabled) end # TODO: Backward compatibility with Sequel def store_using_table(_table_privacy_changed = false) store end def store save! self end def is_owner?(user) user && user.id == user_id end def unlink_from(user_table) layers_dependent_on(user_table).each do |layer| Carto::Analysis.find_by_natural_id(id, layer.source_id).try(:destroy) if layer.source_id layer.destroy end end def invalidate_after_commit # This marks this visualization as affected by this transaction, so AR will call its `after_commit` hook, which # performs the actual invalidations. This takes this operation outside of the DB transaction to avoid long locks if self.class.connection.open_transactions.zero? raise 'invalidate_after_commit should be called within a transaction' end add_to_transaction true end # TODO: Privacy manager compatibility, can be removed after removing ::UserTable alias :invalidate_cache :invalidate_after_commit def has_permission?(user, permission_type) return false if user.viewer && permission_type == Carto::Permission::ACCESS_READWRITE return is_owner?(user) if permission_id.nil? is_owner?(user) || permission.permitted?(user, permission_type) end def ensure_valid_privacy self.privacy = default_privacy if privacy.nil? self.privacy = PRIVACY_PUBLIC unless can_be_private? end def default_privacy can_be_private? ? PRIVACY_LINK : PRIVACY_PUBLIC end def privacy=(privacy) super(privacy.try(:downcase)) end def password=(value) if value.present? self.password_salt = "" self.encrypted_password = Carto::Common::EncryptionService.encrypt(password: value, secret: Cartodb.config[:password_secret]) end end def synced? synchronization.present? end def dependent_visualizations user_table&.dependent_visualizations || [] end def faster_dependent_visualizations(limit: nil) @faster_dependent_visualizations ||= user_table&.faster_dependent_visualizations(limit: limit) end def dependent_visualizations_count user_table&.dependent_visualizations_count.to_i end def backup_visualization(category = Carto::VisualizationBackup::CATEGORY_VISUALIZATION) return true if remote? if map && !destroyed? create_visualization_backup(visualization: self, category: category) end end def subscription if user_table doss = Carto::DoSyncServiceFactory.get_for_user(user) doss&.subscription_from_sync_table(user_table.name) end end private def remove_password self.password_salt = nil self.encrypted_password = nil end def perform_invalidations invalidation_service.invalidate rescue StandardError => e # This is called at an after_commit. If there's any error, we won't notice # but the after_commit chain stops. # This was discovered during #12844, because "Updates changes even if named maps communication fails" test # begun failing because Overlay#invalidate_cache invokes this method directly. # We chose to log and continue to keep coherence on calls to this outside the callback. log_error(message: "Error on visualization invalidation", exception: e, visualization: { id: id }) end def propagate_privacy_and_name_to raise "Empty table sent to propagate_privacy_and_name_to()" unless table propagate_privacy if privacy_changed? && canonical? propagate_name if name_was != name # name_changed? returns false positives in changes like a->A->a (sanitization) end def propagate_privacy table.reload if privacy && table.privacy_text.casecmp(privacy) != 0 # privacy is different, case insensitive CartoDB::TablePrivacyManager.new(table).set_from_visualization(self).update_cdb_tablemetadata end end def propagate_name # TODO: Move this to ::Table? return if table.changing_name? table.register_table_only = register_table_only table.name = name if table.name != name # Sanitization. For example, spaces -> _ update_column(:name, table.name) end table.update(name: name) if name_changed? support_tables.rename(name_was, name, true, name_was) end self rescue StandardError => exception if name_changed? && !(exception.to_s =~ /relation.*does not exist/) revert_name_change(name_was) end raise CartoDB::InvalidMember.new(exception.to_s) end def revert_name_change(previous_name) self.name = previous_name store rescue StandardError => exception raise CartoDB::InvalidMember.new(exception.to_s) end def propagate_attribution_change table.propagate_attribution_change(attributions) if table && attributions_changed? end def support_tables @support_tables ||= CartoDB::Visualization::SupportTables.new( user.in_database, parent_id: id, parent_kind: kind, public_user_roles: user.db_service.public_user_roles ) end def auto_generate_indices_for_all_layers user_tables = data_layers.map(&:user_tables).flatten.uniq user_tables.each do |ut| ::Resque.enqueue(::Resque::UserDBJobs::UserDBMaintenance::AutoIndexTable, ut.id) end end def set_random_id # This should be done with a DB default self.id ||= random_uuid end def set_default_permission self.permission ||= Carto::Permission.create(owner: user, owner_username: user.username) end def has_private_tables? !related_tables.index { |table| table.private? }.nil? end def sorted_related_table_names mapped_table_names = related_tables.map { |table| "#{user.database_schema}.#{table.name}" } mapped_table_names.sort { |i, j| i <=> j }.join(',') end def get_related_tables return [] unless map map.data_layers.flat_map(&:user_tables).uniq end def get_related_canonical_visualizations get_related_visualizations_by_types([TYPE_CANONICAL]) end def get_related_visualizations_by_types(types) Carto::Visualization.where(map_id: related_tables.map(&:map_id), type: types).all end def has_write_permission?(user) user && !user.viewer? && (owner?(user) || (permission && permission.user_has_write_permission?(user))) end def owner?(user) user_id == user.id end def validate_password_presence errors.add(:password, 'required for protected visualization') if password_protected? && !has_password? end def remove_password_if_unprotected remove_password unless password_protected? end def validate_privacy_changes return unless privacy_changed? && (map? || table?) is_privacy_private? ? validate_change_to_private : validate_change_to_public end def validate_change_to_private if (!user&.private_tables_enabled? && table?) || (!user&.private_maps_enabled? && map?) errors.add(:privacy, 'cannot be set to private') end return unless !privacy_was || privacy_was != Carto::Visualization::PRIVACY_PRIVATE if map? && CartoDB::QuotaChecker.new(user).will_be_over_private_map_quota? errors.add(:privacy, 'over account private map quota') end end def validate_change_to_public return unless !privacy_was || privacy_was == Carto::Visualization::PRIVACY_PRIVATE if map? && CartoDB::QuotaChecker.new(user).will_be_over_public_map_quota? errors.add(:privacy, 'over account public map quota') end if table? && CartoDB::QuotaChecker.new(user).will_be_over_public_dataset_quota? errors.add(:privacy, 'over account public dataset quota') end end def validate_user_not_viewer if user.viewer errors.add(:user, 'cannot be viewer') end end def invalidation_service @invalidation_service ||= Carto::VisualizationInvalidationService.new(self) end class Watcher # watcher:_orgid_:_vis_id_:_user_id_ KEY_FORMAT = "watcher:%s".freeze # @params user Carto::User # @params visualization Carto::Visualization # @throws Carto::Visualization::WatcherError def initialize(user, visualization, notification_ttl = nil) raise WatcherError.new('User must belong to an organization') if user.organization.nil? @user = user @visualization = visualization default_ttl = Cartodb.get_config(:watcher, 'ttl') || 60 @notification_ttl = notification_ttl.nil? ? default_ttl : notification_ttl end # Notifies that is editing the visualization # NOTE: Expiration is handled internally by redis def notify key = KEY_FORMAT % @visualization.id $tables_metadata.multi do $tables_metadata.hset(key, @user.username, current_timestamp + @notification_ttl) $tables_metadata.expire(key, @notification_ttl) end end # Returns a list of usernames currently editing the visualization def list key = KEY_FORMAT % @visualization.id users_expiry = $tables_metadata.hgetall(key) now = current_timestamp users_expiry.select { |_, expiry| expiry.to_i > now }.keys end private def current_timestamp Time.now.getutc.to_i end end class WatcherError < CartoDB::BaseCartoDBError; end class AlreadyLikedError < StandardError; end class UnauthorizedLikeError < StandardError; end end