Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
module AccountCreator
def trigger_account_creation(account_creator)
creation_data = account_creator.enqueue_creation(self)
flash.now[:success] = 'User creation in progress'
# Template variables
@user_creation_id = creation_data[:id]
@user_name = creation_data[:id]
@redirect_url = CartoDB.url(self, 'dashboard')
end
end
@@ -0,0 +1,40 @@
class AccountTokensController < ApplicationController
include LoginHelper
layout 'frontend'
ssl_required :enable, :resend
skip_before_filter :ensure_account_has_been_activated, :only => [ :enable, :resend ]
def enable
# Cleans session information, making sure that Warden authenticate runs the strategy. Check #10489.
cdb_logout
token = params[:id]
user = ::User.where(enable_account_token: token).first
render(file: 'signup/account_already_enabled', status: 404) and return unless user
authenticate!(:enable_account_token, scope: params[:user_domain].present? ? params[:user_domain] : user.username)
@user = user.reload
@organization = @user.organization
@destination_url = CartoDB.url(self, 'dashboard', user: @user)
flash.now[:success] = 'Account enabled, yikes!'
render 'signup/account_enabled'
end
def resend
user_id = params[:user_id]
render_404 and return unless user_id
@user = ::User.where(id: user_id).first
render_404 and return unless @user
@organization = @user.organization
@user.notify_new_organization_user
render 'signup/resend'
end
end
+15
View File
@@ -0,0 +1,15 @@
class Admin::AdminController < ApplicationController
protected
def invalidate_browser_cache
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = 'Mon, 01 Jan 1990 00:00:00 GMT'
end
def valid_password_confirmation
unless current_user.valid_password_confirmation(params[:password_confirmation])
raise Carto::PasswordConfirmationError.new
end
end
end
@@ -0,0 +1,78 @@
require_relative './../helpers/organization_notifications_helper'
class Admin::ClientApplicationsController < Admin::AdminController
include OrganizationNotificationsHelper
ssl_required :oauth, :api_key, :regenerate_api_key, :regenerate_oauth
before_filter :invalidate_browser_cache
before_filter :login_required
before_filter :enforce_engine_enabled, only: :regenerate_api_key
before_filter :load_dashboard_notifications, only: :api_key
before_filter :load_organization_notifications, only: :api_key
layout 'application'
def oauth
respond_to do |format|
format.html { render 'oauth' }
end
end
def api_key
@has_engine_enabled = current_user.engine_enabled?
respond_to do |format|
format.html { render 'api_key' }
end
end
def regenerate_api_key
begin
current_user.regenerate_api_key
rescue Errno::ECONNREFUSED => e
CartoDB::StdoutLogger.info "Could not clear varnish cache", "#{e.inspect}"
if Rails.env.development?
current_user.set_map_key
error_message = "Your API key has been regenerated succesfully but the varnish cache has not been invalidated."
else
raise e
end
rescue CartoDB::CentralCommunicationFailure => e
CartoDB::Logger.warning(exception: e, message: 'Error updating API key in mobile apps')
error_message = "Your API key has been successfully generated, " \
"but there was an error updating the license keys of mobile apps"
rescue => e
raise e
end
flash = if error_message
{ error: error_message }
else
{ success: "Your API key has been regenerated successfully" }
end
redirect_to CartoDB.url(self, 'api_key_credentials', params: { type: 'api_key' }, user: current_user), flash: flash
end
def regenerate_oauth
@client_application = current_user.client_application
return if request.get?
current_user.reset_client_application!
redirect_to CartoDB.url(self, 'oauth_credentials', params: { type: 'oauth' }, user: current_user),
flash: { success: "Your OAuth credentials have been updated successfully" }
end
private
def enforce_engine_enabled
unless current_user.engine_enabled?
render_403
end
end
def load_dashboard_notifications
carto_user = Carto::User.where(id: current_user.id).first if current_user
@dashboard_notifications = carto_user ? carto_user.notifications_for_category(:dashboard) : {}
end
end
@@ -0,0 +1,304 @@
require_dependency 'carto/controller_helper'
require_dependency 'dummy_password_generator'
class Admin::OrganizationUsersController < Admin::AdminController
include OrganizationUsersHelper
include DummyPasswordGenerator
# Organization actions
ssl_required :new, :create, :edit, :update, :destroy
# Data of single users
ssl_required :profile, :account, :oauth, :api_key, :regenerate_api_key
before_filter :get_config
before_filter :login_required, :check_permissions, :load_organization
before_filter :get_user, only: [:edit, :update, :destroy, :regenerate_api_key]
before_filter :ensure_edit_permissions, only: [:edit, :update, :destroy, :regenerate_api_key]
layout 'application'
def new
@user = ::User.new
@user.quota_in_bytes = [@organization.unassigned_quota, @organization.default_quota_in_bytes].min
@user.soft_geocoding_limit = current_user.soft_geocoding_limit
@user.soft_here_isolines_limit = current_user.soft_here_isolines_limit
@user.soft_obs_snapshot_limit = current_user.soft_obs_snapshot_limit
@user.soft_obs_general_limit = current_user.soft_obs_general_limit
@user.soft_twitter_datasource_limit = current_user.soft_twitter_datasource_limit
@user.soft_mapzen_routing_limit = current_user.soft_mapzen_routing_limit
@user.viewer = @organization.remaining_seats <= 0 && @organization.remaining_viewer_seats > 0
respond_to do |format|
format.html { render 'new' }
end
end
def edit
set_flash_flags
respond_to do |format|
format.html { render 'edit' }
end
end
def create
@user = ::User.new
# Validation is done on params to allow checking the change of the value.
# The error is deferred to display values in the form in the error scenario.
validation_failure = !soft_limits_validation(@user, params[:user], @organization.owner)
# set organization first, so some validations related to org users are applied (i.e. strong passwords)
@user.org_admin = params[:user][:org_admin] unless params[:user][:org_admin].nil?
@user.organization = @organization
if !@organization.auth_username_password_enabled &&
!params[:user][:password].present? &&
!params[:user][:password_confirmation].present?
dummy_password = generate_dummy_password
params[:user][:password] = dummy_password
params[:user][:password_confirmation] = dummy_password
end
@user.set_fields(
params[:user],
[
:username, :email, :password, :quota_in_bytes, :password_confirmation,
:twitter_datasource_enabled, :soft_geocoding_limit, :soft_here_isolines_limit,
:soft_obs_snapshot_limit, :soft_obs_general_limit, :soft_mapzen_routing_limit
]
)
@user.viewer = params[:user][:viewer] == 'true'
current_user.copy_account_features(@user)
# Validate password first, so nicer errors are displayed
model_validation_ok = @user.valid_password?(:password,
params[:user][:password],
params[:user][:password_confirmation]) &&
@user.valid_creation?(current_user)
valid_password_confirmation
unless model_validation_ok
raise Sequel::ValidationFailed.new("Validation failed: #{@user.errors.full_messages.join(', ')}")
end
raise Carto::UnprocesableEntityError.new("Soft limits validation error") if validation_failure
@user.save(raise_on_failure: true)
@user.create_in_central
common_data_url = CartoDB::Visualization::CommonDataService.build_url(self)
::Resque.enqueue(::Resque::UserDBJobs::CommonData::LoadCommonData, @user.id, common_data_url)
@user.notify_new_organization_user
@user.organization.notify_if_seat_limit_reached unless @user.viewer?
CartoGearsApi::Events::EventManager.instance.notify(
CartoGearsApi::Events::UserCreationEvent.new(
CartoGearsApi::Events::UserCreationEvent::CREATED_VIA_ORG_ADMIN, @user
)
)
redirect_to CartoDB.url(self, 'organization', user: current_user),
flash: { success: "New user created successfully" }
rescue Carto::UnprocesableEntityError => e
CartoDB::Logger.error(exception: e, message: "Validation error")
set_flash_flags
flash.now[:error] = e.user_message
render 'new', status: 422
rescue CartoDB::CentralCommunicationFailure => e
CartoDB.report_exception(e)
begin
@user.destroy
rescue => ee
CartoDB.report_exception(ee)
end
set_flash_flags
flash.now[:error] = e.user_message
@user = default_user
render 'new'
rescue Carto::PasswordConfirmationError => e
flash.now[:error] = e.message
render action: 'new', status: e.status
rescue Sequel::ValidationFailed => e
flash.now[:error] = e.message
render 'new'
end
def update
valid_password_confirmation
session[:show_dashboard_details_flash] = params[:show_dashboard_details_flash].present?
session[:show_account_settings_flash] = params[:show_account_settings_flash].present?
# Validation is done on params to allow checking the change of the value.
# The error is deferred to display values in the form in the error scenario.
validation_failure = !soft_limits_validation(@user, params[:user])
attributes = params[:user]
@user.set_fields(attributes, [:email]) if attributes[:email].present? && !@user.google_sign_in
@user.set_fields(attributes, [:quota_in_bytes]) if attributes[:quota_in_bytes].present?
@user.set_fields(attributes, [:disqus_shortname]) if attributes[:disqus_shortname].present?
@user.set_fields(attributes, [:available_for_hire]) if attributes[:available_for_hire].present?
@user.set_fields(attributes, [:name]) if attributes[:name].present?
@user.set_fields(attributes, [:website]) if attributes[:website].present?
@user.set_fields(attributes, [:description]) if attributes[:description].present?
@user.set_fields(attributes, [:twitter_username]) if attributes[:twitter_username].present?
@user.set_fields(attributes, [:location]) if attributes[:location].present?
@user.set_fields(attributes, [:org_admin]) if attributes[:org_admin].present?
@user.viewer = attributes[:viewer] == 'true'
@user.password = attributes[:password] if attributes[:password].present?
@user.password_confirmation = attributes[:password_confirmation] if attributes[:password_confirmation].present?
@user.soft_geocoding_limit = attributes[:soft_geocoding_limit] if attributes[:soft_geocoding_limit].present?
@user.soft_here_isolines_limit = attributes[:soft_here_isolines_limit] if attributes[:soft_here_isolines_limit].present?
@user.soft_obs_snapshot_limit = attributes[:soft_obs_snapshot_limit] if attributes[:soft_obs_snapshot_limit].present?
@user.soft_obs_general_limit = attributes[:soft_obs_general_limit] if attributes[:soft_obs_general_limit].present?
@user.twitter_datasource_enabled = attributes[:twitter_datasource_enabled] if attributes[:twitter_datasource_enabled].present?
@user.soft_twitter_datasource_limit = attributes[:soft_twitter_datasource_limit] if attributes[:soft_twitter_datasource_limit].present?
@user.soft_mapzen_routing_limit = attributes[:soft_mapzen_routing_limit] if attributes[:soft_mapzen_routing_limit].present?
model_validation_ok = @user.valid_update?(current_user)
if attributes[:password].present? || attributes[:password_confirmation].present?
model_validation_ok &&= @user.valid_password?(:password, attributes[:password], attributes[:password_confirmation])
end
unless model_validation_ok
raise Sequel::ValidationFailed.new("Validation failed: #{@user.errors.full_messages.join(', ')}")
end
raise Carto::UnprocesableEntityError.new("Soft limits validation error") if validation_failure
ActiveRecord::Base.transaction do
if attributes[:mfa].present?
service = Carto::UserMultifactorAuthUpdateService.new(user_id: @user.id)
service.update(enabled: attributes[:mfa] == '1')
end
# update_in_central is duplicated because we don't wan ta local save if Central fails,
# but before/after save at user can change some attributes that we also want to persist.
# Since those callbacks aren't idempotent there's no much better solution without a big refactor.
@user.update_in_central
@user.save(raise_on_failure: true)
@user.update_in_central
end
redirect_to CartoDB.url(self, 'edit_organization_user', params: { id: @user.username }, user: current_user),
flash: { success: "Your changes have been saved correctly." }
rescue Carto::UnprocesableEntityError => e
CartoDB::Logger.error(exception: e, message: "Validation error")
set_flash_flags
flash.now[:error] = e.user_message
render 'edit', status: 422
rescue CartoDB::CentralCommunicationFailure => e
set_flash_flags
flash.now[:error] = "There was a problem while updating this user. Please, try again and contact us if the problem persists. #{e.user_message}"
render 'edit'
rescue Carto::PasswordConfirmationError => e
flash.now[:error] = e.message
render action: 'edit', status: e.status
rescue Sequel::ValidationFailed, ActiveRecord::RecordInvalid => e
flash.now[:error] = e.message
render 'edit', status: 422
end
def destroy
valid_password_confirmation
raise "Can't delete user. Has shared entities" if @user.has_shared_entities?
@user.destroy
@user.delete_in_central
flash[:success] = "User was successfully deleted."
redirect_to CartoDB.url(self, 'organization', user: current_user)
rescue CartoDB::CentralCommunicationFailure => e
if e.user_message =~ /No organization user found with username/
flash[:success] = "User was successfully deleted."
redirect_to CartoDB.url(self, 'organization', user: current_user)
else
CartoDB::Logger.error(exception: e, message: 'Error deleting organizational user from central', target_user: @user.username)
flash[:success] = "#{e.user_message}. User was deleted from the organization server."
redirect_to organization_path(user_domain: params[:user_domain])
end
rescue Carto::PasswordConfirmationError => e
flash[:error] = e.message
redirect_to organization_path(user_domain: params[:user_domain])
rescue => e
CartoDB::Logger.error(exception: e, message: 'Error deleting organizational user', target_user: @user.username)
flash[:error] = "User was not deleted. #{e.message}"
redirect_to organization_path(user_domain: params[:user_domain])
end
def regenerate_api_key
valid_password_confirmation
@user.regenerate_all_api_keys
flash[:success] = "User API key regenerated successfully"
redirect_to CartoDB.url(self, 'edit_organization_user', params: { id: @user.username }, user: current_user),
flash: { success: "Your changes have been saved correctly." }
rescue Carto::PasswordConfirmationError => e
flash[:error] = e.message
render action: 'edit', status: e.status
rescue => e
CartoDB.notify_exception(e, { user_id: @user.id, current_user: current_user.id })
flash[:error] = "There was an error regenerating the API key. Please, try again and contact us if the problem persists"
render 'edit'
end
private
def default_user
::User.new(username: @user.username, email: @user.email, quota_in_bytes: @user.quota_in_bytes, twitter_datasource_enabled: @user.twitter_datasource_enabled)
end
def extras_enabled?
extra_geocodings_enabled? || extra_here_isolines_enabled? || extra_obs_snapshot_enabled? || extra_obs_general_enabled? || extra_tweets_enabled?
end
def extra_geocodings_enabled?
!Cartodb.get_config(:geocoder, 'app_id').blank?
end
def extra_here_isolines_enabled?
true
end
def extra_obs_snapshot_enabled?
true
end
def extra_obs_general_enabled?
true
end
def extra_tweets_enabled?
!Cartodb.get_config(:datasource_search, 'twitter_search', 'standard', 'username').blank?
end
def set_flash_flags(show_dashboard_details_flash = nil, show_account_settings_flash = nil)
@show_dashboard_details_flash = session[:show_dashboard_details_flash] || show_dashboard_details_flash
@show_account_settings_flash = session[:show_account_settings_flash] || show_account_settings_flash
session[:show_dashboard_details_flash] = nil
session[:show_account_settings_flash] = nil
end
def get_config
@extras_enabled = extras_enabled?
@extra_geocodings_enabled = extra_geocodings_enabled?
@extra_tweets_enabled = extra_tweets_enabled?
end
def check_permissions
raise RecordNotFound unless current_user.organization_admin?
end
def get_user
@user = @organization.users_dataset.where(username: params[:id]).first
raise RecordNotFound unless @user
end
def load_organization
@organization = current_user.organization
end
def ensure_edit_permissions
render_403 unless @user.editable_by?(current_user)
end
end
@@ -0,0 +1,227 @@
require_relative './../helpers/avatar_helper'
require_relative './../helpers/organization_notifications_helper'
class Admin::OrganizationsController < Admin::AdminController
include AvatarHelper
include OrganizationNotificationsHelper
ssl_required :show, :settings, :settings_update, :regenerate_all_api_keys, :groups, :auth, :auth_update,
:notifications, :new_notification, :destroy_notification, :destroy
before_filter :login_required, :load_organization_and_members, :load_ldap_configuration
before_filter :owners_only, only: [:settings, :settings_update, :regenerate_all_api_keys, :auth, :auth_update,
:destroy]
before_filter :enforce_engine_enabled, only: :regenerate_all_api_keys
before_filter :load_carto_organization, only: [:notifications, :new_notification]
before_filter :load_notification, only: [:destroy_notification]
before_filter :load_organization_notifications, only: [:settings, :auth, :show, :groups, :notifications,
:new_notification]
helper_method :show_billing
layout 'application'
def show
respond_to do |format|
format.html { render 'show' }
end
end
def destroy
deletion_password_confirmation = params[:deletion_password_confirmation]
if current_user.needs_password_confirmation? && !current_user.validate_old_password(deletion_password_confirmation)
flash.now[:error] = "Password doesn't match"
render 'show', status: 400
else
@organization.destroy_cascade(delete_in_central: true)
redirect_to logout_url
end
rescue => e
CartoDB::Logger.error(message: "Error deleting organization", exception: e, organization: @organization)
flash.now[:error] = "Error deleting organization: #{e.message}"
render 'show', status: 500
end
def settings
@avatar_valid_extensions = AVATAR_VALID_EXTENSIONS
respond_to do |format|
format.html { render 'settings' }
end
end
def groups
respond_to do |format|
format.html { render 'groups' }
end
end
def notifications(status = 200)
@notification ||= Carto::Notification.new(recipients: Carto::Notification::RECIPIENT_ALL)
@notifications = @carto_organization.notifications.limit(12).map { |n| Carto::Api::NotificationPresenter.new(n) }
respond_to do |format|
format.html { render 'notifications', status: status }
end
end
def new_notification
valid_password_confirmation
carto_organization = Carto::Organization.find(@organization.id)
attributes = {
body: params[:carto_notification]['body'],
icon: Carto::Notification::ICON_ALERT,
recipients: params[:carto_notification]['recipients']
}
@notification = carto_organization.notifications.build(attributes)
if @notification.save
redirect_to CartoDB.url(self, 'organization_notifications_admin', user: current_user),
flash: { success: 'Notification sent!' }
else
flash.now[:error] = @notification.errors.full_messages.join(', ')
notifications
end
rescue Carto::PasswordConfirmationError => e
flash.now[:error] = e.message
notifications(e.status)
end
def destroy_notification
@notification.destroy
redirect_to CartoDB.url(self, 'organization_notifications_admin', user: current_user),
flash: { success: 'Notification was successfully deleted!' }
end
def settings_update
valid_password_confirmation
attributes = params[:organization]
if attributes.include?(:avatar_url) && valid_avatar_file?(attributes[:avatar_url])
@organization.avatar_url = attributes[:avatar_url]
end
@organization.website = attributes[:website]
@organization.admin_email = attributes[:admin_email]
@organization.description = attributes[:description]
@organization.display_name = attributes[:display_name]
@organization.color = attributes[:color]
if attributes.include?(:default_quota_in_bytes)
default_quota_in_bytes = attributes[:default_quota_in_bytes]
@organization.default_quota_in_bytes = default_quota_in_bytes.blank? ? nil : default_quota_in_bytes.to_i * 1024 * 1024
end
@organization.discus_shortname = attributes[:discus_shortname]
@organization.twitter_username = attributes[:twitter_username]
@organization.location = attributes[:location]
@organization.update_in_central
@organization.save(raise_on_failure: true)
redirect_to CartoDB.url(self, 'organization_settings', user: current_user),
flash: { success: "Your changes have been saved correctly." }
rescue CartoDB::CentralCommunicationFailure => e
@organization.reload
flash.now[:error] = "There was a problem while updating your organization. Please, try again and contact us if the problem persists. #{e.user_message}"
render action: 'settings'
rescue Carto::PasswordConfirmationError => e
flash.now[:error] = e.message
render action: 'settings', status: e.status
rescue Sequel::ValidationFailed => e
flash.now[:error] = "There's been a validation error, check your values"
render action: 'settings'
end
def regenerate_all_api_keys
valid_password_confirmation
@organization.users.each(&:regenerate_all_api_keys)
redirect_to CartoDB.url(self, 'organization_settings', user: current_user),
flash: { success: "Users API keys regenerated successfully" }
rescue Carto::PasswordConfirmationError => e
flash.now[:error] = e.message
render action: 'settings', status: e.status
rescue => e
CartoDB.notify_exception(e, { organization: @organization.id, current_user: current_user.id })
flash[:error] = "There was an error regenerating the API keys. Please, try again and contact us if the problem persists"
render action: 'settings'
end
def auth
respond_to do |format|
format.html { render 'auth' }
end
end
def auth_update
valid_password_confirmation
attributes = params[:organization]
@organization.whitelisted_email_domains = attributes[:whitelisted_email_domains].split(",")
@organization.auth_username_password_enabled = attributes[:auth_username_password_enabled]
@organization.auth_google_enabled = attributes[:auth_google_enabled]
@organization.auth_github_enabled = attributes[:auth_github_enabled]
@organization.strong_passwords_enabled = attributes[:strong_passwords_enabled]
@organization.password_expiration_in_d = attributes[:password_expiration_in_d]
@organization.update_in_central
@organization.save(raise_on_failure: true)
redirect_to CartoDB.url(self, 'organization_auth', user: current_user),
flash: { success: "Your changes have been saved correctly." }
rescue CartoDB::CentralCommunicationFailure => e
@organization.reload
flash.now[:error] = "There was a problem while updating your organization. Please, try again and contact us if the problem persists. #{e.user_message}"
render action: 'auth'
rescue Carto::PasswordConfirmationError => e
flash.now[:error] = e.message
render action: 'auth', status: e.status
rescue Sequel::ValidationFailed => e
flash.now[:error] = "There's been a validation error, check your values"
render action: 'auth'
end
private
def load_organization_and_members
raise RecordNotFound unless current_user.organization_admin?
@organization = current_user.organization
display_signup_warnings if @organization.signup_page_enabled
# INFO: Special scenario of handcrafted URL to go to organization-based signup page
@organization_signup_url =
"#{CartoDB.protocol}://#{@organization.name}.#{CartoDB.account_host}#{CartoDB.path(self, 'signup_organization_user')}"
end
def owners_only
raise RecordNotFound unless current_user.organization_owner?
end
def display_signup_warnings
warning = []
warning << "Your organization has run out of quota" unless @organization.valid_disk_quota?
warning << "Your organization has run out of seats" unless @organization.valid_builder_seats?
unless warning.empty?
flash.now[:warning] = "#{warning.join('. ')}."
flash.now[:warning_detail] = "Users won't be able to sign up to your organization. <a href='mailto:contact@carto.com'>Contact us</a> to increase your quota."
end
end
def show_billing
!Cartodb.config[:cartodb_com_hosted].present? && (!current_user.organization.present? || current_user.organization_owner?)
end
def load_ldap_configuration
@ldap_configuration = Carto::Ldap::Configuration.where(organization_id: @organization.id).first
end
def enforce_engine_enabled
unless @organization.engine_enabled
render_403
end
end
def load_carto_organization
@carto_organization = Carto::Organization.find(@organization.id)
end
def load_notification
@notification = Carto::Notification.find(params[:id])
end
end
+505
View File
@@ -0,0 +1,505 @@
require 'active_support/inflector'
require 'carto/api/vizjson3_presenter'
require_relative '../../models/table'
require_relative '../../models/visualization/member'
require_relative '../../models/visualization/collection'
class Admin::PagesController < Admin::AdminController
include Carto::HtmlSafe
include CartoDB
include VisualizationsControllerHelper
DATASETS_PER_PAGE = 9
MAPS_PER_PAGE = 9
USER_TAGS_LIMIT = 100
PAGE_NUMBER_PLACEHOLDER = 'PAGENUMBERPLACEHOLDER'
# TODO logic as done client-side, how and where to encapsulate this better?
GEOMETRY_MAPPING = {
'st_multipolygon' => 'polygon',
'st_polygon' => 'polygon',
'st_multilinestring' => 'line',
'st_linestring' => 'line',
'st_multipoint' => 'point',
'st_point' => 'point'
}
ssl_required :common_data, :public, :datasets, :maps, :user_feed
ssl_allowed :index, :sitemap, :datasets_for_user, :datasets_for_organization, :maps_for_user, :maps_for_organization,
:render_not_found
before_filter :login_required, :except => [:public, :datasets, :maps, :sitemap, :index, :user_feed]
before_filter :load_viewed_entity
before_filter :set_new_dashboard_flag
before_filter :ensure_organization_correct
skip_before_filter :browser_is_html5_compliant?, only: [:public, :datasets, :maps, :user_feed]
skip_before_filter :ensure_user_organization_valid, only: [:public]
helper_method :named_map_vizjson3
# Just an entrypoint to dispatch to different places according to
def index
if current_user
# I am logged in, visiting my subdomain -> my dashboard
redirect_to CartoDB.url(self, 'dashboard', user: current_user)
elsif CartoDB.extract_subdomain(request).present?
# I am visiting another user subdomain -> other user public pages
redirect_to CartoDB.url(self, 'public_user_feed_home')
elsif current_viewer
# I am logged in but did not specify a subdomain -> my dashboard
redirect_to CartoDB.url(self, 'dashboard', user: current_viewer)
else
# I am not logged in and did not specify a subdomain -> login
# Avoid using CartoDB.url helper, since we cannot get any user information from domain, path or session
redirect_to login_url
end
end
def common_data
redirect_to CartoDB.url(self, 'datasets_library')
end
def sitemap
if @viewed_user.nil?
username = CartoDB.extract_subdomain(request)
org = get_organization_if_exists(username)
render_404 and return if org.nil?
visualizations = public_builder(organization_id: org.id).build
else
# Redirect to org url if has only user
if eligible_for_redirect?(@viewed_user)
redirect_to CartoDB.base_url(@viewed_user.organization.name) << CartoDB.path(self, 'public_sitemap') and return
end
visualizations = public_builder(user_id: @viewed_user.id).with_prefetch_user(true).build
end
@urls = visualizations.map { |vis|
case vis.type
when Carto::Visualization::TYPE_DERIVED
{
loc: CartoDB.url(self, 'public_visualizations_public_map', params: { id: vis.id }, user: vis.user),
lastfreq: vis.updated_at.strftime("%Y-%m-%dT%H:%M:%S%:z")
}
when Carto::Visualization::TYPE_CANONICAL
{
loc: CartoDB.url(self, 'public_table', params: { id: vis.name }, user: vis.user),
lastfreq: vis.updated_at.strftime("%Y-%m-%dT%H:%M:%S%:z")
}
end
}.compact
render :formats => [:xml]
end
def datasets
datasets = CartoDB::ControllerFlows::Public::Datasets.new(self)
content = CartoDB::ControllerFlows::Public::Content.new(self, request, datasets)
content.render()
end
def maps
maps = CartoDB::ControllerFlows::Public::Maps.new(self)
content = CartoDB::ControllerFlows::Public::Content.new(self, request, maps)
content.render()
end
def public
if current_user
index
else
user_feed
end
end
def user_feed
# The template of this endpoint get the user_feed data calling
# to another endpoint in the front-end part
if @viewed_user.nil?
username = CartoDB.extract_subdomain(request).strip.downcase
org = get_organization_if_exists(username)
unless org.nil?
redirect_to CartoDB.url(self, 'public_maps_home') and return
end
render_404
else
set_layout_vars_for_user(@viewed_user, 'feed')
dataset_builder = user_datasets_public_builder(@viewed_user)
maps_builder = user_maps_public_builder(@viewed_user)
@name = @viewed_user.name_or_username
@avatar_url = @viewed_user.avatar
@tables_num = dataset_builder.build.count
@maps_count = maps_builder.build.count
@website = website_url(@viewed_user.website)
@website_clean = @website ? @website.gsub(/https?:\/\//, "") : ""
if eligible_for_redirect?(@viewed_user)
# redirect username.host.ext => org-name.host.ext/u/username
redirect_to CartoDB.base_url(@viewed_user.organization.name, @viewed_user.username) <<
CartoDB.path(self, 'public_user_feed_home') and return
end
description = @name.dup
# TODO: move to helper
if @maps_count == 0 && @tables_num == 0
description << " uses CARTO to transform location intelligence into dynamic renderings that enable discovery of trends and patterns"
else
description << " has"
unless @maps_count == 0
description << " created #{@maps_count} #{'map'.pluralize(@maps_count)}"
end
unless @maps_count == 0 || @tables_num == 0
description << " and"
end
unless @tables_num == 0
description << " published #{@tables_num} public #{'dataset'.pluralize(@tables_num)}"
end
description << " · View #{@name} CARTO profile for the latest activity and contribute to Open Data by creating an account in CARTO"
end
@page_description = description
respond_to do |format|
format.html { render 'user_feed', layout: 'public_user_feed' }
end
end
end
def datasets_for_user(user)
set_layout_vars_for_user(user, 'datasets')
render_datasets(user_datasets_public_builder(user), user)
end
def datasets_for_organization(org)
set_layout_vars_for_organization(org, 'datasets')
render_datasets(org_datasets_public_builder(org))
end
def maps_for_user(user)
set_layout_vars_for_user(user, 'maps')
render_maps(user_maps_public_builder(user), user)
end
def maps_for_organization(org)
set_layout_vars_for_organization(org, 'maps')
render_maps(org_maps_public_builder(org))
end
def render_not_found
render_404
end
protected
def eligible_for_redirect?(user)
return false if CartoDB.subdomainless_urls?
user.has_organization? && CartoDB.subdomain_from_request(request) != user.organization.name
end
def render_datasets(vis_query_builder, user = nil)
home = CartoDB.url(self, 'public_datasets_home', params: { page: PAGE_NUMBER_PLACEHOLDER }, user: user)
set_pagination_vars(total_count: vis_query_builder.build.count,
per_page: DATASETS_PER_PAGE,
first_page_url: CartoDB.url(self, 'public_datasets_home', user: user),
numbered_page_url: home)
@datasets = []
vis_list = vis_query_builder.build_paged(current_page, DATASETS_PER_PAGE).map do |v|
Carto::Admin::VisualizationPublicMapAdapter.new(v, current_user, self)
end
vis_list.each do |vis|
@datasets << process_dataset_render(vis)
end
@datasets.compact!
description = @name.dup
# TODO: move to helper
if @datasets.size == 0
description << " uses CARTO to transform location intelligence into dynamic renderings that enable discovery of trends and patterns"
else
description << " has published #{@datasets.size} public #{'dataset'.pluralize(@datasets.size)}"
end
description << " · View #{@name} CARTO profile for the latest activity and contribute to Open Data by creating an account in CARTO"
@page_description = description
respond_to do |format|
format.html { render 'public_datasets', layout: 'public_dashboard' }
end
end
def render_maps(vis_query_builder, user=nil)
set_pagination_vars(
total_count: vis_query_builder.build.count,
per_page: MAPS_PER_PAGE,
first_page_url: CartoDB.url(self, 'public_maps_home', user: user),
numbered_page_url: CartoDB.url(self, 'public_maps_home', params: { page: PAGE_NUMBER_PLACEHOLDER }, user: user)
)
vis_list = vis_query_builder.build_paged(current_page, MAPS_PER_PAGE).map do |v|
Carto::Admin::VisualizationPublicMapAdapter.new(v, current_user, self)
end
@visualizations = []
vis_list.each do |vis|
@visualizations << process_map_render(vis)
end
@visualizations.compact!
description = @name.dup
# TODO: move to helper
if @visualizations.size == 0 && @tables_num == 0
description << " uses CARTO to transform location intelligence into dynamic renderings that enable discovery of trends and patterns"
else
description << " has"
unless @visualizations.size == 0
description << " created #{@visualizations.size} #{'map'.pluralize(@visualizations.size)}"
end
unless @visualizations.size == 0 || @tables_num == 0
description << " and"
end
unless @tables_num == 0
description << " published #{@tables_num} public #{'dataset'.pluralize(@tables_num)}"
end
description << " · View #{@name} CARTO profile for the latest activity and contribute to Open Data by creating an account in CARTO"
end
@page_description = description
respond_to do |format|
format.html { render 'public_maps', layout: 'public_dashboard' }
end
end
def set_new_dashboard_flag
ff_user = @viewed_user || @viewed_org.try(:owner)
unless ff_user.nil?
@has_new_dashboard = ff_user.builder_enabled?
end
end
def set_layout_vars_for_user(user, content_type)
builder = user_maps_public_builder(user, visualization_version)
most_viewed = builder.with_order(:mapviews, :desc).build_paged(1, 1).first
set_layout_vars({
most_viewed_vis_map: most_viewed ? Carto::Admin::VisualizationPublicMapAdapter.new(most_viewed, current_user, self) : nil,
content_type: content_type,
default_fallback_basemap: user.default_basemap,
user: user,
base_url: user.public_url(nil, request.protocol == "https://" ? "https" : "http")
})
set_shared_layout_vars(user, {
name: user.name_or_username,
avatar_url: user.avatar,
}, {
available_for_hire: user.available_for_hire,
email: user.email,
user: user
})
end
def set_layout_vars_for_organization(org, content_type)
most_viewed_vis_map = org.public_vis_by_type(Carto::Visualization::TYPE_DERIVED,
1,
1,
nil,
'mapviews',
visualization_version).first
set_layout_vars(most_viewed_vis_map: most_viewed_vis_map,
content_type: content_type,
default_fallback_basemap: org.owner ? org.owner.default_basemap : nil,
base_url: '')
set_shared_layout_vars(org,
name: org.display_name.blank? ? org.name : org.display_name,
avatar_url: org.avatar_url)
end
def set_layout_vars(required)
@most_viewed_vis_map = required.fetch(:most_viewed_vis_map)
@content_type = required.fetch(:content_type)
@maps_url = CartoDB.url(view_context, 'public_maps_home', user: required.fetch(:user, nil))
@datasets_url = CartoDB.url(view_context, 'public_datasets_home', user: required.fetch(:user, nil))
@default_fallback_basemap = required.fetch(:default_fallback_basemap, {})
@base_url = required.fetch(:base_url, {})
end
def set_pagination_vars(required)
# Force all number pagination vars to be integers avoiding problems with
# undesired strings
@total_count = required.fetch(:total_count, 0).to_i
@per_page = required.fetch(:per_page, 9).to_i
@current_page = current_page.to_i
@first_page_url = required.fetch(:first_page_url)
@numbered_page_url = required.fetch(:numbered_page_url)
@page_number_placeholder = PAGE_NUMBER_PLACEHOLDER
end
# Shared as in shared for both new and old layout
def set_shared_layout_vars(model, required, optional = {})
@twitter_username = model.twitter_username
@location = model.location
@description = model.description
@website = website_url(model.website)
@website_clean = @website ? @website.gsub(/https?:\/\//, "") : ""
@name = required.fetch(:name)
@avatar_url = required.fetch(:avatar_url)
@email = optional.fetch(:email, nil)
@available_for_hire = optional.fetch(:available_for_hire, false)
@user = optional.fetch(:user, nil)
@is_org = model.is_a? Organization
@tables_num = (@is_org ? org_datasets_public_builder(model) : user_datasets_public_builder(model)).build.count
@maps_count = (@is_org ? org_maps_public_builder(model) : user_maps_public_builder(model)).build.count
@needs_gmaps_lib = @most_viewed_vis_map.try(:map).try(:provider) == 'googlemaps'
@needs_gmaps_lib ||= @default_fallback_basemap['className'] == 'googlemaps'
gmaps_user = @most_viewed_vis_map.try(:user) || @viewed_user
@gmaps_query_string = gmaps_user ? gmaps_user.google_maps_query_string : @viewed_org.google_maps_key
end
def user_datasets_public_builder(user)
public_builder(user_id: user.id, vis_type: Carto::Visualization::TYPE_CANONICAL)
end
def user_maps_public_builder(user, version = nil)
public_builder(user_id: user.id, vis_type: Carto::Visualization::TYPE_DERIVED, version: version)
end
def org_datasets_public_builder(org)
public_builder(vis_type: Carto::Visualization::TYPE_CANONICAL, organization_id: org.id)
end
def org_maps_public_builder(org)
public_builder(vis_type: Carto::Visualization::TYPE_DERIVED, organization_id: org.id)
end
def public_builder(user_id: nil, vis_type: nil, organization_id: nil, version: nil)
tags = tag_or_nil.nil? ? nil : [tag_or_nil]
builder = Carto::VisualizationQueryBuilder.new
.with_privacy(Carto::Visualization::PRIVACY_PUBLIC)
.with_published
.without_raster
.with_order(:updated_at, :desc)
.with_user_id(user_id)
.with_type(vis_type)
.with_tags(tags)
.with_organization_id(organization_id)
.with_version(version)
builder
end
def visualization_version
@has_new_dashboard ? Carto::Visualization::VERSION_BUILDER : nil
end
def named_map_vizjson3(visualization)
generate_named_map_vizjson3(Carto::Visualization.find(visualization.id))
end
def get_organization_if_exists(name)
Organization.where(name: name).first
end
def current_page
params[:page].to_i > 0 ? params[:page] : 1
end
def tag_or_nil
params[:tag]
end
def ensure_organization_correct
return if CartoDB.subdomainless_urls?
user_or_org_domain = CartoDB.subdomain_from_request(request)
user_domain = CartoDB.extract_subdomain(request)
user = ::User.where(username: user_domain).first
unless user.nil?
if user.username != user_or_org_domain and not user.belongs_to_organization?(get_organization_if_exists(user_or_org_domain))
render_404
end
end
end
def process_dataset_render(dataset)
geometry_type = dataset.kind
if geometry_type != 'raster'
table_geometry_types = dataset.table.geometry_types
geometry_type = GEOMETRY_MAPPING.fetch(table_geometry_types.first.try(&:downcase), '')
end
vis_item(dataset).merge(
rows_count: dataset.table.rows_counted,
size_in_bytes: dataset.table.table_size,
geometry_type: geometry_type,
source: markdown_html_safe(dataset.source)
)
rescue StandardError => e
# A dataset might be invalid. For example, having the table deleted and not yet cleaned.
# We don't want public page to be broken, but error must be traced.
CartoDB.notify_exception(e, vis: dataset)
nil
end
def process_map_render(map)
vis_item(map)
end
def vis_item(vis)
return {
id: vis.id,
title: vis.name,
description: markdown_html_safe(vis.description),
tags: vis.tags,
updated_at: vis.updated_at,
owner: vis.user,
map_zoom: vis.map.zoom
}
end
def load_viewed_entity
username = CartoDB.extract_subdomain(request)
@viewed_user = ::User.where(username: username).first
if @viewed_user.nil?
username = username.strip.downcase
@viewed_org = get_organization_if_exists(username)
end
end
def website_url(url)
if url.blank?
""
else
!url.blank? && url[/^https?:\/\//].nil? ? "http://#{url}" : url
end
end
end
@@ -0,0 +1,63 @@
require_relative '../../models/map/presenter'
class Admin::TablesController < Admin::AdminController
ssl_required :index, :show, :public
skip_before_filter :browser_is_html5_compliant?, :only => [:embed_map]
before_filter :login_required, :only => [:index]
after_filter :update_user_last_activity, only: [:index, :show]
def index
end
# We only require login for index, so we must manage the security at this level.
# we present different actions depending on if there is a user logged in or not.
# if the user is not logged in, we redirect them to the public page
def show
if current_user.present?
@table = ::Table.get_by_id(params[:id], current_user)
respond_to do |format|
format.html
download_formats @table, format
end
else
redirect_to CartoDB.path(self, 'public_table', { id: params[:id], :format => params[:format] })
end
end
def public
@table = nil
@subdomain = CartoDB.extract_subdomain(request)
@table = ::Table.get_by_id(params[:id], ::User.find(:username => @subdomain))
# Has quite strange checks to see if a user can access a public table
if @table.blank? || @table.private? || ((current_user && current_user.id != @table.user_id) && @table.private?)
render_403
else
@vizjson = CartoDB::Map::Presenter.new(
@table.map,
{ full: true },
Cartodb.config
)
respond_to do |format|
format.html { render 'public', layout: 'application_table_public' }
download_formats @table, format
end
end
end
private
def download_formats table, format
format.sql { send_data table.to_sql, send_data_conf(table, 'zip', 'zip') }
format.kml { send_data table.to_kml, send_data_conf(table, 'zip', 'kmz') }
format.csv { send_data table.to_csv, send_data_conf(table, 'zip', 'zip') }
format.shp { send_data table.to_shp, send_data_conf(table, 'octet-stream', 'zip') }
end
def send_data_conf table, type, ext
{ :type => "application/#{type}; charset=binary; header=present",
:disposition => "attachment; filename=#{table.name}.#{ext}" }
end
end
+43
View File
@@ -0,0 +1,43 @@
class Admin::UsersController < Admin::AdminController
include LoginHelper
ssl_required :account, :profile, :lockout, :maintenance
before_filter :invalidate_browser_cache
before_filter :login_required
before_filter :setup_user
layout 'application'
def profile
render(file: "public/static/profile/index.html", layout: false)
end
def account
render(file: "public/static/account/index.html", layout: false)
end
def lockout
if current_user.locked?
@expiration_days = @user.remaining_days_deletion
@payments_url = @user.plan_url(request.protocol)
render locals: { breadcrumb: false }
else
render_404
end
end
def maintenance
if current_user.maintenance_mode?
render locals: { breadcrumb: false }
else
render_404
end
end
private
def setup_user
@user = current_user
end
end
@@ -0,0 +1,680 @@
require_relative '../../models/map/presenter'
require_relative '../carto/admin/user_table_public_map_adapter'
require_relative '../carto/admin/visualization_public_map_adapter'
require_relative '../carto/api/visualization_presenter'
require_relative '../carto/api/received_notification_presenter'
require_relative '../../helpers/embed_redis_cache'
require_dependency 'carto/tracking/events'
require_dependency 'resque/user_jobs'
require_dependency 'static_maps_url_helper'
require_dependency 'carto/helpers/frame_options_helper'
require_dependency 'carto/visualization'
class Admin::VisualizationsController < Admin::AdminController
include CartoDB, VisualizationsControllerHelper
include Carto::FrameOptionsHelper
MAX_MORE_VISUALIZATIONS = 3
DEFAULT_PLACEHOLDER_CHARS = 4
ssl_allowed :embed_map, :public_map, :show_protected_embed_map, :public_table,
:show_organization_public_map, :show_organization_embed_map,
:embed_protected, :public_map_protected, :embed_forbidden, :track_embed
ssl_required :index, :show, :protected_public_map, :show_protected_public_map
before_filter :x_frame_options_allow, only: [:embed_forbidden, :embed_map, :embed_protected,
:show_organization_embed_map, :show_protected_embed_map,
:track_embed]
before_filter :login_required, only: [:index]
before_filter :table_and_schema_from_params, only: [:show, :public_table, :public_map, :show_protected_public_map,
:show_protected_embed_map, :embed_map]
before_filter :get_viewed_user, only: [:public_map, :public_table, :show_protected_public_map, :show_organization_public_map, :public_map_protected, :embed_map, :embed_protected]
before_filter :resolve_visualization_and_table,
:ensure_visualization_viewable,
only: [:show, :public_table, :public_map,
:show_organization_public_map, :show_organization_embed_map,
:show_protected_public_map, :show_protected_embed_map]
before_filter :resolve_visualization_and_table_if_not_cached, only: [:embed_map]
before_filter :redirect_to_builder_embed_if_v3, only: [:embed_map, :show_organization_public_map,
:show_organization_embed_map, :show_protected_public_map,
:show_protected_embed_map,
:public_map, :show_protected_public_map]
after_filter :update_user_last_activity, only: [:show]
skip_before_filter :browser_is_html5_compliant?, only: [:public_map, :embed_map, :track_embed,
:show_protected_embed_map, :show_protected_public_map]
skip_before_filter :verify_authenticity_token, only: [:show_protected_public_map, :show_protected_embed_map]
def index
render(file: "public/static/dashboard/index.html", layout: false)
end
def show
table_action = request.original_fullpath =~ %r{/tables/}
unless current_user.present?
if table_action
return(redirect_to CartoDB.url(self, 'public_table_map', params: { id: request.params[:id] }))
else
return(redirect_to CartoDB.url(self, 'public_visualizations_public_map', params: { id: request.params[:id] }))
end
end
@google_maps_query_string = @visualization.user.google_maps_query_string
@basemaps = @visualization.user.basemaps
if table_action
if current_user.builder_enabled? && @visualization.has_read_permission?(current_user)
return redirect_to CartoDB.url(self, 'builder_dataset', params: { id: request.params[:id] }, user: current_user)
elsif !@visualization.has_write_permission?(current_user)
return redirect_to CartoDB.url(self, 'public_table_map', params: { id: request.params[:id], redirected: true })
end
elsif current_user.builder_enabled? && !@visualization.open_in_editor?
return redirect_to CartoDB.url(self, 'builder_visualization', params: { id: request.params[:id] },
user: current_user)
elsif current_user.has_feature_flag?('static_editor') && !current_user.builder_enabled?
return render(file: 'public/static/show/index.html', layout: false)
elsif !@visualization.has_write_permission?(current_user)
return redirect_to CartoDB.url(self, 'public_visualizations_public_map',
params: { id: request.params[:id], redirected: true })
end
if @visualization.is_privacy_private? && @visualization.has_read_permission?(current_user)
@auth_tokens = current_user.get_auth_tokens
end
respond_to { |format| format.html }
end
def public_table
return(render_pretty_404) if @visualization.private?
get_viewed_user
ff_user = @viewed_user || @org.try(:owner)
if @visualization.derived?
if current_user.nil? || current_user.username != request.params[:user_domain]
destination_user = ::User.where(username: request.params[:user_domain]).first
else
destination_user = nil
end
return(redirect_to CartoDB.url(self, 'public_visualizations_public_map', params: { id: request.params[:id] },
user: destination_user))
end
if current_user.nil? && !request.params[:redirected].present?
redirect_url = get_corrected_url_if_proceeds(for_table=true)
unless redirect_url.nil?
redirect_to redirect_url and return
end
end
if @visualization.organization?
unless current_user && @visualization.has_read_permission?(current_user)
return(embed_forbidden)
end
end
return(redirect_to protocol: 'https://') if @visualization.is_privacy_private? \
&& !(request.ssl? || request.local? || Rails.env.development?)
# Legacy redirect, now all public pages also with org. name
if eligible_for_redirect?(@visualization.user)
redirect_to CartoDB.url(self,
'public_table',
params: { id: params[:id].to_s, redirected: true },
user: @visualization.user)
return
end
@vizjson = @visualization.to_vizjson({https_request: request.protocol == 'https://'})
@auth_tokens = nil
@use_https = false
@api_key = nil
@can_copy = false
if current_user && @visualization.has_read_permission?(current_user)
if @visualization.is_privacy_private?
@auth_tokens = current_user.get_auth_tokens
@use_https = true
@api_key = current_user.api_key
end
@can_copy = true # this table can be copied to user dashboard
end
owner = @visualization.user
# set user to current user only if the user is in the same organization
# this allows to enable "copy this table to your tables" button
if current_user && current_user.organization.present? && owner.organization.present? &&
current_user.organization_id == owner.organization_id
@user = current_user
response.headers['Cache-Control'] = "no-cache,private"
else
@user = @visualization.user
end
@name = @visualization.user.name_or_username
@user_url = CartoDB.url(self, 'public_user_feed_home', user: @visualization.user)
@is_data_library = data_library_user?
if @is_data_library
@name = "Data Library"
@user_url = Cartodb.get_config(:data_library, 'path') ? "#{request.protocol}#{CartoDB.account_host}#{Cartodb.config[:data_library]['path']}" : @user_url
end
@avatar_url = @visualization.user.avatar
@twitter_username = @visualization.user.twitter_username.present? ? @visualization.user.twitter_username : nil
@location = @visualization.user.location.present? ? @visualization.user.location : nil
@user_domain = user_domain_variable(request)
@visualization_id = @visualization.id
@disqus_shortname = @visualization.user.disqus_shortname.presence || 'cartodb'
@public_tables_count = @visualization.user.public_table_count
@total_visualizations = @table.dependent_visualizations.select do |vis|
vis.privacy == Carto::Visualization::PRIVACY_PUBLIC && vis.published?
end
@total_nonpublic_total_vis_count = @table.dependent_visualizations.reject { |vis|
vis.privacy == Carto::Visualization::PRIVACY_PUBLIC
}.count
# Public export API SQL url
@export_sql_api_url = "#{ sql_api_url("SELECT * FROM #{ @table.owner.sql_safe_database_schema }.#{ @table.name }", @user) }&format=shp"
respond_to do |format|
format.html { render 'public_dataset', layout: 'application_table_public' }
end
end
def public_map
if current_user.nil? && !request.params[:redirected].present?
redirect_url = get_corrected_url_if_proceeds(for_table=false)
unless redirect_url.nil?
redirect_to redirect_url and return
end
end
return(embed_forbidden) unless @visualization.is_accesible_by_user?(current_user)
if current_user && @visualization.is_privacy_private? &&
@visualization.has_read_permission?(current_user)
return(show_organization_public_map)
end
# Legacy redirect, now all public pages also with org. name
if eligible_for_redirect?(@visualization.user)
# INFO: here we only want the presenter to rewrite the url of @visualization.user namespacing it like 'schema.id',
# so current_user also equals @visualization.user
visualization_presenter = Carto::Api::VisualizationPresenter.new(@visualization, @visualization.user, self)
redirect_to visualization_presenter.privacy_aware_map_url({ redirected: true },
'public_visualizations_public_map') and return
end
return(public_map_protected) if @visualization.password_protected?
if @visualization.can_be_cached?
response.headers['X-Cache-Channel'] = "#{@visualization.varnish_key}:vizjson"
end
if @more_visualizations && @more_visualizations.length > 0
additional_keys = []
@more_visualizations.each do |vis_adapter|
additional_keys << vis_adapter.visualization.surrogate_key
end
additional_keys = " #{additional_keys.join(' ')}"
else
additional_keys = ''
end
if @visualization.can_be_cached?
response.headers['Surrogate-Key'] =
"#{CartoDB::SURROGATE_NAMESPACE_PUBLIC_PAGES} #{@visualization.surrogate_key}#{additional_keys}"
response.headers['Cache-Control'] = "no-cache,max-age=86400,must-revalidate, public"
end
@name = @visualization.user.name_or_username
@avatar_url = @visualization.user.avatar
@twitter_username = @visualization.user.twitter_username.present? ? @visualization.user.twitter_username : nil
@location = @visualization.user.location.present? ? @visualization.user.location : nil
@google_maps_query_string = @visualization.user.google_maps_query_string
@mapviews = @visualization.total_mapviews
@disqus_shortname = @visualization.user.disqus_shortname.presence || 'cartodb'
@visualization_count = @visualization.user.public_visualization_count
@related_tables = @visualization.related_tables
@related_canonical_visualizations = @visualization.related_canonical_visualizations
@related_tables_owners = Hash.new
@related_tables.each { |table|
unless @related_tables_owners.include?(table.user_id)
table_owner = ::User.where(id: table.user_id).first
if table_owner.nil?
# strange scenario, as user has been deleted but his table still exists
@related_tables_owners[table.user_id] = nil
else
@related_tables_owners[table.user_id] = table_owner
end
end
}
@user_domain = user_domain_variable(request)
@public_tables_count = @visualization.user.public_table_count
@nonpublic_tables_count = @related_tables.select{|t| !t.public? }.count
# We need to know if visualization logo is visible or not
@hide_logo = is_logo_hidden(@visualization, params)
respond_to do |format|
format.html { render layout: 'application_public_visualization_layout' }
format.js { render 'public_map', content_type: 'application/javascript' }
end
rescue => e
CartoDB.notify_exception(e, {user:current_user})
embed_forbidden
end
def show_organization_public_map
return(embed_forbidden) unless org_user_has_map_permissions?(current_user, @visualization)
response.headers['Cache-Control'] = "no-cache,private"
@protected_map_tokens = current_user.get_auth_tokens
@name = @visualization.user.name_or_username
@avatar_url = @visualization.user.avatar
@disqus_shortname = @visualization.user.disqus_shortname.presence || 'cartodb'
@visualization_count = @visualization.user.public_visualization_count
@related_tables = @visualization.related_tables
@related_canonical_visualizations = @visualization.related_canonical_visualizations
@public_tables_count = @visualization.user.public_table_count
@nonpublic_tables_count = @related_tables.select{|p| !p.public? }.count
# We need to know if visualization logo is visible or not
@hide_logo = is_logo_hidden(@visualization, params)
respond_to do |format|
format.html { render 'public_map', layout: 'application_public_visualization_layout' }
end
end
def show_organization_embed_map
return(embed_forbidden) unless org_user_has_map_permissions?(current_user, @visualization)
response.headers['X-Cache-Channel'] = "#{@visualization.varnish_key}:vizjson"
response.headers['Surrogate-Key'] = "#{CartoDB::SURROGATE_NAMESPACE_PUBLIC_PAGES} #{@visualization.surrogate_key}"
response.headers['Cache-Control'] = "no-cache,max-age=86400,must-revalidate, public"
@protected_map_tokens = current_user.get_auth_tokens
respond_to do |format|
format.html { render 'embed_map', layout: 'application_public_visualization_layout' }
end
end
def show_protected_public_map
submitted_password = params.fetch(:password, nil)
return(render_pretty_404) unless @visualization.password_protected? and @visualization.has_password?
unless @visualization.password_valid?(submitted_password)
flash[:placeholder] = '*' * (submitted_password ? submitted_password.size : DEFAULT_PLACEHOLDER_CHARS)
flash[:error] = "Invalid password"
return(public_map_protected)
end
response.headers['X-Cache-Channel'] = "#{@visualization.varnish_key}:vizjson"
response.headers['Surrogate-Key'] = "#{CartoDB::SURROGATE_NAMESPACE_PUBLIC_PAGES} #{@visualization.surrogate_key}"
response.headers['Cache-Control'] = "no-cache,max-age=86400,must-revalidate, public"
@protected_map_tokens = @visualization.get_auth_tokens
@name = @visualization.user.name_or_username
@avatar_url = @visualization.user.avatar
@user_domain = user_domain_variable(request)
@disqus_shortname = @visualization.user.disqus_shortname.presence || 'cartodb'
@visualization_count = @visualization.user.public_visualization_count
@related_tables = @visualization.related_tables
@related_canonical_visualizations = @visualization.related_canonical_visualizations
@public_tables_count = @visualization.user.public_table_count
@nonpublic_tables_count = @related_tables.select{|p| !p.public? }.count
# We need to know if visualization logo is visible or not
@hide_logo = is_logo_hidden(@visualization, params)
respond_to do |format|
format.html { render 'public_map', layout: 'application_public_visualization_layout' }
end
rescue => e
CartoDB::Logger.error(exception: e)
public_map_protected
end
def show_protected_embed_map
submitted_password = params.fetch(:password, nil)
return(render_pretty_404) unless @visualization.password_protected? and @visualization.has_password?
unless @visualization.password_valid?(submitted_password)
flash[:placeholder] = '*' * (submitted_password ? submitted_password.size : DEFAULT_PLACEHOLDER_CHARS)
flash[:error] = "Invalid password"
return(embed_protected)
end
get_viewed_user
response.headers['Cache-Control'] = "no-cache, private"
@protected_map_tokens = @visualization.get_auth_tokens
respond_to do |format|
format.html { render 'embed_map', layout: 'application_public_visualization_layout' }
end
rescue => e
CartoDB::Logger.error(exception: e)
embed_protected
end
def embed_map
if @viewed_user && @viewed_user.has_feature_flag?('static_embed_map')
return render(file: "public/static/embed_map/index.html", layout: false)
end
if request.format == 'text/javascript'
error_message = "/* Javascript embeds are deprecated, please use the html iframe instead */"
return render inline: error_message, status: 400
end
if @cached_embed
response.headers.merge! @cached_embed[:headers].stringify_keys
respond_to do |format|
# Use html_safe to mark the string as trusted since it comes from a successful response.
# We cannot use `render body: @cached_embed[:body]` in Rails 3
format.html { render inline: "<%= @cached_embed[:body].html_safe %>" }
end
else
resp = embed_map_actual
if response.ok? && (@visualization.public? || @visualization.public_with_link?)
#cache response
is_https = (request.protocol == 'https://')
embed_redis_cache.set(@visualization.id, is_https, response.headers, response.body)
end
resp
end
end
# Renders input password view
def embed_protected
render 'embed_map_password', :layout => 'application_password_layout'
end
def public_map_protected
render 'public_map_password', :layout => 'application_password_layout'
end
def embed_forbidden
render 'embed_map_error', layout: false, status: :forbidden
end
def track_embed
response.headers['X-Cache-Channel'] = "embeds_google_analytics"
response.headers['Cache-Control'] = "no-cache,max-age=86400,must-revalidate, public"
render 'track', layout: false
end
protected
def disallowed_type?(visualization)
return true if visualization.nil?
visualization.type_slide?
end
# Check if visualization logo should be hidden or not
def is_logo_hidden(vis, parameters)
has_logo = vis.overlays.any? {|o| o.type == "logo" }
(!has_logo && vis.user.remove_logo? && (!parameters['cartodb_logo'] || parameters['cartodb_logo'] != "true")) || (has_logo && vis.user.remove_logo? && (parameters["cartodb_logo"] == 'false'))
end
private
def more_visualizations(user, excluded_visualization)
vqb = Carto::VisualizationQueryBuilder.user_public_visualizations(user).with_order(:updated_at, :desc)
vqb.with_excluded_ids([excluded_visualization.id]) if excluded_visualization
visualizations = vqb.build_paged(1, MAX_MORE_VISUALIZATIONS)
visualizations.map { |v|
Carto::Admin::VisualizationPublicMapAdapter.new(v, current_user, self)
}
end
def eligible_for_redirect?(user)
return false if CartoDB.subdomainless_urls?
user.has_organization? && !request.params[:redirected].present? &&
CartoDB.subdomain_from_request(request) != user.organization.name
end
def org_user_has_map_permissions?(user, visualization)
user && visualization && visualization.has_read_permission?(user)
end
def resolve_visualization_and_table
filters = { exclude_raster: true }
@visualization, @table =
get_visualization_and_table(@table_id, username_from_schema || CartoDB.extract_subdomain(request), filters)
if @visualization && @visualization.user
@more_visualizations = more_visualizations(@visualization.user, @visualization)
end
end
def ensure_visualization_viewable
render_pretty_404 if disallowed_type?(@visualization)
end
def resolve_visualization_and_table_if_not_cached
is_https = (request.protocol == 'https://')
# TODO review the naming confusion about viz and tables, I suspect templates also need review
@cached_embed = embed_redis_cache.get(@table_id, is_https)
if !@cached_embed
resolve_visualization_and_table
render('embed_map_error', layout: false, status: :not_found) if disallowed_type?(@visualization)
end
end
# If user A shares to user B a table link (being both from same org), attept to rewrite the url to the correct format
# Messing with sessions is bad so just redirect to newly formed url and let new request handle permissions/access
def get_corrected_url_if_proceeds(for_table=true)
url = nil
return url if CartoDB.subdomainless_urls?
org_name = CartoDB.subdomain_from_request(request)
if CartoDB.extract_subdomain(request) != org_name
# Might be an org url, try getting the org
organization = Organization.where(name: org_name).first
unless organization.nil?
authenticated_users = request.session.to_hash.select { |k, _v|
k.start_with?("warden.user") && !k.end_with?(".session")
}.values
authenticated_users.each { |username|
user = ::User.where(username: username).first
if url.nil? && !user.nil? && !user.organization.nil?
if user.organization.id == organization.id
if for_table
url = CartoDB.url(self, 'public_tables_show',
params: { id: "#{params[:user_domain]}.#{params[:id]}", redirected: true },
user: user)
else
url = CartoDB.url(self, 'public_visualizations_show',
params: { id: "#{params[:user_domain]}.#{params[:id]}", redirected: true },
user: user)
end
end
end
}
end
end
url
end
def username_from_schema
(@schema && @schema != 'public') ? @schema : nil
end
def table_and_schema_from_params
if params.fetch('id', nil) =~ /\./
@table_id, @schema = params.fetch('id').split('.').reverse
else
@table_id, @schema = [params.fetch('id', nil), nil]
end
end
def full_table_id
id = @table_id
if @schema
id = @schema + "." + id
end
id
end
def public_url
if request.path_info =~ %r{/tables/}
CartoDB.path(self, 'public_table', { id: full_table_id })
else
CartoDB.path(self, 'public_visualization', { id: full_table_id })
end
end
def public_map_url
if request.path_info =~ %r{/tables/}
CartoDB.path(self, 'public_table_map', { id: full_table_id })
else
CartoDB.path(self, 'public_visualizations_public_map', { id: full_table_id })
end
end
def embed_map_url_for(id)
if request.path_info =~ %r{/tables/}
CartoDB.path(self, 'public_tables_embed_map', { id: id })
else
CartoDB.path(self, 'public_visualizations_embed_map', { id: id })
end
end
def download_formats(table, format)
format.sql { send_data table.to_sql, data_for(table, 'zip', 'zip') }
format.kml { send_data table.to_kml, data_for(table, 'zip', 'kmz') }
format.csv { send_data table.to_csv, data_for(table, 'zip', 'zip') }
format.shp { send_data table.to_shp, data_for(table, 'octet-stream', 'zip') }
end
def data_for(table, type, extension)
{
type: "application/#{type}; charset=binary; header=present",
disposition: "attachment; filename=#{table.name}.#{extension}"
}
end
def render_pretty_404
render(file: "public/404.html", layout: false, status: 404)
end
def user_domain_variable(request)
if params[:user_domain].present?
CartoDB.subdomain_from_request(request) != params[:user_domain] ? params[:user_domain] : nil
else
nil
end
end
def get_visualization_and_table(table_id, schema, filter)
user = Carto::User.where(username: schema).first
# INFO: organization public visualizations
if user
visualization = get_priority_visualization(table_id, user_id: user.id)
else
organization = Carto::Organization.where(name: schema).first
visualization = get_priority_visualization(table_id, organization_id: organization.id) if organization
end
return get_visualization_and_table_from_table_id(table_id) if visualization.nil?
render_pretty_404 if visualization.kind == Carto::Visualization::KIND_RASTER
return Carto::Admin::VisualizationPublicMapAdapter.new(visualization, current_user, self), visualization.table_service
end
def get_visualization_and_table_from_table_id(table_id)
return nil, nil if !is_uuid?(table_id)
user_table = Carto::UserTable.where({ id: table_id }).first
return nil, nil if user_table.nil?
visualization = user_table.visualization
return Carto::Admin::VisualizationPublicMapAdapter.new(visualization, current_user, self), visualization.table_service
end
# TODO: remove this method and use app/helpers/carto/uuidhelper.rb. Not used yet because this changed was pushed before
def is_uuid?(text)
!(Regexp.new(%r{\A#{UUIDTools::UUID_REGEXP}\Z}) =~ text).nil?
end
def sql_api_url(query, user)
"#{ ApplicationHelper.sql_api_template("public").gsub! '{user}', user.username }#{ Cartodb.config[:sql_api]['public']['endpoint'] }?q=#{ URI::encode query }"
end
def embed_map_actual
return(embed_forbidden) if @visualization.private?
return(embed_protected) if @visualization.password_protected?
return(show_organization_embed_map) if org_user_has_map_permissions?(current_user, @visualization)
response.headers['X-Cache-Channel'] = "#{@visualization.varnish_key}:vizjson"
response.headers['Surrogate-Key'] = "#{CartoDB::SURROGATE_NAMESPACE_PUBLIC_PAGES} #{@visualization.surrogate_key}"
response.headers['Cache-Control'] = "no-cache,max-age=86400,must-revalidate, public"
# We need to know if visualization logo is visible or not
@hide_logo = is_logo_hidden(@visualization, params)
respond_to do |format|
format.html { render layout: 'application_public_visualization_layout' }
end
rescue => e
CartoDB::Logger.error(exception: e)
embed_forbidden
end
def embed_redis_cache
@embed_redis_cache ||= EmbedRedisCache.new($tables_metadata)
end
def get_viewed_user
username = CartoDB.extract_subdomain(request)
@viewed_user = ::User.where(username: username).first
if @viewed_user.nil?
username = username.strip.downcase
@org = get_organization_if_exists(username)
end
end
def get_organization_if_exists(name)
Organization.where(name: name).first
end
def data_library_user?
@viewed_user && Cartodb.get_config(:data_library, 'username') == @viewed_user.username
end
def redirect_to_builder_embed_if_v3
# @visualization is not loaded if the embed is cached
# Changing version invalidates the embed cache
if @visualization && @visualization.version == 3
redirect_to CartoDB.url(self, 'builder_visualization_public_embed',
params: { visualization_id: @visualization.id })
end
end
end
@@ -0,0 +1,47 @@
require_relative '../../../lib/cartodb/stats/editor_apis'
class Api::ApplicationController < ApplicationController
protect_from_forgery with: :null_session
# Don't force org urls
skip_before_filter :ensure_org_url_if_org_user, :browser_is_html5_compliant?
skip_before_filter :verify_authenticity_token, if: :json_formatted_request?
before_filter :api_authorization_required
before_filter :ensure_account_has_been_activated
before_filter :setup_stats_instance
protected
def set_start_time
@time_start = Time.now
end
# dry up the jsonp output
def render_jsonp(obj, status = 200, options = {})
if callback_valid?
options.reverse_merge! :json => obj, :status => status, :callback => params[:callback]
else
options.reverse_merge! :json => { errors: { callback: "Invalid callback format" } }, :status => 400
end
render options
end
def setup_stats_instance
@stats_aggregator = CartoDB::Stats::EditorAPIs.instance
end
def valid_password_confirmation
unless current_user.valid_password_confirmation(params[:password_confirmation])
raise Carto::PasswordConfirmationError.new
end
end
private
def callback_valid?
# While only checks basic characters, represents most common use of JS function names
params[:callback].nil? || !!(params[:callback] =~ /\A[$a-z_][0-9a-z_$]*\z/i)
end
end
@@ -0,0 +1,42 @@
class Api::Json::AssetsController < Api::ApplicationController
ssl_required :create, :destroy
def create
@stats_aggregator.timing('assets.create') do
begin
@asset = Asset.new
@asset.raise_on_save_failure = true
@asset.user_id = current_user.id
@asset.asset_file = params[:filename]
@asset.url = params[:url]
@asset.kind = params[:kind]
@stats_aggregator.timing('save') do
@asset.save
end
render_jsonp(Carto::Api::AssetPresenter.new(@asset).to_hash)
rescue Sequel::ValidationFailed => e
CartoDB::Logger.warning(exception: e, message: 'Validation error creating asset')
render json: { error: @asset.errors.full_messages }, status: 400
rescue => e
CartoDB::Logger.error(exception: e, message: 'Error creating asset')
render json: { error: [e.message] }, status: 400
end
end
end
def destroy
@stats_aggregator.timing('assets.destroy.delete') do
begin
Asset[params[:id]].destroy
head :ok
rescue => e
CartoDB::Logger.error(exception: e, message: 'Error destroying asset')
render json: { error: [e.message] }, status: 400
end
end
end
end
@@ -0,0 +1,90 @@
require Rails.root.join('services', 'sql-api', 'sql_api')
class Api::Json::GeocodingsController < Api::ApplicationController
ssl_required :create, :update
before_filter :load_table, only: [:create, :estimation_for]
# In seconds
GEOCODING_SQLAPI_CALLS_TIMEOUT = 45
def update
@stats_aggregator.timing('geocodings.update') do
begin
geocoding = current_user.geocodings_dataset.where(id: params[:id]).first
return head(401) unless geocoding && params[:state] == 'cancelled'
@stats_aggregator.timing('save') do
geocoding.cancel
end
render_jsonp(geocoding.reload)
rescue => e
render_jsonp({ errors: e.message }, 400)
end
end
end
def create
@stats_aggregator.timing('geocodings.create') do
begin
geocoding = Geocoding.new params.slice(:kind, :geometry_type, :formatter, :country_code, :region_code)
geocoding.user = current_user
geocoding.table_id = @table.try(:id)
geocoding.table_name = params[:table_name] ? params[:table_name] : @table.try(:name)
geocoding.raise_on_save_failure = true
geocoding.force_all_rows = (params[:force_all_rows].to_s == 'true')
geocoding.formatter = "{#{ params[:column_name] }}" if params[:column_name].present?
geocoding = @stats_aggregator.timing('special-params') do
# TODO api should be more regular
unless ['high-resolution', 'ipaddress'].include? params[:kind] then
if params[:text]
countries = [params[:location]]
else
countries = @table.sequel.distinct.select_map(params[:location].to_sym)
geocoding.country_column = params[:location]
end
geocoding.country_code = countries.map{|c| "'#{ c }'"}.join(',')
if params[:region]
if params[:region_text]
regions = [params[:region]]
else
regions = @table.sequel.distinct.select_map(params[:region].to_sym)
geocoding.region_column = params[:region]
end
geocoding.region_code = regions.map{|r| "'#{ r }'"}.join(',')
end
end
geocoding
end
geocoding = @stats_aggregator.timing('save') do
geocoding.save
geocoding
end
@table.automatic_geocoding.destroy if @table.automatic_geocoding.present?
Resque.enqueue(Resque::GeocoderJobs, job_id: geocoding.id)
render_jsonp(geocoding.to_json)
rescue Sequel::ValidationFailed => e
CartoDB.notify_exception(e)
render_jsonp( { description: e.message }, 422)
rescue => e
CartoDB.notify_exception(e)
render_jsonp( { description: e.message }, 500)
end
end
end
protected
def load_table
@table = Carto::Helpers::TableLocator.new.get_by_id_or_name(params.fetch('table_name'), current_user).try(:service)
end
end
@@ -0,0 +1,233 @@
require_relative '../../../helpers/file_upload'
require_relative '../../../../services/datasources/lib/datasources'
require_relative '../../../models/visualization/external_source'
require_relative '../../../../services/platform-limits/platform_limits'
require_relative '../../../../services/importer/lib/importer/exceptions'
require_dependency 'carto/uuidhelper'
require_dependency 'carto/url_validator'
class Api::Json::ImportsController < Api::ApplicationController
include Carto::UUIDHelper
include Carto::UrlValidator
ssl_required :create
ssl_allowed :invalidate_service_token
# NOTE: When/If OAuth tokens management is built into the UI, remove this to send and check CSRF
skip_before_filter :verify_authenticity_token, only: [:invalidate_service_token]
INVALID_TOKEN_MESSAGE = 'OAuth token invalid or expired'
# -------- Import process -------
def create
@stats_aggregator.timing('imports.create') do
begin
file_upload_helper = CartoDB::FileUpload.new(Cartodb.config[:importer].fetch("uploads_path", nil))
external_source = nil
concurrent_import_limit =
CartoDB::PlatformLimits::Importer::UserConcurrentImportsAmount.new({
user: current_user,
redis: { db: $users_metadata }
})
raise CartoDB::Importer2::UserConcurrentImportsLimitError.new if concurrent_import_limit.is_over_limit!
options = default_creation_options
if params[:url].present?
validate_url!(params.fetch(:url)) unless Rails.env.development? || Rails.env.test?
options[:data_source] = params.fetch(:url)
elsif params[:connector].present?
options[:service_name] = 'connector'
options[:service_item_id] = params[:connector].to_json
elsif params[:remote_visualization_id].present?
external_source = external_source(params[:remote_visualization_id])
options[:data_source] = external_source.import_url.presence
else
options = @stats_aggregator.timing('upload-or-enqueue') do
results = file_upload_helper.upload_file_to_storage(
filename_param: params[:filename],
file_param: params[:file],
request_body: request.body,
s3_config: Cartodb.config[:importer]['s3'])
# In Rack < 1.6 / Rails < 4, tempfiles are not inmediately cleaned (https://github.com/rack/rack/pull/671).
# Instead they stay around until a GC cycle which can take a while in instances with low traffic.
# This forces the tempfile to be removed right away, just after we have saved it to our storage.
[:filename, :file].each do |param_name|
file = params[param_name]
file.tempfile.close! if file
end
# Not queued import is set by skipping pending state and setting directly as already enqueued
options.merge({
data_source: results[:file_uri].presence,
state: results[:enqueue] ? DataImport::STATE_PENDING : DataImport::STATE_ENQUEUED
})
end
end
# override param to store as string
user_limits = ::JSON.dump(options[:user_defined_limits])
data_import = @stats_aggregator.timing('save') do
DataImport.create(options.merge!({ user_defined_limits: user_limits }))
end
if external_source.present?
@stats_aggregator.timing('external-data-import.save') do
ExternalDataImport.new(data_import.id, external_source.id).save
end
end
Resque.enqueue(Resque::ImporterJobs, job_id: data_import.id) if options[:state] == DataImport::STATE_PENDING
render_jsonp({ item_queue_id: data_import.id, success: true })
rescue CartoDB::Importer2::UserConcurrentImportsLimitError
rl_value = decrement_concurrent_imports_rate_limit
render_jsonp({
errors: { imports: "We're sorry but you're already using your allowed #{rl_value} import slots" }
}, 429)
rescue => ex
decrement_concurrent_imports_rate_limit
CartoDB::StdoutLogger.info('Error: create', "#{ex.message} #{ex.backtrace.inspect}")
render_jsonp({ errors: { imports: ex.message } }, 400)
end
end
end
# ----------- Import OAuths Management -----------
def invalidate_service_token
@stats_aggregator.timing('imports.invalidate-service-token') do
begin
oauth = current_user.oauths.select(params[:id])
raise CartoDB::Datasources::AuthError.new("No oauth set for service #{params[:id]}") if oauth.nil?
datasource = oauth.get_service_datasource
raise CartoDB::Datasources::AuthError.new("Couldn't fetch datasource for service #{params[:id]}") if datasource.nil?
unless datasource.kind_of? CartoDB::Datasources::BaseOAuth
raise CartoDB::Datasources::InvalidServiceError.new("Datasource #{params[:id]} does not support OAuth")
end
result = @stats_aggregator.timing('revoke') do
datasource.revoke_token
end
if result
@stats_aggregator.timing('remove') do
current_user.oauths.remove(oauth.service)
end
end
render_jsonp({ success: true })
rescue => ex
CartoDB::StdoutLogger.info('Error: invalidate_service_token', "#{ex.message} #{ex.backtrace.inspect}")
render_jsonp({ errors: { imports: ex.message } }, 400)
end
end
end
private
def default_creation_options
user_defined_limits = params.fetch(:user_defined_limits, {})
# Sanitize
user_defined_limits[:twitter_credits_limit] =
user_defined_limits[:twitter_credits_limit].presence.nil? ? 0 : user_defined_limits[:twitter_credits_limit].to_i
# Already had an internal issue due to forgetting to send always a string (e.g. for Twitter is an stringified JSON)
raise "service_item_id field should be empty or a string" unless (params[:service_item_id].is_a?(String) ||
params[:service_item_id].is_a?(NilClass))
# Keep in sync with https://docs.carto.com/cartodb-platform/import-api.html#params
{
user_id: current_user.id,
table_name: params[:table_name].presence,
# Careful as this field has rules (@see DataImport data_source=)
data_source: nil,
table_id: params[:table_id].presence,
append: (params[:append].presence == 'true'),
table_copy: params[:table_copy].presence,
from_query: params[:sql].presence,
service_name: params[:service_name].present? ? params[:service_name] : CartoDB::Datasources::Url::PublicUrl::DATASOURCE_NAME,
service_item_id: params[:service_item_id].present? ? params[:service_item_id] : params[:url].presence,
type_guessing: !["false", false].include?(params[:type_guessing]),
quoted_fields_guessing: !["false", false].include?(params[:quoted_fields_guessing]),
content_guessing: ["true", true].include?(params[:content_guessing]),
state: DataImport::STATE_PENDING, # Pending == enqueue the task
upload_host: Socket.gethostname,
create_visualization: ["true", true].include?(params[:create_vis]),
user_defined_limits: user_defined_limits,
privacy: privacy,
collision_strategy: params[:collision_strategy]
}
end
def decorate_twitter_import_data!(data, data_import)
return if data_import.service_name != CartoDB::Datasources::Search::Twitter::DATASOURCE_NAME
audit_entry = ::SearchTweet.where(data_import_id: data_import.id).first
data[:tweets_georeferenced] = audit_entry.retrieved_items
data[:tweets_cost] = audit_entry.price
data[:tweets_overquota] = audit_entry.user.remaining_twitter_quota == 0
end
def decorate_default_visualization_data!(data, data_import)
derived_vis_id = nil
if data_import.create_visualization && !data_import.visualization_id.nil?
derived_vis_id = data_import.visualization_id
end
data[:derived_visualization_id] = derived_vis_id
end
def external_source(remote_visualization_id)
external_source = Carto::ExternalSource.where(visualization_id: remote_visualization_id).first
unless remote_visualization_id.present? && external_source.importable_by?(current_user)
raise CartoDB::Datasources::AuthError.new('Illegal external load')
end
external_source
end
def decrement_concurrent_imports_rate_limit
begin
concurrent_import_limit =
CartoDB::PlatformLimits::Importer::UserConcurrentImportsAmount.new({
user: current_user,
redis: {
db: $users_metadata
}
})
# It's ok to decrease always as if over limit, will get just at limit and next try again go overlimit
concurrent_import_limit.decrement!
concurrent_import_limit.peek # return limit value
rescue => sub_exception
CartoDB::StdoutLogger.info('Error decreasing concurrent import limit',
"#{sub_exception.message} #{sub_exception.backtrace.inspect}")
nil
end
end
def privacy
if params[:privacy].present?
privacy = Carto::UserTable::PRIVACY_VALUES_TO_TEXTS.invert[params[:privacy].downcase]
if privacy.nil?
valid_privacies = [
Carto::UserTable::PRIVACY_VALUES_TO_TEXTS.values[0..-2].join(', '),
Carto::UserTable::PRIVACY_VALUES_TO_TEXTS.values[-1]
].join(' and ')
raise "Unknown value '#{params[:privacy]}' for 'privacy'. Allowed values are: #{valid_privacies}"
elsif !current_user.valid_privacy?(privacy)
raise "Your account type (#{current_user.account_type.tr('[]', '')}) does not allow to create private "\
"datasets. Check https://carto.com/pricing for more info."
end
privacy
end
end
end
@@ -0,0 +1,20 @@
class Api::Json::OrganizationsController < Api::ApplicationController
include CartoDB
ssl_required :show, :users
# Fetch info from the current user orgranization
def show
render json: {}.to_json if current_user.organization.nil?
render json: current_user.organization.to_poro
end
# Return user list of current user organization
def users
render json: {}.to_json if current_user.organization.nil?
render json: current_user.organization.to_poro[:users]
end
end
@@ -0,0 +1,243 @@
require 'json'
require_relative '../../../models/synchronization/member'
require_relative '../../../models/synchronization/collection'
require_relative '../../../../services/datasources/lib/datasources'
require_relative '../../../../services/platform-limits/platform_limits'
require_dependency 'carto/url_validator'
class Api::Json::SynchronizationsController < Api::ApplicationController
include CartoDB
include Carto::UrlValidator
ssl_required :create, :update, :destroy, :sync, :sync_now
before_filter :set_external_source, only: [ :create ]
# Upon creation, no rate limit checks
def create
return head(401) unless current_user.sync_tables_enabled || @external_source
@stats_aggregator.timing('synchronizations.create') do
begin
member_attributes = setup_member_attributes
member = Synchronization::Member.new(member_attributes)
member = @stats_aggregator.timing('member.save') do
member.store
end
options = setup_data_import_options(member_attributes, member.id)
data_import = @stats_aggregator.timing('save') do
DataImport.create(options)
end
if @external_source
@stats_aggregator.timing('external-data-import.save') do
ExternalDataImport.new(data_import.id, @external_source.id, member.id).save
end
end
::Resque.enqueue(::Resque::ImporterJobs, job_id: data_import.id)
# Need to mark the synchronization job as queued state.
# If this is missed there is an error state that can be
# achieved where the synchronization job can never be
# manually kicked off ever again. This state will occur if the
# resque job fails to mark the synchronization state to success or
# failure (ie: resque never runs, or bug in ImporterJobs code)
member.state = Synchronization::Member::STATE_QUEUED
member.store
response = {
data_import: {
endpoint: '/api/v1/imports',
item_queue_id: data_import.id
}
}.merge(member.to_hash)
render_jsonp(response)
rescue CartoDB::InvalidMember => exception
render_jsonp({ errors: member.full_errors }, 400)
puts exception.to_s
puts exception.backtrace
rescue CartoDB::InvalidInterval => exception
render_jsonp({ errors: "#{exception.detail['message']}: #{exception.detail['hint']}" }, 400)
rescue InvalidUrlError => exception
CartoDB::StdoutLogger.info('Error: create', "#{exception.message} #{exception.backtrace.inspect}")
render_jsonp({ errors: exception.message }, 400)
end
end
end
def sync(from_sync_now=false)
@stats_aggregator.timing('synchronizations.sync') do
begin
enqueued = false
member = Synchronization::Member.new(id: params[:id]).fetch
return head(401) unless member.authorize?(current_user)
# @see /services/synchronizer/lib/synchronizer/collection.rb -> enqueue_rate_limited()
if ( member.should_auto_sync? || (from_sync_now && member.can_manually_sync?) )
platform_limit = CartoDB::PlatformLimits::Importer::UserConcurrentSyncsAmount.new({
user: current_user, redis: { db: $users_metadata }
})
if platform_limit.is_within_limit?
@stats_aggregator.timing('enqueue') do
member.enqueue
end
enqueued = true
platform_limit.increment!
end
end
render_jsonp( { enqueued: enqueued, synchronization_id: member.id})
rescue => exception
CartoDB.notify_exception(exception)
head(404)
end
end
end
def sync_now
sync(true)
end
def update
@stats_aggregator.timing('synchronizations.update') do
begin
member = Synchronization::Member.new(id: params.fetch('id')).fetch
return head(401) unless member.authorize?(current_user)
member.attributes = payload
member = @stats_aggregator.timing('save') do
member.store.fetch
end
render_jsonp(member)
rescue KeyError
head(404)
rescue CartoDB::InvalidMember
render_jsonp({ errors: member.full_errors }, 400)
end
end
end
def destroy
@stats_aggregator.timing('synchronizations.destroy') do
begin
member = Synchronization::Member.new(id: params.fetch('id')).fetch
return(head 401) unless member.authorize?(current_user)
@stats_aggregator.timing('delete') do
member.delete
end
return head 204
rescue KeyError
head(404)
end
end
end
private
def set_external_source
@external_source =
if params[:remote_visualization_id].present?
get_external_source(params[:remote_visualization_id])
end
end
def setup_member_attributes
member_attributes = payload.merge(
name: params[:table_name],
user_id: current_user.id,
state: Synchronization::Member::STATE_CREATED,
# Keep in sync with https://carto.com/developers/import-api/guides/sync-tables/#params-1
type_guessing: !["false", false].include?(params[:type_guessing]),
quoted_fields_guessing: !["false", false].include?(params[:quoted_fields_guessing]),
content_guessing: ["true", true].include?(params[:content_guessing])
)
if from_sync_file_provider?
member_attributes = member_attributes.merge({
service_name: params[:service_name],
service_item_id: params[:service_item_id]
})
end
if params[:remote_visualization_id].present?
member_attributes[:interval] = Carto::ExternalSource::REFRESH_INTERVAL
external_source = @external_source
member_attributes[:url] = external_source.import_url.presence
member_attributes[:service_item_id] = external_source.import_url.presence
end
if params[:connector].present?
member_attributes[:service_name] = 'connector'
member_attributes[:service_item_id] = params[:connector].to_json
end
member_attributes
end
def setup_data_import_options(member_attributes, member_id)
if from_sync_file_provider?
service_name = params[:service_name]
service_item_id = params[:service_item_id]
else
service_name = CartoDB::Datasources::Url::PublicUrl::DATASOURCE_NAME
service_item_id = params[:url].presence
end
options = {
user_id: current_user.id,
table_name: params[:table_name].presence,
service_name: service_name,
service_item_id: service_item_id,
type_guessing: member_attributes[:type_guessing],
quoted_fields_guessing: member_attributes[:quoted_fields_guessing],
content_guessing: member_attributes[:content_guessing],
create_visualization: ["true", true].include?(params[:create_vis])
}
if params[:remote_visualization_id].present?
external_source = get_external_source(params[:remote_visualization_id])
options.merge!(data_source: external_source.import_url.presence)
elsif params[:connector].present?
options[:service_name] = 'connector'
options[:service_item_id] = params[:connector].to_json
else
url = params[:url]
validate_url!(url) unless Rails.env.development? || Rails.env.test? || url.nil? || url.empty?
options.merge!(data_source: url)
end
options.merge!({ synchronization_id: member_id })
options
end
def from_sync_file_provider?
params.include?(:service_name) && params.include?(:service_item_id)
end
def payload
request.body.rewind
::JSON.parse(request.body.read.to_s || String.new)
end
def get_external_source(remote_visualization_id)
external_source = Carto::ExternalSource.where(visualization_id: remote_visualization_id).first
unless remote_visualization_id.present? && external_source.importable_by?(current_user)
raise CartoDB::Datasources::AuthError.new('Illegal external load')
end
external_source
end
end
@@ -0,0 +1,50 @@
class Api::Json::UploadsController < Api::ApplicationController
ssl_required :create
skip_before_filter :verify_authenticity_token
before_filter :api_or_user_authorization_required
def create
@stats_aggregator.timing('uploads.create') do
begin
temp_file = filename = filedata = nil
case
when params[:filename].present? && request.body.present?
filename = params[:filename]
filedata = request.body.read.force_encoding('utf-8')
when params[:file].present?
filename = params[:file].original_filename
filedata = params[:file].read.force_encoding('utf-8')
end
random_token = Digest::SHA2.hexdigest("#{Time.now.utc}--#{filename.object_id.to_s}").first(20)
file_upload_helper = CartoDB::FileUpload.new(Cartodb.config[:importer].fetch("uploads_path", nil))
file_upload_helper.get_uploads_path
@stats_aggregator.timing('save') do
FileUtils.mkdir_p(file_upload_helper.get_uploads_path.join(random_token))
file = File.new(file_upload_helper.get_uploads_path.join(random_token).join(File.basename(filename)), 'w')
file.write filedata
file.close
end
render :json => {:file_uri => file.path[/(\/uploads\/.*)/, 1], :success => true}
rescue => e
logger.error e
logger.error e.backtrace
head(400)
end
end
end
def api_or_user_authorization_required
api_authorization_required || login_required
end
private :api_or_user_authorization_required
end
@@ -0,0 +1,16 @@
require 'uri'
require 'json'
require_relative '../../../../services/wms/proxy'
class Api::Json::WmsController < Api::ApplicationController
ssl_required :index
ssl_allowed :proxy
def proxy
proxy = CartoDB::WMS::Proxy.new(params.fetch(:url))
render_jsonp(proxy.serialize)
rescue URI::InvalidURIError => exception
render_jsonp({ errors: "Couldn't load URL" }, 400)
end
end
+490
View File
@@ -0,0 +1,490 @@
require_relative '../../lib/cartodb/profiler.rb'
require_dependency 'carto/http_header_authentication'
class ApplicationController < ActionController::Base
include UrlHelper
protect_from_forgery
helper :all
around_filter :wrap_in_profiler
before_filter :set_security_headers
before_filter :http_header_authentication, if: :http_header_authentication?
before_filter :store_request_host
before_filter :ensure_user_organization_valid
before_filter :ensure_org_url_if_org_user
before_filter :ensure_account_has_been_activated
before_filter :browser_is_html5_compliant?
before_filter :set_asset_debugging
before_filter :cors_preflight_check
before_filter :check_maintenance_mode
before_filter :check_user_state
after_filter :allow_cross_domain_access
after_filter :remove_flash_cookie
after_filter :add_revision_header
rescue_from NoHTML5Compliant, :with => :no_html5_compliant
rescue_from ActiveRecord::RecordNotFound, RecordNotFound, with: :render_404
ME_ENDPOINT_COOKIE = :_cartodb_base_url
IGNORE_PATHS_FOR_CHECK_USER_STATE = %w(maintenance_mode lockout login logout unauthenticated multifactor_authentication).freeze
def self.ssl_required(*splat)
if Rails.env.production? || Rails.env.staging?
if splat.any?
force_ssl only: splat
else
force_ssl
end
end
end
def self.ssl_allowed(*_splat)
# noop
end
# current_user relies on request subdomain ALWAYS, so current_viewer will always return:
# - If subdomain is present in the sessions: subdomain-based session (aka current_user)
# - Else: the first session found at request.session that comes from warden
def current_viewer
if @current_viewer.nil?
if current_user && env["warden"].authenticated?(current_user.username)
@current_viewer = current_user if validate_session(current_user)
else
authenticated_usernames = request.session.to_hash.select { |k, _|
k.start_with?("warden.user") && !k.end_with?(".session")
}.values
# See if there's a session of the viewed subdomain corresponding user
current_user_present = authenticated_usernames.select { |username|
CartoDB.extract_subdomain(request) == username
}.first
# If current user session was there, do nothing; else, retrieve first available
if current_user_present.nil?
unless authenticated_usernames.first.nil?
user = ::User.where(username: authenticated_usernames.first).first
validate_session(user, false) unless user.nil?
@current_viewer = user
end
end
end
end
@current_viewer
end
protected
Warden::Manager.after_authentication do |user, auth, opts|
auth.cookies.permanent[ME_ENDPOINT_COOKIE] = {
value: CartoDB.base_url(user.username),
domain: Cartodb.config[:session_domain]
} if opts[:store]
# Do not even send the Set-Cookie header if the strategy did not store anything in the session
auth.request.session_options[:skip] = true if opts[:store] == false
end
Warden::Manager.before_logout do |_user, auth, _opts|
auth.cookies.delete(ME_ENDPOINT_COOKIE, domain: Cartodb.config[:session_domain])
end
def handle_unverified_request
render_403
end
# @see Warden::Manager.after_set_user
def update_session_security_token(user)
warden.session(user.username)[:sec_token] = Digest::SHA1.hexdigest(user.crypted_password)
end
def session_security_token_valid?(user)
warden.session(user.username).key?(:sec_token) &&
warden.session(user.username)[:sec_token] == Digest::SHA1.hexdigest(user.crypted_password)
rescue Warden::NotAuthenticated
false
end
def validate_session(user = current_user, reset_session_on_error = true)
if session_security_token_valid?(user)
true
else
reset_session if reset_session_on_error
false
end
end
def is_https?
request.protocol == 'https://'
end
def http_header_authentication
authenticate(:http_header_authentication, scope: CartoDB.extract_subdomain(request))
if current_user
validate_session(current_user)
else
authenticator = Carto::HttpHeaderAuthentication.new
if authenticator.autocreation_enabled?
if authenticator.creation_in_progress?(request)
redirect_to CartoDB.path(self, 'signup_http_authentication_in_progress')
else
redirect_to CartoDB.path(self, 'signup_http_authentication')
end
end
end
end
# To be used only when domainless urls are present, to replicate sent subdomain
def store_request_host
return unless CartoDB.subdomainless_urls?
match = /([\w\-\.]+)(:[\d]+)?\/?/.match(request.host.to_s)
unless match.nil?
CartoDB.request_host = match[1]
end
end
def wrap_in_profiler
if params[:profile_request].present? && current_user.present? && current_user.has_feature_flag?('profiler')
CartoDB::Profiler.new().call(request) { yield }
else
yield
end
end
def set_asset_debugging
CartoDB::Application.config.assets.debug =
(Cartodb.config[:debug_assets].nil? ? true : Cartodb.config[:debug_assets]) if Rails.env.development?
end
def cors_preflight_check
if request.method == :options && check_cors_headers_for_whitelisted_origin
common_cors_headers
response.headers['Access-Control-Max-Age'] = '3600'
elsif !Rails.env.production? && !Rails.env.staging?
development_cors_headers
end
end
def allow_cross_domain_access
if !request.headers['origin'].blank? && check_cors_headers_for_whitelisted_origin
common_cors_headers
response.headers['Access-Control-Allow-Credentials'] = 'true'
elsif !Rails.env.production? && !Rails.env.staging?
development_cors_headers
end
end
def common_cors_headers
response.headers['Access-Control-Allow-Origin'] = request.headers['origin']
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, DELETE'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
end
def development_cors_headers
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = '*'
response.headers['Access-Control-Allow-Headers'] = '*'
end
def check_cors_headers_for_whitelisted_origin
origin = request.headers['origin']
cors_enabled_hosts = Cartodb.get_config(:cors_enabled_hosts) || []
allowed_hosts = ([Cartodb.config[:account_host]] + cors_enabled_hosts).compact
allowed_hosts.include?(URI.parse(origin).host)
end
def check_user_state
return if IGNORE_PATHS_FOR_CHECK_USER_STATE.any? { |path| request.path.end_with?("/" + path) }
viewed_username = CartoDB.extract_subdomain(request)
if current_user.nil? || current_user.username != viewed_username
user = Carto::User.find_by_username(viewed_username)
if user.try(:locked?)
render_locked_owner
return
end
elsif current_user.locked?
render_locked_user
return
end
render_multifactor_authentication if multifactor_authentication_required?
end
def check_maintenance_mode
return if IGNORE_PATHS_FOR_CHECK_USER_STATE.any? { |path| request.path.end_with?("/" + path) }
viewed_username = CartoDB.extract_subdomain(request)
if current_user.nil? || current_user.username != viewed_username
user = Carto::User.find_by_username(viewed_username)
if user.try(:maintenance_mode?)
render_locked_owner
return
end
elsif current_user.maintenance_mode?
render_maintenance_mode
return
end
end
def render_403
respond_to do |format|
format.html { render(file: 'public/403.html', status: 403, layout: false) }
format.all { head(:forbidden) }
end
end
def render_404
respond_to do |format|
format.html do
render :file => 'public/404.html', :status => 404, :layout => false
end
format.json do
head :not_found
end
end
end
def render_500
render_http_code(500)
end
def render_http_code(error_code, public_page_error_code = error_code, error_message = 'Unknown error')
respond_to do |format|
format.html do
render file: "public/#{public_page_error_code}.html", status: error_code, layout: false
end
format.json do
render json: { error_message: error_message }, status: error_code
end
end
end
def multifactor_authentication_required?(user = current_viewer)
user &&
user.multifactor_authentication_configured? &&
!warden.session(user.username)[:multifactor_authentication_performed] &&
!warden.session(user.username)[:skip_multifactor_authentication]
rescue Warden::NotAuthenticated
false
end
def login_required
is_auth = authenticated?(CartoDB.extract_subdomain(request))
is_auth ? validate_session(current_user) : not_authorized
end
def login_required_any_user
current_viewer ? validate_session(current_viewer) : not_authorized
end
def api_authorization_required
authenticate!(:auth_api, :api_authentication, scope: CartoDB.extract_subdomain(request))
validate_session(current_user)
end
def any_api_authorization_required
authenticate!(:any_auth_api, :api_authentication, scope: CartoDB.extract_subdomain(request))
validate_session(current_user)
end
def engine_required
render_404 unless current_viewer.try(:engine_enabled?)
end
# This only allows to authenticate if sending an API request to username.api_key subdomain,
# but doesn't break the request if can't authenticate
def optional_api_authorization
got_auth = authenticate(:auth_api, :api_authentication, scope: CartoDB.extract_subdomain(request))
validate_session(current_user) if got_auth
end
def redirect_or_forbidden(path, error)
respond_to do |format|
format.html do
redirect_to CartoDB.url(self, path)
end
format.json do
render(json: { error: error }, status: 403)
end
end
end
def render_multifactor_authentication
session[:return_to] = request.original_url
redirect_or_forbidden('multifactor_authentication_session', 'mfa_required')
end
def render_locked_user
redirect_or_forbidden('lockout', 'lockout')
end
def render_maintenance_mode
redirect_or_forbidden('maintenance_mode', 'maintenance_mode')
end
def render_locked_owner
respond_to do |format|
format.html do
render_404
end
format.json do
head 404
end
end
end
def not_authorized
respond_to do |format|
format.html do
session[:return_to] = request.url
redirect_to CartoDB.url(self, 'login', keep_base_url: true)
return
end
format.json do
head :unauthorized
end
end
end
def table_privacy_text(table)
if table.is_a?(::Table)
table.privacy_text
elsif table.is_a?(Hash)
table['privacy']
end
end
helper_method :table_privacy_text
# TODO: Move to own exception infrastructure
def translate_error(exception)
return exception if exception.blank? || exception.is_a?(String)
case exception
when CartoDB::EmptyFile
when CartoDB::InvalidUrl
when CartoDB::InvalidFile
when CartoDB::TableCopyError
when CartoDB::QuotaExceeded
exception.detail
when Sequel::DatabaseError
# TODO: rationalise these error codes
if exception.message.include?("transform: couldn't project")
Cartodb.error_codes[:geometries_error].merge(:raw_error => exception.message)
else
Cartodb.error_codes[:unknown_error].merge(:raw_error => exception.message)
end
else
Cartodb.error_codes[:unknown_error].merge(:raw_error => exception.message)
end.to_json
end
def no_html5_compliant
logout
render :file => "#{Rails.root}/public/HTML5.html", :status => 500, :layout => false
end
# In some cases the flash message is going to be set in the fronted with js after making a request to the API
# We use this filter to ensure it disappears in the very first request
def remove_flash_cookie
cookies.delete(:flash) if cookies[:flash]
end
def browser_is_html5_compliant?
user_agent = request.user_agent.try(:downcase)
return true if user_agent.nil?
banned_regex = [
/msie [0-9]\./, /safari\/[0-4][0-2][0-2]/, /opera\/[0-8].[0-7]/, /firefox\/[0-2].[0-5]/
]
if banned_regex.map { |re| user_agent.match(re) }.compact.first
raise NoHTML5Compliant
end
end
def ensure_user_organization_valid
return if CartoDB.subdomainless_urls?
org_subdomain = CartoDB.extract_host_subdomain(request)
unless org_subdomain.nil? || current_user.nil?
if current_user.organization.nil? || current_user.organization.name != org_subdomain
render_404
end
end
end
# By default, override Admin urls unless :dont_rewrite param is present
def ensure_org_url_if_org_user
return if CartoDB.subdomainless_urls?
rewrite_url = !request.params[:dont_rewrite].present?
if rewrite_url && !current_user.nil? && !current_user.organization.nil? &&
CartoDB.subdomain_from_request(request) == current_user.username
if request.fullpath == '/'
redirect_to CartoDB.url(self, 'dashboard')
else
redirect_to CartoDB.base_url(current_user.organization.name, current_user.username) << request.fullpath
end
end
end
def ensure_account_has_been_activated
return unless current_user
if !current_user.enable_account_token.nil?
respond_to do |format|
format.html {
redirect_to CartoDB.url(self, 'account_token_authentication_error')
}
format.all {
head :forbidden
}
end
end
end
def add_revision_header
response.headers['X-CartoDB-Rev'] = CartoDB::CARTODB_REV unless CartoDB::CARTODB_REV.nil?
end
def current_user
super(CartoDB.extract_subdomain(request))
end
def update_user_last_activity
return false if current_user.nil?
current_user.set_last_active_time
current_user.set_last_ip_address request.remote_ip
end
def ensure_required_params(required_params)
params_with_value = params.reject { |_, v| v.empty? }
missing_params = required_params - params_with_value.keys
raise Carto::MissingParamsError.new(missing_params) unless missing_params.empty?
end
protected :current_user
def json_formatted_request?
format = request.format
format.json? if format
end
private
def http_header_authentication?
Carto::HttpHeaderAuthentication.new.valid?(request)
end
def set_security_headers
headers['X-Frame-Options'] = 'DENY'
headers['X-XSS-Protection'] = '1; mode=block'
headers['X-Content-Type-Options'] = 'nosniff'
end
end
@@ -0,0 +1,32 @@
module Carto
module Admin
class MapPublicMapAdapter
extend Forwardable
delegate [ :provider, :zoom ] => :map
attr_reader :map
def initialize(map)
@map = map
end
def public_values
{
id: @map.id,
user_id: @map.user_id,
provider: @map.provider,
bounding_box_sw: @map.bounding_box_sw,
bounding_box_ne: @map.bounding_box_ne,
center: @map.center,
zoom: @map.zoom,
view_bounds_sw: @map.view_bounds_sw,
view_bounds_ne: @map.view_bounds_ne,
legends: @map.legends,
scrollwheel: @map.scrollwheel
}
end
end
end
end
@@ -0,0 +1,152 @@
require_dependency 'helpers/avatar_helper'
require_dependency 'cartodb/central'
require_dependency 'helpers/organization_notifications_helper'
class Carto::Admin::MobileAppsController < Admin::AdminController
include Carto::ControllerHelper
include MobileAppsHelper
include AvatarHelper
include OrganizationNotificationsHelper
ssl_required :index, :show, :new, :create, :update, :destroy
before_filter :invalidate_browser_cache
before_filter :login_required
before_filter :check_user_permissions
before_filter :initialize_cartodb_central_client
before_filter :load_organization_notifications
before_filter :validate_id, only: [:show, :update, :destroy]
before_filter :load_mobile_app, only: [:show, :update]
before_filter :setup_avatar_upload, only: [:new, :create, :show, :update]
rescue_from Carto::LoadError, with: :render_404
layout 'application'
def index
response = @cartodb_central_client.get_mobile_apps(current_user.username).deep_symbolize_keys
@mobile_apps = response[:mobile_apps].map { |a| Carto::MobileApp.new(a) }
@open_monthly_users = response[:monthly_users][:open]
@private_monthly_users = response[:monthly_users][:private]
rescue CartoDB::CentralCommunicationFailure => e
@mobile_apps = []
CartoDB::Logger.error(message: 'Error loading mobile apps from Central', exception: e)
flash.now[:error] = 'Unable to connect to license server. Try again in a moment.'
end
def show
end
def new
@mobile_app = Carto::MobileApp.new
end
def create
@mobile_app = Carto::MobileApp.new(params[:mobile_app])
@mobile_app.icon_url = get_default_avatar unless @mobile_app.icon_url.present?
unless @mobile_app.valid?
flash.now[:error] = @mobile_app.errors.full_messages.join(', ')
render :new
return
end
attributes = @mobile_app.as_json.symbolize_keys.slice(:name, :description, :icon_url, :platform, :app_id, :app_type)
@cartodb_central_client.create_mobile_app(current_user.username, attributes)
redirect_to CartoDB.url(self, 'mobile_apps'), flash: { success: 'Your app has been added succesfully!' }
rescue CartoDB::CentralCommunicationFailure => e
if e.response_code == 422
if e.errors.any? { |e| e['app_id'] }
@mobile_app.errors["app_id"] = 'has already been taken'
flash.now[:error] = "That application ID has already been taken. Please make sure that it is unique."
else
flash.now[:error] = e.errors.join('. ')
end
else
CartoDB::Logger.error(message: 'Error creating mobile_app in Central', exception: e)
flash.now[:error] = 'Unable to connect to license server. Try again in a moment.'
end
render :new
end
def update
updated_attributes = params[:mobile_app].symbolize_keys.slice(:name, :description, :icon_url, :app_type)
@mobile_app.name = updated_attributes[:name]
@mobile_app.icon_url = updated_attributes[:icon_url]
@mobile_app.description = updated_attributes[:description]
unless @mobile_app.valid?
flash.now[:error] = @mobile_app.errors.full_messages.join(', ')
render :show
return
end
@cartodb_central_client.update_mobile_app(current_user.username, @app_id, updated_attributes)
redirect_to(CartoDB.url(self, 'mobile_app', params: { id: @app_id }),
flash: { success: 'Your app has been updated succesfully!' })
rescue CartoDB::CentralCommunicationFailure => e
if e.response_code == 422
flash.now[:error] = e.errors
else
CartoDB::Logger.error(message: 'Error updating mobile_app in Central', exception: e)
flash.now[:error] = 'Unable to connect to license server. Try again in a moment.'
end
render :show
end
def destroy
valid_password_confirmation
@cartodb_central_client.delete_mobile_app(current_user.username, @app_id)
redirect_to CartoDB.url(self, 'mobile_apps'), flash: { success: 'Your app has been deleted succesfully!' }
rescue Carto::PasswordConfirmationError => e
flash[:error] = e.message
redirect_to(CartoDB.url(self, 'mobile_app', params: { id: @app_id }))
rescue CartoDB::CentralCommunicationFailure => e
raise Carto::LoadError.new('Mobile app not found') if e.response_code == 404
CartoDB::Logger.error(message: 'Error deleting mobile app from Central', exception: e, app_id: @app_id)
redirect_to(CartoDB.url(self, 'mobile_app', params: { id: @app_id }),
flash: { error: 'Unable to connect to license server. Try again in a moment.' })
end
private
def check_user_permissions
raise Carto::LoadError.new('Mobile apps disabled') unless current_user.mobile_sdk_enabled?
end
def initialize_cartodb_central_client
raise Carto::LoadError.new('Mobile apps disabled') unless Cartodb::Central.sync_data_with_cartodb_central?
@cartodb_central_client ||= Cartodb::Central.new
end
def validate_id
@app_id = uuid_parameter(:id)
end
def setup_avatar_upload
@icon_valid_extensions = AVATAR_VALID_EXTENSIONS
end
def load_mobile_app
@mobile_app = Carto::MobileApp.new(@cartodb_central_client.get_mobile_app(current_user.username, @app_id))
rescue CartoDB::CentralCommunicationFailure => e
raise Carto::LoadError.new('Mobile app not found') if e.response_code == 404
CartoDB::Logger.error(message: 'Error loading mobile app from Central', exception: e, app_id: @app_id)
redirect_to(CartoDB.url(self, 'mobile_apps'),
flash: { error: 'Unable to connect to license server. Try again in a moment.' })
end
def get_default_avatar
if Cartodb.asset_path
"#{Cartodb.asset_path}/assets/unversioned/images/avatars/mobile_app_default_avatar.png"
else
"#{relative_url_root}/#{frontend_version}/images/avatars/mobile_app_default_avatar.png"
end
end
end
@@ -0,0 +1,32 @@
module Carto
module Admin
class UserPublicMapAdapter
extend Forwardable
delegate [:id, :name, :username, :disqus_shortname, :avatar, :avatar_url, :remove_logo?, :has_organization?,
:organization, :organization_id, :twitter_username, :location, :public_url, :subdomain,
:sql_safe_database_schema, :account_type, :google_maps_query_string, :basemaps, :default_basemap,
:name_or_username] => :user
attr_reader :user
def initialize(user)
@user = user
end
def public_table_count
@public_table_count ||= Carto::VisualizationQueryBuilder.user_public_tables(@user).build.count
end
def public_visualization_count
@public_visualization_count ||= Carto::VisualizationQueryBuilder.user_public_visualizations(@user).build.count
end
def all_visualization_count
@all_visualization_count ||= Carto::VisualizationQueryBuilder.user_all_visualizations(@user).build.count
end
end
end
end
@@ -0,0 +1,18 @@
module Carto
module Admin
class UserTablePublicMapAdapter
extend Forwardable
delegate [:dependent_visualizations, :partially_dependent_visualizations,
:fully_dependent_visualizations, :name, :id] => :user_table
attr_reader :user_table
def initialize(user_table)
@user_table = user_table
end
end
end
end
@@ -0,0 +1,100 @@
require_relative '../../../helpers/carto/html_safe'
require_dependency 'carto/api/vizjson_presenter'
module Carto
module Admin
class VisualizationPublicMapAdapter
extend Forwardable
include Carto::HtmlSafe
GEOMETRY_MAPPING = {
'st_multipolygon' => 'polygon',
'st_polygon' => 'polygon',
'st_multilinestring' => 'line',
'st_linestring' => 'line',
'st_multipoint' => 'point',
'st_point' => 'point'
}
delegate [
:type_slide?, :derived?, :organization, :organization?, :id,
:password_protected?, :varnish_key, :related_tables, :password_valid?, :get_auth_tokens, :table, :name,
:overlays, :created_at, :updated_at, :description, :mapviews, :geometry_types, :privacy, :tags,
:surrogate_key, :has_password?, :total_mapviews, :is_viewable_by_user?, :is_accesible_by_user?,
:can_be_cached?, :is_privacy_private?, :source, :kind_raster?, :has_read_permission?, :has_write_permission?,
:open_in_editor?, :version, :kind, :public?, :public_with_link?, :user_table, :liked_by?
] => :visualization
attr_reader :visualization
def initialize(visualization, current_viewer, context)
@visualization = visualization
@current_viewer = current_viewer
@context = context
end
def to_vizjson(options = {})
Carto::Api::VizJSONPresenter.new(@visualization, $tables_metadata).to_vizjson(options)
end
def is_owner?(user)
@visualization.owner?(user)
end
def to_hash(options={})
# TODO: using an Api presenter here smells, refactor
presenter = Carto::Api::VisualizationPresenter.new(@visualization, @current_viewer, @context, options.merge(show_stats: false))
options.delete(:public_fields_only) === true ? presenter.to_public_poro : presenter.to_poro
end
def map
Carto::Admin::MapPublicMapAdapter.new(@visualization.map)
end
def user
Carto::Admin::UserPublicMapAdapter.new(@visualization.user)
end
# TODO: remove is_ prefixed methods from visualization
def private?
@visualization.private?
end
def related_canonical_visualizations
@visualization.related_canonical_visualizations.map { |rv|
Carto::Admin::VisualizationPublicMapAdapter.new(rv, @current_viewer, @context) if rv.public?
}.compact
end
def related_visualizations_geometry_types
related_canonical_visualizations.collect(&:geometry_types).flatten.uniq
end
def related_tables_geometry_types
related_tables.collect(&:geometry_types).flatten.uniq
end
def related_visualizations_simple_geometry_types
simplify_geometry_types(related_visualizations_geometry_types)
end
def related_tables_simple_geometry_types
simplify_geometry_types(related_tables_geometry_types)
end
def map_zoom
map.nil? ? nil : map.zoom
end
def display_name_or_name
@visualization.display_name.nil? ? @visualization.name : @visualization.display_name
end
private
def simplify_geometry_types(geometry_types)
geometry_types.map { |gt| GEOMETRY_MAPPING.fetch(gt.downcase, "unknown: #{gt}") }
end
end
end
end
@@ -0,0 +1,155 @@
require_dependency 'carto/uuidhelper'
require_relative '../builder/builder_users_module'
module Carto
module Api
class AnalysesController < ::Api::ApplicationController
include Carto::ControllerHelper
include Carto::UUIDHelper
include Carto::Builder::BuilderUsersModule
ssl_required :show, :create, :update, :destroy
before_filter :builder_users_only
before_filter :load_visualization
before_filter :check_user_can_add_analysis, only: [:show, :create, :update, :destroy]
before_filter :load_analysis, only: [:show, :update, :destroy]
rescue_from StandardError, with: :rescue_from_standard_error
rescue_from Carto::LoadError, with: :rescue_from_carto_error
rescue_from Carto::UnauthorizedError, with: :rescue_from_carto_error
rescue_from Carto::UnprocesableEntityError, with: :rescue_from_carto_error
def show
render_jsonp(AnalysisPresenter.new(@analysis).to_poro)
end
def create
natural_id = analysis_definition_from_request['id']
analysis = Carto::Analysis.find_by_natural_id(@visualization.id, natural_id)
if analysis
analysis.analysis_definition = analysis_definition_from_request
else
analysis = Carto::Analysis.new(
visualization_id: @visualization.id,
user_id: current_user.id,
analysis_definition: analysis_definition_from_request
)
end
analysis.save!
render_jsonp(AnalysisPresenter.new(analysis).to_poro, 201)
end
def update
new_definition = analysis_definition_from_request
new_root_node = Carto::AnalysisNode.new(new_definition.deep_symbolize_keys)
modified_node_ids = find_modified_nodes(@analysis.analysis_node, new_root_node)
affected_node_ids = find_affected_nodes(modified_node_ids)
@analysis.analysis_definition = new_definition
@analysis.save!
purge_layer_node_style_history(affected_node_ids)
render_jsonp(AnalysisPresenter.new(@analysis).to_poro, 200)
end
def destroy
@analysis.destroy
render_jsonp(AnalysisPresenter.new(@analysis).to_poro, 200)
end
private
def purge_layer_node_style_history(node_ids)
Carto::LayerNodeStyle.from_visualization_and_source(@visualization, node_ids).delete_all
end
def find_affected_nodes(modified_node_ids)
all_visualization_nodes = @visualization.analyses.map(&:analysis_node).map(&:descendants).flatten
all_visualization_nodes.select { |node|
node.descendants.any? { |descendant| modified_node_ids.include?(descendant.id) }
}.map(&:id)
end
def find_modified_nodes(old_root, new_root)
old_nodes = old_root.descendants
new_nodes = new_root.descendants
old_ids = old_nodes.map(&:id)
new_ids = new_nodes.map(&:id)
kept_ids = old_ids & new_ids
kept_ids.select do |node_id|
old_node = old_nodes.find { |n| n.id == node_id }
new_node = new_nodes.find { |n| n.id == node_id }
old_node.non_child_params != new_node.non_child_params
end
end
def analysis_definition_from_request
analysis_json = json_post(request.raw_post)
if analysis_json.nil? || analysis_json.empty?
raise Carto::UnprocesableEntityError.new("Empty analysis")
end
analysis_definition = analysis_json['analysis_definition']
if analysis_definition.nil? || analysis_definition.empty? || analysis_definition.class == String
raise Carto::UnprocesableEntityError.new("Invalid analysis definition")
end
analysis_definition
end
def json_post(raw_post = request.raw_post)
@json_post ||= (raw_post.present? ? JSON.parse(raw_post) : nil)
rescue => e
# Malformed JSON is not our business
CartoDB.notify_warning_exception(e)
raise UnprocesableEntityError.new("Malformed JSON: #{raw_post}")
end
def load_visualization
visualization_id = params[:visualization_id]
if payload_visualization_id.present? && payload_visualization_id != visualization_id
raise UnprocesableEntityError.new("url vis (#{visualization_id}) != payload (#{payload_visualization_id})")
end
@visualization = Carto::Visualization.where(id: visualization_id).first if visualization_id
raise LoadError.new("Visualization not found: #{visualization_id}") unless @visualization
end
def payload_visualization_id
json_post.present? ? json_post['visualization_id'] : nil
end
def payload_analysis_id
json_post.present? ? json_post['id'] : nil
end
def check_user_can_add_analysis
if @visualization.user_id != current_user.id
raise Carto::UnauthorizedError.new("#{current_user.id} doesn't own visualization #{@visualization.id}")
end
end
def load_analysis
if payload_analysis_id.present? && payload_analysis_id != params[:id]
raise UnprocesableEntityError.new("url analysis (#{params[:id]}) != payload (#{payload_analysis_id})")
end
unless params[:id].nil?
@analysis = Carto::Analysis.where(id: params[:id]).first if is_uuid?(params[:id])
if @analysis.nil?
@analysis = Carto::Analysis.find_by_natural_id(@visualization.id, params[:id])
end
end
raise Carto::LoadError.new("Analysis not found: #{params[:id]}") unless @analysis
end
end
end
end
@@ -0,0 +1,34 @@
module Carto
module Api
class AnalysisPresenter
def initialize(analysis)
@analysis = analysis
end
def to_poro
return {} unless @analysis
@analysis.analysis_node.descendants.each do |node|
style_history = Carto::LayerNodeStyle.from_visualization_and_source(@analysis.visualization, node.id)
node.options[:style_history] = style_history.map { |lns|
[
lns.layer_id,
{
tooltip: lns.tooltip,
infowindow: lns.infowindow,
options: lns.options
}
]
}.to_h
end
{
id: @analysis.id,
analysis_definition: @analysis.analysis_definition
}
end
end
end
end
@@ -0,0 +1,81 @@
require 'json'
module Carto
module Api
class ApiKeyPresenter
def initialize(api_key)
@api_key = api_key
end
def to_poro
return {} unless @api_key
{
name: @api_key.name,
user: { username: @api_key.user.username },
type: @api_key.type,
token: @api_key.token,
grants: get_grants,
created_at: @api_key.created_at.to_s,
updated_at: @api_key.updated_at.to_s
}
end
private
def get_grants
grants = [
{
type: 'apis',
apis: @api_key.granted_apis
}
]
type_database = {
type: 'database',
tables: table_permissions_for_api_key,
schemas: schema_permissions_for_api_key
}
if @api_key.dataset_metadata_permissions
type_database['table_metadata'] = []
end
grants << type_database
if @api_key.data_services?
grants << {
type: 'dataservices',
services: @api_key.data_services
}
end
grants
end
def table_permissions_for_api_key
return [] if @api_key.master? || @api_key.default_public?
@api_key.table_permissions_from_db.map do |p|
{
schema: p.schema,
name: p.name,
owner: p.owner,
permissions: p.permissions
}
end
end
def schema_permissions_for_api_key
return [] if @api_key.master? || @api_key.default_public?
@api_key.schema_permissions_from_db.map do |p|
{
name: p.name,
permissions: p.permissions
}
end
end
end
end
end
@@ -0,0 +1,97 @@
require_relative 'paged_searcher'
class Carto::Api::ApiKeysController < ::Api::ApplicationController
include Carto::ControllerHelper
include Carto::UUIDHelper
include Carto::Api::PagedSearcher
include Carto::Api::AuthApiAuthentication
ssl_required :create, :destroy, :regenerate_token, :show, :index
before_filter :any_api_authorization_required, only: [:index, :show]
skip_filter :api_authorization_required, only: [:index, :show]
before_filter :engine_required
before_filter :load_api_key, only: [:destroy, :regenerate_token, :show]
rescue_from Carto::ParamInvalidError, with: :rescue_from_carto_error
rescue_from Carto::LoadError, with: :rescue_from_carto_error
rescue_from Carto::UnprocesableEntityError, with: :rescue_from_carto_error
rescue_from Carto::UnauthorizedError, with: :rescue_from_carto_error
rescue_from Carto::CartoError, with: :rescue_from_carto_error
VALID_ORDER_PARAMS = [:type, :name, :updated_at].freeze
VALID_TYPE_PARAMS = [Carto::ApiKey::TYPE_MASTER,
Carto::ApiKey::TYPE_DEFAULT_PUBLIC,
Carto::ApiKey::TYPE_REGULAR].freeze
def create
carto_viewer = Carto::User.find(current_viewer.id)
api_key = carto_viewer.api_keys.create_regular_key!(name: params[:name], grants: params[:grants])
render_jsonp(Carto::Api::ApiKeyPresenter.new(api_key).to_poro, 201)
rescue ActiveRecord::RecordInvalid => e
raise Carto::UnprocesableEntityError.new(e.message)
rescue CartoDB::QuotaExceeded => e
raise Carto::CartoError.new(e.message, 403)
end
def destroy
raise Carto::UnauthorizedError.new unless @viewed_api_key.can_be_deleted?
@viewed_api_key.destroy
head :no_content
end
def regenerate_token
@viewed_api_key.regenerate_token!
render_jsonp(Carto::Api::ApiKeyPresenter.new(@viewed_api_key).to_poro, 200)
end
def index
page, per_page, order, _order_direction = page_per_page_order_params(VALID_ORDER_PARAMS)
api_keys = Carto::User.find(current_user.id).api_keys.by_type(type_param).order_weighted_by_type
api_keys = request_api_key.master? ? api_keys : api_keys.where(id: request_api_key.id)
filtered_api_keys = Carto::PagedModel.paged_association(api_keys, page, per_page, order)
result = filtered_api_keys.map { |api_key| json_for_api_key(api_key) }
render_jsonp(
paged_result(
result: result,
total_count: api_keys.count,
page: page,
per_page: per_page,
params: params
) { |params| api_keys_url(params) },
200
)
end
def show
render_jsonp(Carto::Api::ApiKeyPresenter.new(@viewed_api_key).to_poro, 200)
end
private
def load_api_key
name = params[:id]
@viewed_api_key = Carto::ApiKey.where(user_id: current_viewer.id, name: name).user_visible.first
if !@viewed_api_key || !request_api_key.master? && @viewed_api_key != request_api_key
raise Carto::LoadError.new("API key not found: #{name}")
end
end
def json_for_api_key(api_key)
Carto::Api::ApiKeyPresenter.new(api_key).to_poro.merge(
_links: {
self: api_key_url(id: CGI::escape(api_key.name))
}
)
end
def type_param
types = (params[:type] || '').split(',').map(&:strip)
raise Carto::ParamInvalidError.new(:type, VALID_TYPE_PARAMS) unless (types - VALID_TYPE_PARAMS).empty?
types
end
end
@@ -0,0 +1,14 @@
class Carto::Api::AssetPresenter
def initialize(asset)
@asset = asset
end
def to_hash
{
id: @asset.id,
public_url: @asset.absolute_public_url,
user_id: @asset.user_id,
kind: @asset.kind
}
end
end
@@ -0,0 +1,11 @@
class Carto::Api::AssetsController < ::Api::ApplicationController
ssl_required :index
def index
assets = current_viewer.assets.map do |asset|
Carto::Api::AssetPresenter.new(asset).to_hash
end
render json: { total_entries: assets.size, assets: assets }
end
end
@@ -0,0 +1,67 @@
require_relative '../../../models/carto/permission'
module Carto
module Api
class ColumnsController < ::Api::ApplicationController
ssl_required :index, :show, :create, :update, :destroy
before_filter :load_user_table, only: [:index, :show, :create, :update, :destroy]
before_filter :read_privileges?, only: [:index, :show]
before_filter :write_privileges?, only: [:create, :update, :destroy]
def index
render_jsonp(@user_table.service.schema(cartodb_types: true))
end
def show
resp = @user_table.service.schema(cartodb_types: true).select { |e| e[0] == params[:id].to_sym }.first.last
render_jsonp(type: resp)
rescue => e
CartoDB::Logger.error(message: 'Error loading column', exception: e,
column_id: params[:id], user_table: @user_table)
render_jsonp({ errors: "Column #{params[:id]} doesn't exist" }, 404)
end
def create
render_jsonp(@user_table.service.add_column!(params.slice(:type, :name)))
rescue => e
errors = e.is_a?(CartoDB::InvalidType) ? [e.db_message] : [translate_error(e.message.split("\n").first)]
render_jsonp({ errors: errors }, 400)
end
def update
render_jsonp(@user_table.service.modify_column!(name: params[:id],
type: params[:type],
new_name: params[:new_name]))
rescue => e
errors = e.is_a?(CartoDB::InvalidType) ? [e.db_message] : [translate_error(e.message.split("\n").first)]
render_jsonp({ errors: errors }, 400)
end
def destroy
@user_table.service.drop_column!(name: params[:id])
head :no_content
rescue => e
errors = e.is_a?(CartoDB::InvalidType) ? [e.db_message] : [translate_error(e.message.split("\n").first)]
render_jsonp({ errors: errors }, 400)
end
protected
def load_user_table
@user_table = Carto::Helpers::TableLocator.new.get_by_id_or_name(params[:table_id], current_user)
raise RecordNotFound unless @user_table
end
def read_privileges?
head(401) unless current_user && @user_table.visualization.is_viewable_by_user?(current_user)
end
def write_privileges?
head(401) unless current_user && @user_table.visualization.writable_by?(current_user)
end
end
end
end
@@ -0,0 +1,75 @@
module Carto
module Api
class ConnectorsController < ::Api::ApplicationController
MAX_LISTED_TABLES = 500
ssl_required :index, :show, :tables, :connect
before_filter :check_availability
def index
render_jsonp(Carto::Connector.providers(user: current_user))
end
def show
provider_id = params[:provider_id]
begin
information = Carto::Connector.information(provider_id)
render_jsonp(information)
rescue Carto::Connector::InvalidParametersError
render_jsonp({ errors: "Provider #{provider_id} unknown" }, 422)
end
end
def connect
provider_id = params[:provider_id]
parameters = build_connection_parameters(provider_id, params)
begin
connector = Carto::Connector.new(parameters, user: current_user, logger: nil)
render_jsonp({"connected": connector.check_connection})
rescue Carto::Connector::InvalidParametersError => e
render_jsonp({ errors: e.message }, 422)
rescue
render_jsonp({ errors: "Error connecting to provider #{provider_id}, check connection parameters" }, 400)
end
end
def tables
provider_id = params[:provider_id]
parameters = build_connection_parameters(provider_id, params)
if Carto::Connector.list_tables?(provider_id)
begin
connector = Carto::Connector.new(parameters, user: current_user, logger: nil)
render_jsonp(connector.list_tables(MAX_LISTED_TABLES))
rescue Carto::Connector::InvalidParametersError => e
render_jsonp({ errors: e.message }, 422)
rescue
render_jsonp({ errors: "Error connecting to provider #{provider_id}, check connection parameters" }, 400)
end
else
render_jsonp({ errors: "Provider #{provider_id} doesn't support list tables" }, 422)
end
end
private
def build_connection_parameters(provider_id, request_params)
parameters = {}
parameters[:provider] = request_params[:provider_id]
parameters[:connection] = {}
provider_information = Carto::Connector.information(provider_id)
provider_information[:parameters]["connection"].each do |key, _value|
if request_params[key.to_sym].present?
parameters[:connection][key.to_sym] = request_params[key.to_sym]
end
end
parameters
end
def check_availability
head 404 unless Connector.available?(current_user)
end
end
end
end
@@ -0,0 +1,191 @@
require 'uri'
require 'json'
module Carto
module Api
class DataImportPresenter
HTTP_RESPONSE_CODE_MESSAGES = {
"300" => "This usually means the file has been moved! Click on the link to get its new location.",
"301" => "This usually means the file has been moved! Click on the link to get its new location.",
"302" => "This usually means the file has been temporally moved! Click on the link to get its new location.",
"303" => "This usually means the file has been moved! Click on the link to get its new location.",
"307" => "This usually means the file has been temporally moved! Click on the link to get its new location.",
"400" => "This usually means CARTO did not perform the request properly.",
"401" => "This usually means CARTO is not authorized to retrieve this file. If you have authorization, " \
"download the file manually and upload it from your computer.",
"402" => "This usually means CARTO is not authorized to retrieve this file. If you have authorization, " \
"download the file manually and upload it from your computer.",
"403" => "This usually means CARTO is not authorized to retrieve this file. If you have authorization, " \
"download the file manually and upload it from your computer. Tip: Should you be using 'https' " \
"instead of 'http' in your URL?",
"404" => "This usually means no file was found at the specified URL.",
"405" => "This usually means CARTO could not negotiate the download with the file's provider. The " \
"provider is probably using a non standard method to serve this file.",
"407" => "This usually means CARTO is not authorized to retrieve this file as proxy authentication " \
"is required. If you can use the apropiate proxy, download the file manually and upload " \
"it from your computer.",
"408" => "This usually means a timeout request was produced. You may want to try again.",
"409" => "This usually means a confilict in the request was produced. You might want to try again.",
"410" => "This usually means the file specified is now longer available at this location.",
"411" => "This usually means CARTO did not perform the request properly. 'Content Length' header is missing.",
"412" => "This usually means CARTO could not negotiate the download with the file's provider. The " \
"provider is probably using a non standard method to serve this file.",
"413" => "This usually means the file provider is denying the download because the file is too large.",
"417" => "This usually means CARTO could not negotiate the download with the file's provider. The " \
"provider is probably using a non standard method to serve this file.",
"500" => "This usually means the file provider responded with an internal server error. They might " \
"be overloaded or having some down time. Try again later!",
"501" => "This usually means CARTO could not negotiate the download with the file's provider. The " \
"provider is probably using a non standard method to serve this file.",
"502" => "This usually means the file provider responded with a bad gateway error.",
"503" => "This usually means the file provider responded with an internal server error. They might " \
"be overloaded or having some down time. Try again later!",
"505" => "This usually means the file provider doesn't seem to support the HTTP version used in the " \
"transaction. The provider is probably using a non standard method to serve this file.",
"511" => "This usually means CARTO is not authorized to retrieve this file. If you have " \
"authorization, download the file manually and upload it from your computer." }
def initialize(data_import)
@data_import = data_import
end
def api_public_values
public_values.reject { |key|
NON_API_VISIBLE_ATTRIBUTES.include?(key)
}
end
private
PUBLIC_ATTRIBUTES = [
'id',
'user_id',
'table_id',
'data_type',
'table_name',
'state',
'error_code',
'queue_id',
'tables_created_count',
'synchronization_id',
'service_name',
'service_item_id',
'type_guessing',
'quoted_fields_guessing',
'content_guessing',
'server',
'host',
'upload_host',
'resque_ppid',
'create_visualization',
'visualization_id',
# String field containing a json, format:
# {
# twitter_credits: Integer
# }
# No automatic conversion coded
'user_defined_limits'
]
NON_API_VISIBLE_ATTRIBUTES = [
'service_item_id',
'service_name',
'server',
'host',
'upload_host',
'resque_ppid',
]
def public_values
values = Hash[PUBLIC_ATTRIBUTES.map{ |attribute| [attribute, @data_import.send(attribute)] }]
values.merge!('get_error_text' => get_error_text)
values.merge!('display_name' => display_name)
values.merge!('queue_id' => @data_import.id)
values.merge!(success: @data_import.success) if @data_import.final_state?
values.merge!(original_url: @data_import.original_url)
values.merge!(data_type: @data_import.data_type)
values.merge!(warnings: get_warnings)
values.merge!(is_raster: @data_import.is_raster?)
if !@data_import.http_response_code.nil? && !@data_import.http_response_code.start_with?('2')
values.merge!(http_response_code: @data_import.http_response_code)
values.merge!(http_response_code_message: get_http_response_code_message(@data_import.http_response_code))
end
values
end
def get_error_text
if @data_import.error_code.nil?
nil
else
@data_import.error_code.blank? ? CartoDB::IMPORTER_ERROR_CODES[99999] : CartoDB::IMPORTER_ERROR_CODES[@data_import.error_code]
end
end
def display_name
if @data_import.service_name == 'twitter_search'
extract_twitter_display_name(@data_import)
else
url = [@data_import.data_source, @data_import.service_item_id].compact.select { |s| s != '' }.first
display_name = url.nil? ? @data_import.id : extract_filename(url)
display_name || @data_import.id
end
rescue => e
CartoDB.notify_debug(
'Error extracting display name',
data_import_id: @data_import.id,
service_item_id: @data_import.service_item_id,
data_source: @data_import.data_source,
exception: e.inspect
)
@data_import.id
end
def extract_filename(url)
URI.decode(File.basename(URI.parse(URI.encode(url.strip)).path))
end
def extract_twitter_display_name(data_import)
"Tweets about '#{JSON.parse(data_import.service_item_id)['categories'].map { |c| c['terms'] }.join(', ')}'"
rescue => e
CartoDB.notify_debug('Error extracting Twitter import display name', { data_import_id: data_import.id, service_item_id: data_import.service_item_id, data_source: data_import.data_source })
"Twitter search #{data_import.id}"
end
def get_http_response_code_message(http_response_code)
return nil if http_response_code.nil?
message = HTTP_RESPONSE_CODE_MESSAGES[http_response_code]
if message.nil?
message = case http_response_code
when /^1/ then "This usually means more actions are required to download this file."
when /^2/ then "This usually means everything went fine while fetching the file."
when /^3/ then "This usually means an unknown redirection error has occured."
when /^4/ then "This usually means an unknown client error has occured."
when /^5/ then "This usually means an unknown server error has occured."
else "This is an unkown type of HTTP status code."
end
end
message
end
# All warnings should be parsed into a single Hash here
def get_warnings
warnings = {}
if !@data_import.rejected_layers.nil?
warnings.merge!(rejected_layers: @data_import.rejected_layers.split(','))
warnings.merge!(user_max_layers: @data_import.user.max_layers)
end
warnings.merge!(JSON.parse(@data_import.runner_warnings)) if !@data_import.runner_warnings.nil?
warnings.empty? ? nil : warnings
end
end
end
end
@@ -0,0 +1,197 @@
require_dependency 'cartodb/errors'
module Carto
module Api
# Group metadata registration. Exclusively for usage from PostgreSQL Extension, not from the Editor.
# It only registers metadata, actual group roles management must be done by the extension.
# Named "DatabaseGroups" because it receives _databases_, not organizations.
class DatabaseGroupsController < ::ApplicationController
respond_to :json
ssl_required :create, :update, :destroy, :add_users, :remove_users, :update_permission, :destroy_permission
# TODO: Make this controller inherit from ::Api::ApplicationController and remove skip_before_filter bellow
skip_before_filter :verify_authenticity_token
# Allow HTTPS on local/test as the calls from the groups API are done sending a https X-Forwarded-Proto,
# like simulating they come from https:
# @see https://github.com/CartoDB/cartodb-postgresql/blob/bce61c1e4359653134134097d269edae581e5660/scripts-available/CDB_Groups_API.sql#L170
# Without it, SSL is forbidden and a 302 to url "without HTTP" was returned so the API didn't work on testing
def ssl_allowed?
Rails.env.development? || Rails.env.test?
end
before_filter :authenticate_extension
before_filter :load_parameters
before_filter :load_mandatory_group, :only => [:destroy, :add_users, :remove_users, :update_permission, :destroy_permission]
before_filter :load_user_from_username, :only => [:load_table, :update_permission, :destroy_permission]
before_filter :load_users_from_username, :only => [:add_users, :remove_users]
before_filter :load_table, :only => [:update_permission, :destroy_permission]
def create
group = Group.new_instance(@database_name, @name, @database_role)
if group.save
render json: group.to_json
else
render json: { errors: "Error saving group: #{group.errors}" }, status: 400
end
rescue CartoDB::ModelAlreadyExistsError => e
CartoDB.notify_debug('Group already exists', { params: params })
render json: { errors: "A group with that data already exists" }, status: 409
rescue => e
CartoDB.notify_exception(e, { params: params , group: (group ? group : 'not created') })
render json: { errors: e.message }, status: 500
end
def update
new_name = params['name']
group = get_group_from_loaded_parameters
if group
group.rename(new_name, @database_role)
if group.save
render json: @group.to_json
else
raise "Error saving group: #{@group.errors}"
end
else
renamed_group = get_group(@database_name, new_name)
if renamed_group && renamed_group.database_role == @database_role
CartoDB.notify_debug('Group already renamed', { params: params })
render json: { errors: "That group has already been renamed" }, status: 409
else
raise "Group not found and no matching rename found"
end
end
rescue => e
CartoDB.notify_exception(e, { params: params , group: (@group ? @group : 'not loaded') })
render json: { errors: e.message }, status: 500
end
def destroy
@group.destroy
render json: {}, status: 204
rescue => e
CartoDB.notify_exception(e, { params: params , group: (@group ? @group : 'not loaded') })
render json: { errors: e.message }, status: 500
end
def add_users
added_usernames = []
@usernames.map { |username|
begin
added_usernames << @group.add_user(username).user.username
rescue CartoDB::ModelAlreadyExistsError => e
# This will provoke 409 response later
end
}
if added_usernames.length == @usernames.length
render json: { users: added_usernames }, status: 200
else
render json: { errors: "Some users were already in the group: #{@usernames - added_usernames }", users: added_usernames }, status: 409
end
rescue => e
CartoDB.notify_exception(e, { params: params , group: (@group ? @group : 'not loaded') })
render json: { errors: e.message }, status: 500
end
def remove_users
removed_usernames = []
@usernames.map { |username|
removed_user = @group.remove_user(username)
removed_usernames << removed_user.username if !removed_user.nil?
}
if removed_usernames.length == @usernames.length
render json: { users: removed_usernames }, status: 200
else
render json: { errors: "Some users (#{@usernames - removed_usernames}) were not in the group", users: removed_usernames }, status: 404
end
rescue => e
CartoDB.notify_exception(e, { params: params , group: (@group ? @group : 'not loaded') })
render json: { errors: e.message }, status: 500
end
def update_permission
permission = CartoDB::Permission[@table.permission.id]
permission.set_group_permission(@group, @access)
permission.save
render json: {}, status: 200
rescue CartoDB::ModelAlreadyExistsError => e
CartoDB.notify_debug('Permission already granted', { params: params })
render json: { errors: "That permission is already granted" }, status: 409
rescue => e
CartoDB.notify_exception(e, { params: params , group: (@group ? @group : 'not loaded') })
render json: { errors: e.message }, status: 500
end
def destroy_permission
permission = CartoDB::Permission[@table.permission.id]
permission.remove_group_permission(@group)
permission.save
render json: {}, status: 200
rescue CartoDB::ModelAlreadyExistsError => e
CartoDB.notify_debug('Permission already revoked', { params: params })
render json: { errors: "That permission is already revoked" }, status: 404
rescue => e
CartoDB.notify_exception(e, { params: params , group: (@group ? @group : 'not loaded') })
render json: { errors: e.message }, status: 500
end
private
def authenticate_extension
raise "missing org_metadata_api configuration" unless Cartodb.config[:org_metadata_api]
authenticate_or_request_with_http_basic do |username, password|
username == Cartodb.config[:org_metadata_api]["username"] && password == Cartodb.config[:org_metadata_api]["password"]
end
end
def load_parameters
@database_name = params[:database_name]
@name = [params[:old_name], params[:name]].compact.first
@database_role = params[:database_role]
@username = params[:username]
@usernames = @username.present? ? [ @username ] : params[:users]
@table_name = params[:table_name]
case params['access']
when nil
when 'r'
@access = CartoDB::Permission::ACCESS_READONLY
when 'w'
@access = CartoDB::Permission::ACCESS_READWRITE
else raise "Unknown access #{params['access']}"
end
end
def get_group_from_loaded_parameters
get_group(@database_name, @name)
end
def get_group(database_name, name)
Group.where(organization_id: Organization.find_by_database_name(database_name).id, name: name).first
end
def load_mandatory_group
@group = get_group_from_loaded_parameters
render json: { errors: "Group with database_name #{@database_name} and name #{@name} not found" }, status: 404 unless @group
end
def load_user_from_username
@user = Carto::User.where(username: @username).first
end
def load_users_from_username
@users = @usernames.map { |username| Carto::User.where(username: username).first }
end
def load_table
@table = Carto::Visualization.where(user_id: @user.id, type: 'table', name: @table_name).first
render json: { errors: "Table #{@username}.#{@table_name} not found" }, status: 404 unless @table
end
end
end
end
@@ -0,0 +1,22 @@
module Carto
module Api
class ExternalSourcePresenter
def initialize(external_source)
@external_source = external_source
end
def to_poro
return {} if @external_source.nil?
{
size: @external_source.size,
row_count: @external_source.rows_counted,
geometry_types: @external_source.geometry_types
}
end
end
end
end
@@ -0,0 +1,105 @@
module Carto
module Api
class GeocodingsController < ::Api::ApplicationController
GEOCODING_SQLAPI_CALLS_TIMEOUT = 45
ssl_required :index, :show, :available_geometries, :estimation_for
def index
# data_import_id is set for content guessing geocodes
# TODO: agree on a more flexible API with params
geocodings = Carto::Geocoding.where(
"user_id = ? AND (state NOT IN (?)) AND (data_import_id IS NULL)",
current_user.id, ['failed', 'finished', 'cancelled']
).all
render json: { geocodings: geocodings }, root: false
end
def show
geocoding = Carto::Geocoding.where(user_id: current_user.id, id: params[:id]).first
raise RecordNotFound unless geocoding
render json: geocoding.public_values
end
def available_geometries
case params[:kind]
when 'admin1'
return render(json: ['polygon'])
when 'namedplace'
return render(json: ['point'])
when 'postalcode'
return available_geometries_for_postalcode
else
return head(400)
end
end
def estimation_for
table = get_table(params[:table_name])
render_jsonp( { description: "table #{params[:table_name]} doesn't exist" }, 500) and return unless table
force_all_rows = (params[:force_all_rows].to_s == 'true')
total_rows = Carto::Geocoding.processable_rows(table, force_all_rows)
remaining_quota = uri_user.remaining_geocoding_quota
remaining_quota = (remaining_quota > 0 ? remaining_quota : 0)
used_credits = total_rows - remaining_quota
used_credits = (used_credits > 0 ? used_credits : 0)
render json: {
rows: total_rows,
estimation: (uri_user.geocoding_block_price.to_i * used_credits) / Carto::User::GEOCODING_BLOCK_SIZE.to_f
}
rescue => e
CartoDB.notify_exception(e, params: params)
render_jsonp( { description: e.message }, 500)
end
private
# TODO: this should be moved upwards in the controller hierarchy, and make it a replacement for current_user
def uri_user
@uri_user ||= Carto::User.where(id: current_user.id).first
end
def available_geometries_for_postalcode
return head(400) unless params[:free_text] || (params[:column_name] && params[:table_name])
input = params[:free_text].present? ? clean_free_text_input([params[:free_text]]) : select_distinct_from_table_and_column(params[:table_name], params[:column_name])
return head(400) if input.nil? && params[:table_name].present?
render(json: []) and return if input.nil? || input.empty?
list = input.map { |v| "'#{v.to_s.gsub("'", "''")}'" }.join(",")
internal_geocoder_api_config = CartoDB::GeocoderConfig.instance.get['internal']
.symbolize_keys
.merge(timeout: GEOCODING_SQLAPI_CALLS_TIMEOUT)
services = CartoDB::SQLApi.new(internal_geocoder_api_config)
.fetch("SELECT (admin0_available_services(Array[#{list}])).*")
geometries = []
points = services.select { |s| s['postal_code_points'] }.size
polygons = services.select { |s| s['postal_code_polygons'] }.size
geometries.append 'point' if points > 0 && points >= polygons
geometries.append 'polygon' if polygons > 0 && polygons >= points
render(json: geometries)
rescue => e
CartoDB.notify_exception(e, params: params)
end
def clean_free_text_input(input)
input.map{ |v| v.to_s.squish.downcase.gsub(/[^\p{Alnum}]/, '') }.reject(&:blank?)
end
def select_distinct_from_table_and_column(table_name, column_name)
table = get_table(table_name)
return nil if table.nil?
input = table.sequel.distinct.select_map(params[:column_name].to_sym)
end
def get_table(table_name)
Carto::Helpers::TableLocator.new.get_by_id_or_name(table_name, uri_user).try(&:service)
end
end
end
end
@@ -0,0 +1,36 @@
module Carto
module Api
class GrantablePresenter
def initialize(grantable)
@grantable = grantable
end
def to_poro
{
id: @grantable.id,
type: @grantable.type,
name: @grantable.name,
avatar_url: @grantable.avatar_url,
model: model_presenter.to_poro
}
end
private
def model_presenter
case @grantable.type
when 'user'
Carto::Api::UserPresenter.new(Carto::User.find(@grantable.id), fetch_groups: true)
when 'group'
Carto::Api::GroupPresenter.new(Carto::Group.find(@grantable.id), fetch_users: true)
else
raise "Unknown grantable type #{@grantable.type}"
end
end
end
end
end
@@ -0,0 +1,50 @@
require_dependency 'cartodb/errors'
module Carto
module Api
class GrantablesController < ::Api::ApplicationController
include PagedSearcher
include Carto::ControllerHelper
respond_to :json
ssl_required :index
before_filter :load_organization
VALID_ORDER_PARAMS = [:id, :name, :type, :avatar_url, :organization_id, :updated_at].freeze
def index
page, per_page, order, _order_direction = page_per_page_order_params(VALID_ORDER_PARAMS)
query = params[:q]
grantable_query = Carto::GrantableQueryBuilder.new(@organization).with_filter(query)
grantables = grantable_query.run(page, per_page, order)
total_entries = grantable_query.count
render_jsonp({
grantables: grantables.map { |g| Carto::Api::GrantablePresenter.new(g).to_poro },
total_entries: total_entries
}, 200)
rescue Carto::ParamInvalidError => e
render json: { errors: e.message }, status: e.status
rescue StandardError => e
CartoDB.notify_exception(e, { params: params })
render json: { errors: e.message }, status: 500
end
private
def load_organization
@organization = Carto::Organization.where(id: params['organization_id']).first
render json: { errors: "Organization #{params['organization_id']} not found" }, status: 404 unless @organization
render json: { errors: "You don't belong to organization #{params['organization_id']}" }, status: 400 unless current_user.organization_id == @organization.id
end
end
end
end
@@ -0,0 +1,67 @@
require_relative 'user_presenter'
module Carto
module Api
class GroupPresenter
FULL_FETCH_OPTIONS = {
fetch_shared_tables_count: true,
fetch_shared_maps_count: true,
fetch_users: true
}
def self.full(group)
Carto::Api::GroupPresenter.new(group, FULL_FETCH_OPTIONS)
end
# Available fetching options:
# - fetch_shared_tables_count
# - fetch_shared_maps_count
# - fetch_users
def initialize(group, fetching_options = {})
@group = group
@fetching_options = fetching_options
end
def to_poro
poro = {
id: @group.id,
organization_id: @group.organization_id,
name: @group.name,
display_name: @group.display_name
}
if @fetching_options[:fetch_shared_tables_count] == true
poro.merge!({ shared_tables_count: shared_tables_count })
end
if @fetching_options[:fetch_shared_maps_count] == true
poro.merge!({ shared_maps_count: shared_maps_count })
end
if @fetching_options[:fetch_users] == true
poro.merge!({ users: users })
end
poro
end
private
def shared_tables_count
shared_visualizations_query.where(:visualizations => { type: Carto::Visualization::TYPE_CANONICAL }).count
end
def shared_maps_count
shared_visualizations_query.where(:visualizations => { type: Carto::Visualization::TYPE_DERIVED }).count
end
def shared_visualizations_query
Carto::SharedEntity.where(recipient_id: @group.id).joins(:visualization)
end
def users
@group.users.map { |u| Carto::Api::UserPresenter.new(u, { fetch_groups: false } ).to_poro }
end
end
end
end
@@ -0,0 +1,178 @@
require_relative 'paged_searcher'
require_dependency 'cartodb/errors'
module Carto
module Api
class GroupsController < ::Api::ApplicationController
include PagedSearcher
include Carto::ControllerHelper
ssl_required :index, :show, :create, :update, :destroy, :add_users, :remove_users
before_filter :load_fetching_options, only: [:show, :index]
before_filter :load_organization
before_filter :load_user
before_filter :validate_organization_or_user_loaded
before_filter :load_group, only: [:show, :update, :destroy, :add_users, :remove_users]
before_filter :org_admin_only, only: [:create, :update, :destroy, :add_users, :remove_users]
before_filter :org_users_only, only: [:show, :index]
before_filter :load_organization_users, only: [:add_users, :remove_users]
before_filter :valid_password_confirmation, only: [:destroy, :add_users, :remove_users]
rescue_from Carto::ParamInvalidError, with: :rescue_from_carto_error
rescue_from Carto::PasswordConfirmationError, with: :rescue_from_password_confirmation_error
VALID_ORDER_PARAMS = [:id, :name, :display_name, :organization_id, :updated_at].freeze
def index
page, per_page, order, _order_direction = page_per_page_order_params(VALID_ORDER_PARAMS)
groups = @user ? @user.groups : @organization.groups
groups = groups.where('name ilike ?', "%#{params[:q]}%") if params[:q]
total_entries = groups.count
groups = Carto::PagedModel.paged_association(groups, page, per_page, order)
render_jsonp({
groups: groups.map { |g| Carto::Api::GroupPresenter.new(g, @fetching_options).to_poro },
total_entries: total_entries
}, 200)
end
def show
render_jsonp(Carto::Api::GroupPresenter.new(@group, @fetching_options).to_poro, 200)
end
def create
group = @organization.create_group(params['display_name'])
render_jsonp(Carto::Api::GroupPresenter.full(group).to_poro, 200)
rescue CartoDB::ModelAlreadyExistsError => e
CartoDB::Logger.debug(message: 'Group already exists', exception: e, params: params)
render json: { errors: ["A group with that name already exists"] }, status: 409
rescue ActiveRecord::StatementInvalid => e
handle_statement_invalid_error(e, group)
rescue => e
CartoDB::Logger.error(exception: e, params: params, group: group || 'no group', organization: @organization)
render json: { errors: [e.message] }, status: 500
end
def update
@group.rename_group_with_extension(params['display_name'])
render_jsonp(Carto::Api::GroupPresenter.full(@group).to_poro, 200)
rescue CartoDB::ModelAlreadyExistsError => e
CartoDB::Logger.debug(message: 'Group display name already exists', params: params)
render json: { errors: ["A group with that name already exists"] }, status: 409
rescue ActiveRecord::StatementInvalid => e
handle_statement_invalid_error(e, @group)
rescue => e
CartoDB::Logger.error(exception: e, params: params, group: @group)
render json: { errors: [e.message] }, status: 500
end
def destroy
@group.destroy_group_with_extension
render json: {}, status: 204
rescue ActiveRecord::StatementInvalid => e
handle_statement_invalid_error(e, @group)
rescue => e
CartoDB::Logger.error(exception: e, params: params, group: @group)
render json: { errors: [e.message] }, status: 500
end
def add_users
@group.add_users_with_extension(@organization_users)
render json: {}, status: 200
rescue ActiveRecord::StatementInvalid => e
handle_statement_invalid_error(e, @group)
rescue => e
CartoDB::Logger.error(exception: e, user: @user, params: params, group: @group)
render json: { errors: [e.message] }, status: 500
end
def remove_users
@group.remove_users_with_extension(@organization_users)
render json: {}, status: 200
rescue ActiveRecord::StatementInvalid => e
handle_statement_invalid_error(e, @group)
rescue => e
CartoDB::Logger.error(exception: e, user: @user, params: params, group: @group)
render json: { errors: [e.message] }, status: 500
end
private
def load_fetching_options
@fetching_options = {
fetch_shared_tables_count: params[:fetch_shared_tables_count] == 'true',
fetch_shared_maps_count: params[:fetch_shared_maps_count] == 'true',
fetch_users: params[:fetch_users] == 'true'
}
end
def load_organization
return unless params['organization_id'].present?
@organization = Carto::Organization.where(id: params['organization_id']).first
render json: { errors: ["Org. #{params['organization_id']} not found"] }, status: 404 unless @organization
end
def load_user
return unless params['user_id'].present?
@user = Carto::User.where(id: params['user_id']).first
render json: { errors: ["User #{params['user_id']} not found"] }, status: 404 unless @user
if @organization.nil?
@organization = @user.organization
elsif @user.organization_id != @organization.id
render json: { errors: ["You can't get other organization users"] }, status: 501
end
unless @user.id == current_user.id || current_user.organization_admin?
render json: { errors: ["You can't get other users groups"] }, status: 501
end
end
def validate_organization_or_user_loaded
render json: { errors: ["You must set user_id or organization_id"] }, status: 404 unless @organization || @user
end
def org_users_only
unless @organization.id == current_user.organization_id
render json: { errors: ["Not organization user"] }, status: 400
end
end
def org_admin_only
unless @organization.admin?(current_user)
render json: { errors: ["Not org. admin"] }, status: 400
end
end
def load_group
@group = @organization.groups.where(id: params['group_id']).first
render json: { errors: ["Group #{params['group_id']} not found"] }, status: 404 unless @group
end
def load_organization_users
ids = params['users'].present? ? params['users'] : [ params['user_id'] ]
@organization_users = ids.map { |id| @organization.users.where(id: id).first }
render json: { errors: ["Users #{ids} not found"] }, status: 404 if @organization_users.empty?
end
def handle_statement_invalid_error(e, group)
err_regexp = /ERROR: (.*)\n/
if e.message =~ err_regexp
render json: { errors: [err_regexp.match(e.message)[1]] }, status: 422
else
CartoDB::Logger.error(exception: e, params: params, group: group || 'no group', organization: @organization)
render json: { errors: [e.message] }, status: 500
end
end
def rescue_from_password_confirmation_error(error)
render_jsonp({ message: "Error modifying groups", errors: [error.message] }, 403)
end
end
end
end
@@ -0,0 +1,26 @@
module Carto
module Api
class ImageProxyController < ::Api::ApplicationController
ssl_required :show
def show
# image url
url = params[:url]
render :status => 404 if url.blank?
# fetch the image
http_client = Carto::Http::Client.get('image_proxy', log_requests: true)
response = http_client.get(url, followlocation: true, timeout: 3)
if response.code == 200
send_data response.response_body, type: response.headers['Content-Type'], disposition: 'inline'
else
render :status => 404
end
end
end
end
end
@@ -0,0 +1,118 @@
module Carto
module Api
class ImportsController < ::Api::ApplicationController
ssl_required :index, :show
ssl_allowed :service_token_valid?, :list_files_for_service, :get_service_auth_url, :validate_service_oauth_code,
:service_oauth_callback
def index
imports = DataImportsService.new.process_recent_user_imports(current_user)
render json: { imports: imports.map(&:id), success: true }
end
def show
import = DataImportsService.new.process_by_id(params[:id])
render_404 and return if import.nil?
data = Carto::Api::DataImportPresenter.new(import).api_public_values
if import.state == Carto::DataImport::STATE_COMPLETE
data[:any_table_raster] = import.is_raster?
decorate_twitter_import_data!(data, import)
decorate_default_visualization_data!(data, import)
end
render json: data
end
def service_token_valid?
valid = DataImportsService.new.validate_synchronization_oauth(uri_user, params[:id])
render_jsonp({ oauth_valid: valid, success: true })
rescue CartoDB::Datasources::TokenExpiredOrInvalidError => e
CartoDB.notify_exception(e, { user: uri_user, params: params })
render_jsonp({ errors: e.message }, 401)
rescue => e
CartoDB.notify_exception(e, { user: uri_user, params: params })
render_jsonp({ errors: e.message }, 400)
end
def list_files_for_service
filter = params[:filter].present? ? params[:filter] : []
results = DataImportsService.new.get_service_files(uri_user, params[:id], filter)
render_jsonp({ files: results, success: true })
rescue CartoDB::Datasources::TokenExpiredOrInvalidError => e
CartoDB.notify_exception(e, { user: uri_user, params: params })
render_jsonp({ errors: e.message }, 401)
rescue => e
CartoDB.notify_exception(e, { user: uri_user, params: params })
render_jsonp({ errors: { imports: e.message } }, 400)
end
def get_service_auth_url
auth_url = DataImportsService.new.get_service_auth_url(uri_user, params[:id])
render_jsonp({ url: auth_url, success: true})
rescue CartoDB::Datasources::TokenExpiredOrInvalidError => e
CartoDB.notify_exception(e, { user: uri_user, params: params })
render_jsonp({ errors: e.message }, 401)
rescue => e
CartoDB.notify_exception(e, { user: uri_user, params: params })
render_jsonp({ errors: { imports: e.message } }, 400)
end
def validate_service_oauth_code
success = DataImportsService.new.validate_service_oauth_code(uri_user, params[:id], params[:code])
render_jsonp({ success: success })
rescue CartoDB::Datasources::TokenExpiredOrInvalidError => e
CartoDB.notify_exception(e, { user: uri_user, params: params })
render_jsonp({ errors: e.message }, 401)
rescue => e
CartoDB.notify_exception(e, { user: uri_user, params: params })
render_jsonp({ errors: { imports: e.message } }, 400)
end
def service_oauth_callback
DataImportsService.new.validate_callback(uri_user, params[:id], params)
request.format = 'html'
respond_to do |format|
format.all { render text: '<script>window.close();</script>', content_type: 'text/html' }
end
rescue CartoDB::Datasources::TokenExpiredOrInvalidError => e
CartoDB::Logger.warning(message: "Expired oauth token", exception: e, user: uri_user, params: params)
render text: 'Expired token. Try reconnecting<script>setTimeout(function(){window.close()}, 1000);</script>',
content_type: 'text/html', status: 401
rescue => e
CartoDB::Logger.warning(message: "Error in oauth callback", exception: e, user: uri_user, params: params)
render text: 'Connection failed<script>setTimeout(function(){window.close()}, 1000);</script>',
content_type: 'text/html', status: 400
end
private
# TODO: this should be moved upwards in the controller hierarchy, and make it a replacement for current_user
def uri_user
@uri_user ||= Carto::User.where(id: current_user.id).first
end
def decorate_twitter_import_data!(data, data_import)
return if data_import.service_name != CartoDB::Datasources::Search::Twitter::DATASOURCE_NAME
audit_entry = ::SearchTweet.where(data_import_id: data_import.id).first
data[:tweets_georeferenced] = audit_entry.retrieved_items
data[:tweets_cost] = audit_entry.price
data[:tweets_overquota] = audit_entry.user.remaining_twitter_quota == 0
end
def decorate_default_visualization_data!(data, data_import)
derived_vis_id = nil
if data_import.create_visualization && !data_import.visualization_id.nil?
derived_vis_id = data_import.visualization_id
end
data[:derived_visualization_id] = derived_vis_id
end
end
end
end
@@ -0,0 +1,97 @@
module Carto
module Api
module InfowindowMigrator
MUSTACHE_ROOT_PATH = 'lib/assets/javascripts/builder/mustache-templates'.freeze
def migrate_builder_infowindow(templated_element, mustache_dir: 'infowindows')
return nil if templated_element.nil?
template = templated_element['template']
return templated_element if template.present?
templated_sym = templated_element.deep_symbolize_keys
old_template_name = templated_sym[:template_name]
return templated_element if old_template_name == 'none'
fields = templated_element['fields']
unless fields.present?
templated_element['template_name'] = 'none'
return templated_element
end
if MIGRATED_TEMPLATES.include?(old_template_name)
new_template_name = 'infowindow_color'
fixed_color = extract_color_from_old_template(old_template_name)
template_content_path = "#{MUSTACHE_ROOT_PATH}/#{mustache_dir}/infowindow_color.jst.mustache"
templated_element[:template] = get_template(
new_template_name,
templated_sym[:template],
template_content_path).gsub('#35AAE5', fixed_color)
templated_element[:headerColor] = {
color: {
opacity: 1,
fixed: fixed_color
}
}
else
new_template_name = parse_old_template_name(old_template_name)
templated_element[:template] = get_template(
old_template_name,
templated_sym[:template],
"#{MUSTACHE_ROOT_PATH}/#{mustache_dir}/#{get_template_name(old_template_name)}.jst.mustache")
end
templated_element[:template_name] = new_template_name
templated_element
end
private
INFOWINDOW_COLOR_TEMPLATE = 'infowindow_color'.freeze
MIGRATED_TEMPLATES = %w{ infowindow_light_header_blue infowindow_light_header_yellow
infowindow_light_header_orange infowindow_light_header_green }.freeze
COLOR_MAP = {
'blue' => '#35AAE5',
'green' => '#7FC97F',
'orange' => '#E68165',
'yellow' => '#E5C13D'
}.freeze
def extract_color_from_old_template(old_template_name)
COLOR_MAP[old_template_name.split('_').last]
end
def parse_old_template_name(old_template_name)
Pathname.new(old_template_name).basename.to_s
rescue => exception
CartoDB::Logger.error(message: "#{self.class}: Error parsing template",
exception: exception,
parsing: old_template_name)
old_template_name
end
def get_template_name(name)
Carto::Layer::TEMPLATES_MAP.fetch(name, name)
end
def get_template(template_name, fallback_template, template_path)
if template_name.present?
path = Rails.root.join(template_path)
File.read(path)
else
fallback_template
end
end
end
end
end
@@ -0,0 +1,22 @@
module Carto
module Api
class InvitationPresenter
def initialize(invitation)
@invitation = invitation
end
def to_poro
{
id: @invitation.id,
users_emails: @invitation.users_emails,
welcome_text: @invitation.welcome_text,
viewer: @invitation.viewer,
created_at: @invitation.created_at,
updated_at: @invitation.updated_at
}
end
end
end
end
@@ -0,0 +1,40 @@
module Carto
module Api
class InvitationsController < ::Api::ApplicationController
ssl_required :create
before_filter :load_organization
def create
@organization.update_attribute(:auth_username_password_enabled, true) if params[:enable_organization_signup] === true
invitation = Carto::Invitation.create_new(
Carto::User.find(current_user.id),
params[:users_emails],
params[:welcome_text],
params[:viewer]
)
if invitation.valid?
render_jsonp(Carto::Api::InvitationPresenter.new(invitation).to_poro)
else
render json: { errors: invitation.errors }, status: 400
end
rescue => e
CartoDB.notify_exception(e, params: params , invitation: (invitation ? invitation : 'not created'))
render json: { errors: e.message }, status: 500
end
private
def load_organization
@organization = Carto::Organization.where(id: params[:organization_id]).first
render_404 && return unless @organization
unless @organization.admin?(current_user)
render_jsonp({ errors: { organization: 'not admin' } }, 401) && return
end
end
end
end
end
@@ -0,0 +1,839 @@
require_dependency 'carto/api/infowindow_migrator'
require_dependency 'carto/table_utils'
module Carto
module Api
class LayerPresenter
include InfowindowMigrator
include Carto::TableUtils
PUBLIC_VALUES = %W{ options kind infowindow tooltip id order }
# CSS is not stored by default, only when sent by frontend,
# so this is returned whenever a layer that needs CSS but has none is requestesd
EMPTY_CSS = '#dummy{}'
TORQUE_ATTRS = %w(
table_name
user_name
property
blendmode
resolution
countby
torque-duration
torque-steps
torque-blend-mode
query
tile_style
named_map
visible
)
INFOWINDOW_KEYS = %w(
fields template_name template alternative_names width maxHeight
)
# Options:
# - viewer_user
# - user: owner user
# - with_style_properties: request style_properties generation if needed or not. Default: false.
def initialize(layer, options = {}, configuration = {}, decoration_data = {})
@layer = layer
@options = options
@configuration = configuration
@decoration_data = decoration_data
@viewer_user = options.fetch(:viewer_user, nil)
@owner_user = options.fetch(:user, nil)
@with_style_properties = options.fetch(:with_style_properties, false)
end
def to_poro(migrate_builder_infowindows: false)
poro = base_poro(@layer)
if migrate_builder_infowindows
if poro['infowindow'].present?
poro['infowindow'] = migrate_builder_infowindow(poro['infowindow'])
end
if poro['tooltip'].present?
poro['tooltip'] = migrate_builder_infowindow(poro['tooltip'], mustache_dir: 'tooltips')
end
end
poro
end
def to_json
public_values(@layer).to_json
end
def to_embed_poro
{
id: @layer.id,
options: @layer.options.select { |k| ['tile_style', 'style_version'].include?(k) }
}
end
def to_vizjson_v2
if base?(@layer)
with_kind_as_type(base_poro(@layer)).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: public_values(@layer).symbolize_keys[:options]['visible'],
options: options_data_v2
}
end
end
def to_vizjson_v1
return base_poro(@layer).symbolize_keys if base?(@layer)
{
id: @layer.id,
kind: 'CartoDB',
infowindow: infowindow_data_v1,
order: @layer.order,
options: options_data_v1
}
end
private
def viewer_is_owner?
return (@owner_user.id == @viewer_user.id) if (@owner_user && @viewer_user)
# This can be removed if 'user_name' support is dropped
layer_opts = @layer.options.nil? ? Hash.new : @layer.options
if @viewer_user && layer_opts['user_name'] && layer_opts['table_name']
@viewer_user.username == layer_opts['user_name']
else
true
end
end
# INFO: Assumes table_name needs to always be qualified, don't call if doesn't
def qualify_table_name
layer_opts = @layer.options.nil? ? Hash.new : @layer.options
# 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
return layer_opts['table_name'] if layer_opts['table_name'].include?('.')
if @owner_user && @viewer_user
@layer.qualified_table_name(@owner_user)
else
# TODO: Legacy support: Remove 'user_name' and use always :viewer_user and :user
user_name = layer_opts['user_name']
if user_name.include?('-')
"\"#{layer_opts['user_name']}\".#{safe_table_name_quoting(layer_opts['table_name'])}"
else
"#{layer_opts['user_name']}.#{safe_table_name_quoting(layer_opts['table_name'])}"
end
end
end
def base_poro(layer)
# .merge left for backwards compatibility
public_values(layer).merge('options' => layer_options)
end
def public_values(layer)
Hash[ PUBLIC_VALUES.map { |attribute| [attribute, layer.send(attribute)] } ]
end
# 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_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[:template] = File.read(path)
infowindow
end
def layer_options
layer_opts = @layer.options.nil? ? Hash.new : @layer.options
if layer_opts['table_name'] && !viewer_is_owner?
layer_opts['table_name'] = qualify_table_name
end
if @with_style_properties &&
(layer_opts['style_properties'].nil? || layer_opts['style_properties']['autogenerated'] == true)
StylePropertiesGenerator.new(@layer).migrate(layer_opts)
end
layer_opts
end
def options_data_v1
return @layer.options if @options[:full]
@layer.options.select { |key, value| public_options.include?(key.to_s) }
end
def options_data_v2
if @options[:full]
decorate_with_data(@layer.options, @decoration_data)
else
sql = sql_from(@layer.options)
data = {
sql: wrap(sql, @layer.options),
layer_name: name_for(@layer),
cartocss: css_from(@layer.options),
cartocss_version: @layer.options.fetch('style_version'), # Mandatory
interactivity: @layer.options.fetch('interactivity') # Mandatory
}
data = decorate_with_data(data, @decoration_data)
if @viewer_user
if @layer.options['table_name'] && !viewer_is_owner?
data['table_name'] = qualify_table_name
end
end
data
end
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: 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 name_for(layer)
layer_alias = layer.options.fetch('table_name_alias', nil)
table_name = layer.options['table_name']
return table_name unless layer_alias && !layer_alias.empty?
layer_alias
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 : 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 viewer_is_owner?
"select * from #{safe_table_name_quoting(layer_options['table_name'])}"
else
"select * from #{qualify_table_name}"
end
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
end
# Used to migrate `wizard_properties` to `style_properties`
class StylePropertiesGenerator
def initialize(layer)
@layer = layer
@wizard_properties = @layer.options['wizard_properties']
@source_type = @wizard_properties.present? ? @wizard_properties['type'] : nil
end
def migrate(options)
return nil unless @wizard_properties.present?
wpp = @wizard_properties['properties']
type = if @source_type == 'density'
wpp['geometry_type'] == 'Rectangles' ? 'squares' : 'hexabins'
elsif @source_type == 'torque_heat'
wpp['heat-animated'] ? 'animation' : 'heatmap'
else
STYLE_PROPERTIES_TYPE[@source_type]
end
return nil unless type
options['cartocss_custom'] = options['tile_style_custom'] || false
options['cartocss_history'] = options['tile_style_history'] || []
options['style_properties'] = {
'autogenerated' => true,
'type' => type,
'properties' => wizard_properties_properties_to_style_properties_properties(wpp, type)
}
merge_into_if_present(options['style_properties']['properties'], 'aggregation', generate_aggregation(wpp))
if SOURCE_TYPES_WITH_SQL_WRAP.include?(@source_type)
options['sql_wrap'] = options['query_wrapper']
end
if @source_type == 'cluster'
options['cartocss_custom'] = true
end
if type == 'animation' && @layer.widgets.where(type: 'time-series').none?
create_time_series_widget(wpp)
end
end
private
STYLE_PROPERTIES_TYPE = {
'polygon' => 'simple',
'bubble' => 'simple',
'choropleth' => 'simple',
'category' => 'simple',
'torque' => 'animation',
'torque_cat' => 'animation',
'cluster' => 'simple'
}.freeze
SOURCE_TYPES_WITH_SQL_WRAP = ['cluster', 'density', 'torque_cat'].freeze
def set_if_present(hash, key, value)
# Dirty check because `false` is a valid `value`
hash[key] = value if value.present?
hash
end
def merge_into_if_present(hash, key, hash_value)
hash[key] = hash_value.deep_merge!(hash[key] || {}) if hash_value.present?
hash
end
def apply_direct_mapping(hash, original_hash, mapping)
mapping.each do |source, target|
set_if_present(hash, target, original_hash[source])
end
hash
end
def apply_default_opacity(hash)
if hash['fixed'] && !hash['opacity']
hash['opacity'] = 1
end
hash
end
PROPERTIES_DIRECT_MAPPING = {
"marker-comp-op" => "blending",
"torque-blend-mode" => "blending",
"line-comp-op" => "blending"
}.freeze
BLENDING_ALIAS = {
'source-over' => 'src-over'
}.freeze
ANIMATED_TYPES = ['animation', 'heatmap'].freeze
def wizard_properties_properties_to_style_properties_properties(wizard_properties_properties, type)
spp = {}
wpp = wizard_properties_properties
return spp unless wpp
apply_direct_mapping(spp, wpp, PROPERTIES_DIRECT_MAPPING)
if spp['blending'].blank?
spp['blending'] = 'none'
else
spp['blending'] = BLENDING_ALIAS.fetch(spp['blending'], spp['blending'])
end
merge_into_if_present(spp, 'stroke', generate_stroke(wpp))
merge_into_if_present(spp, drawing_property(wpp), generate_drawing_properties(wpp))
merge_into_if_present(spp, 'labels', generate_labels(wpp))
if ANIMATED_TYPES.include?(type)
merge_into_if_present(spp, 'animated', generate_animated(wpp))
end
set_animated_style(spp) if type == 'animation'
set_property(spp, wpp)
spp
end
def drawing_property(wpp)
wpp['geometry_type'] == 'line' ? 'stroke' : 'fill'
end
def set_property(spp, wpp)
return unless wpp['property']
drawing_property = drawing_property(wpp)
destination = case @source_type
when 'bubble'
spp[drawing_property]['size']
when 'choropleth', 'category'
spp[drawing_property]['color']
when 'torque', 'torque_heat', 'torque_cat'
spp['animated']
when 'density'
spp['aggregation']['value']
else
# Ignore some malformed wizards that have a property set even when the type does not support it
return
end
destination['attribute'] = wpp['property']
end
def set_animated_style(spp)
spp['style'] = @source_type == 'torque_heat' ? 'heatmap' : 'simple'
end
def generate_drawing_properties(wpp)
fill = {}
merge_into_if_present(fill, @source_type == 'bubble' ? 'size' : 'color', generate_dimension_properties(wpp))
case @source_type
when 'polygon', 'torque', 'torque_cat'
fill['size'] = { 'fixed' => wpp['marker-width'] }.merge(fill['size'] || {})
when 'choropleth', 'category'
fill['size'] = { 'fixed' => 10 }.merge(fill['size'] || {})
when 'torque_heat'
fill['size'] = { 'fixed' => 35 }.merge(fill['size'] || {})
end
merge_into_if_present(fill, 'color', generate_color(wpp))
fill
end
STROKE_FROM_POINT_MAPPING = {
'size' => {
"marker-line-width" => 'fixed'
},
'color' => {
"marker-line-color" => 'fixed',
"marker-line-opacity" => 'opacity'
}
}.freeze
STROKE_FROM_NON_POINT_MAPPING = {
'size' => {
"line-width" => 'fixed'
},
'color' => {
"line-color" => 'fixed',
"line-opacity" => 'opacity'
}
}.freeze
def generate_stroke(wpp)
stroke_mapping = if wpp['geometry_type'] == 'point' && @source_type != 'density'
STROKE_FROM_POINT_MAPPING
else
STROKE_FROM_NON_POINT_MAPPING
end
stroke = {}
merge_into_if_present(stroke, 'size', apply_direct_mapping({}, wpp, stroke_mapping['size']))
merge_into_if_present(stroke, 'color', apply_direct_mapping({}, wpp, stroke_mapping['color']))
stroke
end
TORQUE_HEAT_COLOR_DEFAULTS = {
'attribute' => 'points_agg',
'range' => ['blue', 'cyan', 'lightgreen', 'yellow', 'orange', 'red'],
'bins' => 6
}.freeze
def generate_color(wpp)
color = {}
%w(polygon marker).each do |prefix|
set_if_present(color, 'fixed', wpp["#{prefix}-fill"])
unless color['opacity']
set_if_present(color, 'opacity', wpp["#{prefix}-opacity"])
end
apply_default_opacity(color)
end
if wpp['categories'].present?
color['range'] = wpp['categories'].map { |c| c['color'] }
color['domain'] = wpp['categories'].map { |c| c['title'] }
end
color_attribute = if wpp['property_cat']
wpp['property_cat']
elsif @source_type == 'density'
'agg_value'
end
color['attribute'] = color_attribute if color_attribute
color.merge!(TORQUE_HEAT_COLOR_DEFAULTS) if @source_type == 'torque_heat'
color
end
SIZE_DIRECT_MAPPING = {
'qfunction' => 'quantification'
}.freeze
QUANTIFICATION_MAPPING = {
'Jenks' => 'jenks',
'Equal Interval' => 'equal',
'Heads/Tails' => 'headtails',
'Quantile' => 'quantiles'
}.freeze
COLOR_RANGE_SOURCE_TYPES = ['choropleth', 'density'].freeze
def generate_dimension_properties(wpp)
size = {}
radius_min = wpp['radius_min']
radius_max = wpp['radius_max']
if radius_min && radius_max
size['range'] = [radius_min, radius_max]
end
apply_direct_mapping(size, wpp, SIZE_DIRECT_MAPPING)
quantification = size['quantification']
size['quantification'] = QUANTIFICATION_MAPPING.fetch(quantification, quantification) if quantification.present?
if %w{ bubble category }.include?(@source_type)
size['bins'] = 10
end
if COLOR_RANGE_SOURCE_TYPES.include?(@source_type)
# Commented because it might not be definitive
# size['range'] = colorbrewer_ramp_array_from_color_ramp(wpp['color_ramp'])
size['range'] = wpp['color_ramp']
size['bins'] = extract_bins_from_method(wpp['method']).to_i
end
size
end
ANIMATED_DIRECT_MAPPING = {
'torque-cumulative' => 'overlap',
'torque-duration' => 'duration',
'torque-frame-count' => 'steps',
'torque-resolution' => 'resolution',
'torque-trails' => 'trails'
}.freeze
DEFAULT_ANIMATED = {
'attribute' => nil,
'overlap' => false,
'duration' => 30,
'steps' => 256,
'resolution' => 2,
'trails' => 2
}.freeze
def generate_animated(wpp)
animated = {}
apply_direct_mapping(animated, wpp, ANIMATED_DIRECT_MAPPING)
DEFAULT_ANIMATED.merge(animated)
end
AGGREGATION_SOURCE_TYPES = %w{ density torque_heat }.freeze
def generate_aggregation(wpp)
return {} unless AGGREGATION_SOURCE_TYPES.include?(@source_type)
size = case @source_type
when 'density'
wpp['polygon-size'] || 100
when 'torque_heat'
wpp['torque-resolution']
else
raise "Unsupported source type for aggregation: #{@source_type}"
end
{
"size" => size,
"value" => {
"operator" => 'COUNT',
"attribute" => ''
}
}
end
# Taken from `lib/assets/javascripts/cartodb/models/color_ramps.js`
COLOR_ARRAYS_FROM_RAMPS = {
'pink' => "['#E7E1EF', '#C994C7', '#DD1C77']",
'red' => "['#FFEDA0', '#FEB24C', '#F03B20']",
'black' => "['#F0F0F0', '#BDBDBD', '#636363']",
'green' => "['#E5F5F9', '#99D8C9', '#2CA25F']",
'blue' => "['#EDF8B1', '#7FCDBB', '#2C7FB8']",
'inverted_pink' => "['#DD1C77','#C994C7','#E7E1EF']",
'inverted_red' => "['#F03B20','#FEB24C','#FFEDA0']",
'inverted_black' => "['#636363','#BDBDBD','#F0F0F0']",
'inverted_green' => "['#2CA25F','#99D8C9','#E5F5F9']",
'inverted_blue' => "['#2C7FB8','#7FCDBB','#EDF8B1']",
'spectrum1' => "['#1a9850', '#fff2cc', '#d73027']",
'spectrum2' => "['#0080ff', '#fff2cc', '#ff4d4d']",
'blue_states' => "['#ECF0F6', '#6182B5', '#43618F']",
'purple_states' => "['#F1E6F1', '#B379B3', '#8A4E8A']",
'red_states' => "['#F2D2D3', '#D4686C', '#C1373C']",
'inverted_blue_states' => "['#43618F', '#6182B5', '#ECF0F6']",
'inverted_purple_states' => "['#8A4E8A', '#B379B3', '#F1E6F1']",
'inverted_red_states' => "['#C1373C', '#D4686C', '#F2D2D3']"
}.freeze
def colorbrewer_ramp_array_from_color_ramp(ramp)
return [] unless ramp
COLOR_ARRAYS_FROM_RAMPS[ramp]
end
DEFAULT_BINS = 6
def extract_bins_from_method(method)
return DEFAULT_BINS unless method
number_match = method.match(/(\d*) Buckets/i)
number_match && number_match[1] ? number_match[1] : DEFAULT_BINS
end
TEXT_DIRECT_MAPPING = {
'text-name' => 'attribute',
'text-face-name' => 'font',
'text-dy' => 'offset',
'text-allow-overlap' => 'overlap',
'text-placement-type' => 'placement'
}.freeze
DEFAULT_LABELS = {
'enabled' => false,
'attribute' => nil,
'font' => 'DejaVu Sans Book',
'fill' => {
'size' => {
'fixed' => 10
},
'color' => {
'fixed' => '#000',
'opacity' => 1
}
},
'halo' => {
'size' => {
'fixed' => 1
},
'color' => {
'fixed' => '#111',
'opacity' => 1
}
},
'offset' => -10,
'overlap' => true,
'placement' => 'point'
}.freeze
def generate_labels(wpp)
labels = {}
apply_direct_mapping(labels, wpp, TEXT_DIRECT_MAPPING)
labels['attribute'] = nil if labels['attribute'].to_s.downcase == 'none'
merge_into_if_present(labels, 'fill', generate_labels_fill(wpp))
merge_into_if_present(labels, 'halo', generate_labels_halo(wpp))
labels['enabled'] = labels['attribute'].present?
DEFAULT_LABELS.merge(labels)
end
def generate_labels_fill(wpp)
labels_fill = {}
merge_into_if_present(labels_fill, 'size', generate_labels_fill_size(wpp))
merge_into_if_present(labels_fill, 'color', generate_labels_fill_color(wpp))
labels_fill
end
TEXT_SIZE_DIRECT_MAPPING = {
'text-size' => 'fixed'
}.freeze
TEXT_COLOR_DIRECT_MAPPING = {
'text-fill' => 'fixed',
'text-opacity' => 'opacity'
}.freeze
def generate_labels_fill_size(wpp)
size = {}
apply_direct_mapping(size, wpp, TEXT_SIZE_DIRECT_MAPPING)
size
end
def generate_labels_fill_color(wpp)
color = {}
apply_direct_mapping(color, wpp, TEXT_COLOR_DIRECT_MAPPING)
apply_default_opacity(color)
color
end
def generate_labels_halo(wpp)
labels_halo = {}
merge_into_if_present(labels_halo, 'size', generate_labels_halo_size(wpp))
merge_into_if_present(labels_halo, 'color', generate_labels_halo_color(wpp))
labels_halo
end
HALO_SIZE_DIRECT_MAPPING = {
'text-halo-radius' => 'fixed'
}.freeze
HALO_COLOR_DIRECT_MAPPING = {
'text-halo-fill' => 'fixed',
'text-halo-opacity' => 'opacity'
}.freeze
def generate_labels_halo_size(wpp)
size = {}
apply_direct_mapping(size, wpp, HALO_SIZE_DIRECT_MAPPING)
size
end
def generate_labels_halo_color(wpp)
color = {}
apply_direct_mapping(color, wpp, HALO_COLOR_DIRECT_MAPPING)
apply_default_opacity(color)
color
end
def create_time_series_widget(wpp)
if wpp['property'] && @layer.options[:source]
@layer.widgets.create(
type: 'time-series',
order: 0,
title: 'time_date__t',
options: {
column: wpp['property'],
bins: 256,
sync_on_data_change: true,
sync_on_bbox_change: true
},
source_id: @layer.options[:source]
)
end
end
end
end
end
@@ -0,0 +1,73 @@
module Carto
module Api
class LayerVizJSONAdapter
extend Forwardable
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'
}
delegate [:options, :kind, :id, :order, :legend, :user] => :layer
attr_reader :layer
def initialize(layer)
@layer = layer
end
def public_values
{
'options' => options,
# TODO: kind should be renamed to type
# rename once a new layer presenter is written. See CartoDB::LayerModule::Presenter#with_kind_as_type
# TODO: use symbols instead of strings
'kind' => kind,
'infowindow' => infowindow,
'tooltip' => tooltip,
'id' => id,
'order' => order
}
end
def get_presenter(options, configuration)
Carto::Api::LayerPresenter.new(self, options, configuration)
end
def infowindow
@layer.infowindow
end
def tooltip
@layer.tooltip
end
def infowindow_template_path
if infowindow.present? && infowindow['template_name'].present?
template_name = TEMPLATES_MAP.fetch(infowindow['template_name'], self.infowindow['template_name'])
Rails.root.join("lib/assets/javascripts/cartodb/table/views/infowindow/templates/#{template_name}.jst.mustache")
else
nil
end
end
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")
else
nil
end
end
end
end
end
@@ -0,0 +1,283 @@
require_dependency 'carto/uuidhelper'
module Carto
module Api
class LayersController < ::Api::ApplicationController
include Carto::ControllerHelper
ssl_required :show, :layers_by_map, :custom_layers_by_user, :map_index, :user_index, :map_show, :user_show,
:map_create, :user_create, :map_update, :user_update, :map_destroy, :user_destroy
before_filter :ensure_current_user, only: [:user_index, :user_show, :user_create, :user_update, :user_destroy]
before_filter :load_user_layer, only: [:user_show, :user_destroy]
before_filter :load_user_layers, only: [:user_update]
before_filter :load_map, only: [:map_index, :map_show, :map_create, :map_update, :map_destroy]
before_filter :ensure_writable_map, only: [:map_create, :map_update, :map_destroy]
before_filter :load_map_layer, only: [:map_show, :map_destroy]
before_filter :load_map_layers, only: [:map_update]
rescue_from LoadError,
UnprocesableEntityError,
UnauthorizedError, with: :rescue_from_carto_error
def map_index
index(@map.layers)
end
def user_index
index(@user.layers, owner: @user)
end
def map_show
show(@map.user)
end
def user_show
show(current_user)
end
def map_create
layer = Carto::Layer.new(layer_attributes(params))
validate_for_map(layer)
save_layer(layer) do
@map.layers << layer
@map.process_privacy_in(layer)
from_layer = Carto::Layer.where(id: params[:from_layer_id]).first if params[:from_layer_id]
from_letter = params[:from_letter]
update_layer_node_styles(layer, from_layer, from_letter)
end
end
def user_create
layer = Carto::Layer.new(layer_attributes(params))
save_layer(layer) { @user.layers << layer }
end
def map_update
update
end
def user_update
update
end
def map_destroy
destroy
end
def user_destroy
destroy
end
private
def validate_for_map(layer)
unless @map.can_add_layer?(current_user, layer)
raise UnprocesableEntityError.new('Cannot add more layers to this visualization')
end
unless @map.admits_layer?(layer)
raise UnprocesableEntityError.new('Cannot add more layers of this type')
end
table_name = layer.options['table_name']
user_name = layer.options['user_name']
if user_name.present?
table_name = user_name + '.' + table_name
end
if layer.data_layer?
table_visualization = Helpers::TableLocator.new.get_by_id_or_name(
table_name,
current_user
).visualization
unless table_visualization.has_read_permission?(current_user)
raise UnauthorizedError.new('You do not have permission in the layer you are trying to add')
end
end
end
def index(layers, owner: nil)
presented_layers = layers.map do |layer|
Carto::Api::LayerPresenter.new(layer, viewer_user: current_user, user: owner || owner_user(layer)).to_poro
end
render_jsonp layers: presented_layers, total_entries: presented_layers.size
end
def show(owner)
render_jsonp Carto::Api::LayerPresenter.new(@layer, viewer_user: current_user, user: owner).to_json
end
# Takes a block, executed after saving
def save_layer(layer)
if layer.save
yield
render_jsonp Carto::Api::LayerPresenter.new(layer, viewer_user: current_user).to_poro
else
CartoDB::Logger.error(message: 'Error creating layer', errors: layer.errors.full_messages)
raise UnprocesableEntityError.new(layer.errors.full_messages)
end
end
def update
layers = @layers.map do |layer|
layer_params = params[:layers].present? ? params[:layers].find { |p| p['id'] == layer.id } : params
# don't allow to override table_name and user_name
new_layer_options = layer_params[:options]
['table_name', 'user_name'].each do |key|
if layer.options.include?(key)
new_layer_options[key] = layer.options[key]
else
new_layer_options.delete(key)
end
end
unless layer.update_attributes(layer_attributes(layer_params))
raise UnprocesableEntityError.new(layer.errors.full_messages)
end
layer
end
if layers.count > 1
render_jsonp(layers: layers.map { |l| Carto::Api::LayerPresenter.new(l, viewer_user: current_user).to_poro })
else
render_jsonp Carto::Api::LayerPresenter.new(layers[0], viewer_user: current_user).to_poro
end
rescue RuntimeError => e
CartoDB::Logger.error(message: 'Error updating layer', exception: e)
render_jsonp({ description: e.message }, 400)
end
def destroy
@layer.destroy
head :no_content
end
def layer_attributes(param)
param.slice(:options, :kind, :infowindow, :tooltip, :order).permit!
end
def ensure_current_user
user_id = uuid_parameter(:user_id)
raise UnauthorizedError unless current_user.id == user_id
@user = Carto::User.find(user_id)
end
def load_user_layer
load_user_layers
raise LoadError.new('Layer not found') unless @layers.length == 1
@layer = @layers.first
end
def load_user_layers
@layers = layers_ids.map { |id| @user.layers.find(id) }
rescue ActiveRecord::RecordNotFound
raise LoadError.new('Layer not found')
end
def load_map
map_id = uuid_parameter(:map_id)
# User must be owner or have permissions for the map's visualization
@map = Carto::Map.find(map_id)
vis = @map.visualization
raise LoadError.new('Map not found') unless vis.try(:is_viewable_by_user?, current_user)
rescue ActiveRecord::RecordNotFound
raise LoadError.new('Map not found')
end
def ensure_writable_map
raise UnauthorizedError unless @map.visualization.writable_by?(current_user)
end
def load_map_layer
load_map_layers
raise LoadError.new('Layer not found') unless @layers.length == 1
@layer = @layers.first
end
def load_map_layers
@layers = layers_ids.map { |id| @map.layers.find(id) }
rescue ActiveRecord::RecordNotFound
raise LoadError.new('Layer not found')
end
def layers_ids
if params[:id]
[params[:id]]
elsif params[:layers]
params[:layers].map { |l| l['id'] }
else
raise LoadError.new('Layer not found')
end
end
def owner_user(layer)
if current_user.nil? || @map.user.id != current_user.id
# This keeps backwards compatibility with map user assignment. See #8974
@map.user
elsif layer.options && layer.options['user_name'].present?
::User.where(username: layer.options['user_name']).first
else
layer.user
end
end
def update_layer_node_styles(to_layer, from_layer, from_letter)
to_letter = to_layer.options['letter']
to_source = to_layer.options['source']
if from_layer.present? && from_letter.present? && to_letter.present? && to_source.present?
move_layer_node_styles(from_layer, from_letter, to_layer, to_letter, to_source)
update_source_layer_styles(from_layer, from_letter, to_letter, to_source)
end
rescue => e
CartoDB::Logger.error(
message: 'Error updating layer node styles',
exception: e,
from_layer: from_layer,
from_letter: from_letter,
to_layer: to_layer
)
end
def move_layer_node_styles(from_layer, from_letter, to_layer, to_letter, to_source)
source_node_number = to_source[1..-1].to_i
nodes_to_move = from_layer.layer_node_styles.select do |lns|
lns.source_id.starts_with?(from_letter) && lns.source_id[1..-1].to_i < source_node_number
end
nodes_to_move.each do |lns|
# Move LayerNodeStyles from the old layer if given.
lns.source_id = lns.source_id.gsub(from_letter, to_letter)
to_layer.layer_node_styles << lns
end
end
def update_source_layer_styles(from_layer, from_letter, to_letter, to_source)
if from_letter != to_letter
# Dragging middle node: rename the moved node
node_id_to_fix = to_source.gsub(to_letter, from_letter)
style_node = ::LayerNodeStyle.where(layer_id: from_layer.id, source_id: node_id_to_fix).first
if style_node
style_node.source_id = to_source
style_node.save
end
else
# Dragging head node: remove unneeded old styles in the old layer
from_layer.reload
from_layer.layer_node_styles.select { |lns|
lns.source_id.starts_with?(from_letter) && lns.source_id != to_source
}.each(&:destroy)
end
end
end
end
end
@@ -0,0 +1,24 @@
module Carto
module Api
class LegendPresenter
def initialize(legend)
@legend = legend
end
def to_hash
{
conf: @legend.conf,
created_at: @legend.created_at,
definition: @legend.definition,
id: @legend.id,
layer_id: @legend.layer_id,
post_html: @legend.post_html,
pre_html: @legend.pre_html,
title: @legend.title,
type: @legend.type,
updated_at: @legend.updated_at
}
end
end
end
end
@@ -0,0 +1,89 @@
module Carto
module Api
class LegendsController < ::Api::ApplicationController
include Carto::ControllerHelper
ssl_required :index, :show, :create, :update, :destroy
before_filter :load_layer,
:owners_only
before_filter :load_legend, only: [:show, :update, :destroy]
rescue_from Carto::LoadError,
Carto::UnauthorizedError,
Carto::UnprocesableEntityError, with: :rescue_from_carto_error
def index
legend_presentations = @layer.legends.map do |legend|
LegendPresenter.new(legend).to_hash
end
render_jsonp(legend_presentations, :ok)
end
def show
legend_presentation = LegendPresenter.new(@legend).to_hash
render_jsonp(legend_presentation, :ok)
end
def create
legend_params_with_layer_id = legend_params.merge(layer_id: @layer.id)
legend = Legend.create(legend_params_with_layer_id)
if legend.valid?
legend_presentation = LegendPresenter.new(legend).to_hash
render_jsonp(legend_presentation, :created)
else
error = legend.errors.full_messages.join(', ')
raise Carto::UnprocesableEntityError.new(error)
end
end
def update
@legend.update_attributes!(legend_params)
legend_presentation = LegendPresenter.new(@legend).to_hash
render_jsonp(legend_presentation, :ok)
rescue ActiveRecord::RecordInvalid
error = @legend.errors.full_messages.join(', ')
raise Carto::UnprocesableEntityError.new(error)
end
def destroy
@legend.destroy
render_jsonp({}, :no_content)
end
private
def load_layer
@layer = Carto::Layer.find(params[:layer_id])
rescue ActiveRecord::RecordNotFound
raise Carto::LoadError.new('Layer not found')
end
def owners_only
visualization = Carto::Visualization.find(params[:visualization_id])
unless visualization.layers.include?(@layer) &&
visualization.writable_by?(current_viewer)
raise Carto::UnauthorizedError.new
end
rescue ActiveRecord::RecordNotFound
raise Carto::LoadError.new('Visualization not found')
end
def load_legend
@legend = @layer.legends.find(params[:id])
rescue ActiveRecord::RecordNotFound
raise Carto::LoadError.new('Legend not found')
end
def legend_params
params.slice(:title, :pre_html, :post_html, :type, :definition, :conf).permit!
end
end
end
end
@@ -0,0 +1,26 @@
module Carto
module Api
class MapPresenter
def initialize(map)
@map = map
end
def to_hash
{
bounding_box_ne: @map.bounding_box_ne,
bounding_box_sw: @map.bounding_box_sw,
center: @map.center,
options: @map.options,
id: @map.id,
provider: @map.provider,
user_id: @map.user_id,
view_bounds_ne: @map.view_bounds_ne,
view_bounds_sw: @map.view_bounds_sw,
zoom: @map.zoom,
legends: @map.legends,
scrollwheel: @map.scrollwheel
}
end
end
end
end
@@ -0,0 +1,20 @@
module Carto
module Api
class MapcapPresenter
def initialize(mapcap)
@mapcap = mapcap
end
def to_poro
return {} unless @mapcap
{
id: @mapcap.id,
related_ids: @mapcap.ids_json,
created_at: @mapcap.created_at
}
end
end
end
end
@@ -0,0 +1,80 @@
require_relative '../builder/builder_users_module'
require_dependency 'carto/tracking/events'
module Carto
module Api
class MapcapsController < ::Api::ApplicationController
include Carto::ControllerHelper
include Carto::Builder::BuilderUsersModule
ssl_required :show, :create, :destroy, :index
before_filter :builder_users_only,
:load_visualization,
:owners_only
before_filter :load_mapcap, only: [:show, :destroy]
after_filter :track_published_map, only: :create
rescue_from StandardError, with: :rescue_from_standard_error
rescue_from Carto::LoadError,
Carto::UnauthorizedError,
Carto::UnprocesableEntityError, with: :rescue_from_carto_error
def index
render_jsonp(@visualization.mapcaps.map { |mapcap| Carto::Api::MapcapPresenter.new(mapcap).to_poro })
end
def create
mapcap = @visualization.create_mapcap!
render_jsonp(Carto::Api::MapcapPresenter.new(mapcap).to_poro, :created)
rescue ActiveRecord::RecordInvalid => exception
message = exception.record.errors.full_messages.join(', ')
raise Carto::UnprocesableEntityError.new(message)
end
def show
render_jsonp(Carto::Api::MapcapPresenter.new(@mapcap).to_poro)
end
def destroy
@mapcap.destroy
render_jsonp({}, :no_content)
end
private
def load_visualization
visualization_id = uuid_parameter(:visualization_id)
@visualization = Carto::Visualization.find(visualization_id)
raise ActiveRecord::RecordNotFound if @visualization.canonical?
rescue ActiveRecord::RecordNotFound
raise Carto::LoadError.new("Visualization not found: #{visualization_id}")
end
def owners_only
raise Carto::UnauthorizedError.new unless @visualization.writable_by?(current_user)
end
def load_mapcap
mapcap_id = uuid_parameter(:id)
@mapcap = Carto::Mapcap.find(mapcap_id)
rescue ActiveRecord::RecordNotFound
raise Carto::LoadError.new("Mapcap not found: #{mapcap_id}")
end
def track_published_map
current_viewer_id = current_viewer.id
Carto::Tracking::Events::PublishedMap.new(current_viewer_id,
user_id: current_viewer_id,
visualization_id: @visualization.id).report
end
end
end
end
@@ -0,0 +1,67 @@
module Carto
module Api
class MapsController < ::Api::ApplicationController
include Carto::ControllerHelper
ssl_required :show, :update
before_filter :load_map, :owners_only
rescue_from Carto::LoadError,
Carto::UnprocesableEntityError, with: :rescue_from_carto_error
def show
render_jsonp(map_presentation)
end
def update
@map.update_attributes!(update_params)
render_jsonp(map_presentation)
rescue ActiveRecord::RecordInvalid
validation_errors = @map.errors.full_messages.join(', ')
raise Carto::UnprocesableEntityError.new(validation_errors)
end
private
def load_map
@map = Carto::Map.find(params[:id])
rescue ActiveRecord::RecordNotFound
raise Carto::LoadError.new('Map not found')
end
def owners_only
unless @map.writable_by_user?(current_viewer)
raise Carto::LoadError.new('Map not found')
end
end
STRING_PARAMS = [:bounding_box_sw, :bounding_box_ne, :center, :view_bounds_sw, :view_bounds_ne].freeze
def update_params
update_params = params.slice(:bounding_box_ne,
:bounding_box_sw,
:center,
:options,
:provider,
:view_bounds_ne,
:view_bounds_sw,
:zoom,
:legends,
:scrollwheel).permit!
STRING_PARAMS.each do |param|
update_params[param] = update_params[param].to_s
end
# Remove empty values, keeping `false`s (`present?` can't be used because of this)
update_params.reject { |_k, v| v.nil? || v == '' }
end
def map_presentation
Carto::Api::MapPresenter.new(@map).to_hash
end
end
end
end
@@ -0,0 +1,38 @@
module Carto
module Api
class MetricsController < ::Api::ApplicationController
include Carto::ControllerHelper
ssl_required :create
skip_before_filter :api_authorization_required
before_filter :load_event, only: :create
rescue_from Carto::LoadError,
Carto::UnauthorizedError,
Carto::UnprocesableEntityError, with: :rescue_from_carto_error
def create
@event.report!
render json: Hash.new, status: :created
end
private
def load_event
event_name = params[:name]
raise Carto::UnprocesableEntityError.new('name not provided') unless event_name
modulized_name = "Carto::Tracking::Events::#{event_name.parameterize('_').camelize}"
@event = Carto::Tracking::Events::SegmentEvent.build(params[:name], current_viewer.try(:id), params[:properties])
@event ||= modulized_name.constantize.new(current_viewer.try(:id), params[:properties])
rescue NameError
raise Carto::LoadError.new("Event not found: #{event_name}")
end
end
end
end
@@ -0,0 +1,99 @@
module Carto
module Api
class MobileAppPresenter
def initialize(mobile_app, current_user, fetch_mobile_platforms, fetch_app_types)
@mobile_app = mobile_app
@current_user = current_user
@fetch_mobile_platforms = fetch_mobile_platforms
@fetch_app_types = fetch_app_types
end
def data
return {} if @mobile_app.nil?
data = {
id: @mobile_app.id,
name: @mobile_app.name,
description: @mobile_app.description,
icon_url: @mobile_app.icon_url,
platform: @mobile_app.platform,
app_type: @mobile_app.app_type,
app_id: @mobile_app.app_id,
license_key: @mobile_app.license_key,
monthly_users: @mobile_app.monthly_users
}
if @fetch_mobile_platforms == true
data[:mobile_platforms] = {
"android": {
text: "Android",
available: platform_available?('android', true),
selected: platform_selected?('android'),
legend: "Use package from AndroidManifest.xml. E.g: com.example.mycartoapp."
},
"ios": {
text: "iOS",
available: platform_available?('ios', true),
selected: platform_selected?('ios'),
legend: "Use Bundle identifier. You can find it in the project properties. E.g: com.example.mycartoapp."
},
"xamarin-android": {
text: "Xamarin Android",
available: platform_available?('xamarin-android', @current_user.mobile_xamarin),
selected: platform_selected?('xamarin-android'),
legend: "Use package from AndroidManifest.xml. E.g: com.example.mycartoapp."
},
"xamarin-ios": {
text: "Xamarin iOS",
available: platform_available?('xamarin-ios', @current_user.mobile_xamarin),
selected: platform_selected?('xamarin-ios'),
legend: "Use Bundle identifier. You can find it in the project properties. E.g: com.example.mycartoapp."
},
"windows-phone": {
text: "Windows Phone",
available: platform_available?('windows-phone', @current_user.mobile_xamarin),
selected: platform_selected?('windows-phone'),
legend: "Use the Package name from Package.appmanifest. E.g: c882d38a-5c09-4994-87f0-89875cdee539."
}
}
end
if @fetch_app_types == true
data[:app_types] = {
"open": {
text: "Limits based on your CARTO plan. <a href='https://carto.com/developers/fundamentals/limits/'
target='_blank'>Learn more</a>.",
available: app_type_available?('open', @current_user.open_apps_enabled?),
selected: app_type_selected?('open')
},
"private": {
text: "Only for enterprise. <a href='https://carto.com/pricing/' target='_blank'>Learn more</a>.",
available: app_type_available?('private', @current_user.private_apps_enabled?),
selected: app_type_selected?('private')
}
}
end
data
end
private
def platform_available?(platform, current_platform_enabled)
(!@mobile_app.persisted? && current_platform_enabled) || platform_selected?(platform)
end
def platform_selected?(platform)
@mobile_app.platform == platform
end
def app_type_available?(app_type, current_apps_enabled)
(!@mobile_app.persisted? && current_apps_enabled) || app_type_selected?(app_type) || (@mobile_app.persisted? && current_apps_enabled)
end
def app_type_selected?(app_type)
@mobile_app.app_type == app_type
end
end
end
end
@@ -0,0 +1,28 @@
module Carto
module Api
class MultifactorAuthPresenter
def initialize(multifactor_auth)
@multifactor_auth = multifactor_auth
end
def to_poro
return {} unless @multifactor_auth
{
id: @multifactor_auth.id,
user: @multifactor_auth.user.username,
type: @multifactor_auth.type,
enabled: @multifactor_auth.enabled,
created_at: @multifactor_auth.created_at.to_s,
updated_at: @multifactor_auth.updated_at.to_s
}
end
def to_poro_with_qrcode
to_poro.merge(
qrcode: @multifactor_auth.qr_code
)
end
end
end
end
@@ -0,0 +1,51 @@
module Carto
module Api
class MultifactorAuthenticationController < ::Api::ApplicationController
include Carto::ControllerHelper
extend Carto::DefaultRescueFroms
include OrganizationUsersHelper
ssl_required
before_action :load_organization
before_action :admins_only
before_action :load_user
before_action :create_service
before_action :ensure_edit_permissions
setup_default_rescues
def show
render_jsonp({ mfa_required: @service.exists?(type: base_params[:type]) }, 200)
end
def create
if @service.exists?(type: base_params[:type])
raise Carto::UnprocesableEntityError.new('Multi-factor authentication already exists')
end
@service.update(enabled: true, type: base_params[:type])
render_jsonp({ success: true }, 201)
end
def destroy
unless @service.exists?(type: base_params[:type])
raise Carto::UnprocesableEntityError.new('Multi-factor authentication does not exist')
end
@service.update(enabled: false, type: base_params[:type])
render_jsonp({ success: true }, 204)
end
private
def base_params
@base_params ||= params.permit([:type])
end
def create_service
@service = Carto::UserMultifactorAuthUpdateService.new(user_id: @user.id)
end
end
end
end
@@ -0,0 +1,60 @@
class Carto::Api::MultifactorAuthsController < ::Api::ApplicationController
include Carto::ControllerHelper
ssl_required
before_action :load_user
before_action :load_multifactor_auth, only: [:show, :verify_code, :destroy]
rescue_from Carto::UnprocesableEntityError, with: :rescue_from_carto_error
rescue_from Carto::UnauthorizedError, with: :rescue_from_carto_error
def create
multifactor_auth = @carto_viewer.user_multifactor_auths.create!(create_params)
render json: Carto::Api::MultifactorAuthPresenter.new(multifactor_auth).to_poro_with_qrcode, status: 201
rescue ActionController::ParameterMissing => e
raise Carto::UnprocesableEntityError.new(e.message)
rescue ActiveRecord::RecordInvalid => e
raise Carto::UnprocesableEntityError.new(e.message)
end
def verify_code
@multifactor_auth.verify!(params[:code])
render json: Carto::Api::MultifactorAuthPresenter.new(@multifactor_auth).to_poro, status: 200
rescue ActiveRecord::RecordInvalid => e
raise Carto::UnprocesableEntityError.new(e.message)
end
def destroy
@multifactor_auth.destroy
render json: Carto::Api::MultifactorAuthPresenter.new(@multifactor_auth).to_poro, status: 204
end
def show
render json: Carto::Api::MultifactorAuthPresenter.new(@multifactor_auth).to_poro, status: 200
end
def index
auths = @carto_viewer.user_multifactor_auths.map do |auth|
Carto::Api::MultifactorAuthPresenter.new(auth).to_poro
end
render json: auths, status: 200
end
private
def load_user
@carto_viewer = Carto::User.find(current_viewer.id)
end
def load_multifactor_auth
@multifactor_auth = @carto_viewer.user_multifactor_auths.find(params[:id])
end
def create_params
{ type: params.require(:type) }
end
end
@@ -0,0 +1,25 @@
class Carto::Api::NotificationPresenter
extend Forwardable
include Carto::HtmlSafe
delegate [:id, :icon, :recipients, :body, :created_at] => :@notification
def initialize(notification)
@notification = notification
end
def html_body
markdown_html_safe(@notification.body)
end
def to_hash
{
id: @notification.id,
organization_id: @notification.organization_id,
icon: @notification.icon,
recipients: @notification.recipients,
body: @notification.body,
created_at: @notification.created_at
}
end
end
@@ -0,0 +1,170 @@
require 'uri'
module Carto
module Api
class OembedController < ::Api::ApplicationController
include VisualizationSearcher
ssl_allowed :show
skip_before_filter :api_authorization_required
# Returns oembed data as required
def show
url = params[:url]
width = params[:maxwidth] || '100%'
height = params[:maxheight] || '520px'
format = request.query_parameters[:format]
force_https = true if params[:allow_http].nil?
raise ActionController::RoutingError.new('Incorrect width') if (width =~ /\A[0-9]+(%|px)?\z/).nil?
raise ActionController::RoutingError.new('Incorrect height') if (height =~ /\A[0-9]+(%|px)?\z/).nil?
uri = URI.parse(url)
uuid = extract_uuid_from_string(uri.path)
raise ActionController::RoutingError.new('UUID not found in URL') if uuid.nil?
vis = Carto::Visualization.where(id: @id).first
if !vis.nil?
name = vis.name
end
fields = url_fields_from_fragments(url, force_https)
# build the url using full schema because any visuaization should work with any user
if fields[:organization_name].nil?
url = CartoDB.base_url(fields[:username])
else
url = CartoDB.base_url(fields[:organization_name], fields[:username])
end
url += CartoDB.path(self, 'public_visualizations_embed_map', id: uuid, user_domain: nil)
# force the schema
if fields[:protocol] == 'https' && !url.include?('https')
url = url.sub('http', 'https')
end
html = "<iframe width='#{width}' height='#{height}' frameborder='0' src='#{url}' allowfullscreen webkitallowfullscreen mozallowfullscreen oallowfullscreen msallowfullscreen></iframe>"
response_data = {
:type => 'rich',
:version => '1.0',
:width => width,
:height => height,
:title => name,
:html => html,
:author_name => fields[:username],
:author_url => fields[:user_profile_url],
:provider_name => 'CartoDB',
:provider_url => "#{fields[:protocol]}://carto.com/"
}
if format == 'xml'
render xml: response_data.to_xml(root: 'oembed')
else
render json: response_data.to_json, :callback => params['callback']
end
end
private
def url_fields_from_fragments(url, force_https)
domain = CartoDB.session_domain
# @see http://ruby-doc.org/stdlib-1.9.3/libdoc/uri/rdoc/URI.html#method-c-split
url_fragments = URI.split(url)
protocol = force_https ? "https" : URI.parse(url).scheme
data = nil
unless CartoDB.subdomainless_urls?
begin
data = from_url(url_fragments, protocol, domain)
rescue UrlFRagmentsError
# URL is subdomainless so do nothing
end
end
# Either subdomains disallowed or url doesn't uses them
if data.nil?
data = from_domainless_url(url_fragments, protocol)
end
raise UrlFRagmentsError.new("Couldn't extract URL fields") if data.nil?
{
organization_name: data[:organization_name],
username: data[:username],
user_profile_url: data[:user_profile_url],
protocol: protocol
}
end
# testuser.carto.com || testorg.carto.com/u/user
def from_url(url_fragments, protocol, domain)
# To ease testing don't request eactly all URI.split params
raise UrlFRagmentsError.new("Invalid url_fragments parameter") unless url_fragments.length > 5
subdomain = url_fragments[2].sub(domain, '.').split('.')[0]
raise UrlFRagmentsError.new("Subdomain not found at url") if subdomain.nil?
# org-based
if url_fragments[5][0..2] == "/u/"
organization_name = subdomain
username = username_from_url_fragments(url_fragments)
else
organization_name = nil
username = subdomain
end
{
username: username,
organization_name: organization_name,
user_profile_url: CartoDB.base_url(subdomain, organization_name.nil? ? nil : username, protocol)
}
end
# https://carto.com/u/testuser/...
def from_domainless_url(url_fragments, protocol)
# To ease testing don't request eactly all URI.split params
raise UrlFRagmentsError.new("Invalid url_fragments parameter") unless url_fragments.length > 5
# url_fragments[5]: Path
raise UrlFRagmentsError.new("URL needs username specified in the Path") if url_fragments[5][0..2] != "/u/"
# url_fragments[3]: Host
port_fragment =
url_fragments[3].nil? || url_fragments[3] == '' || url_fragments[3].to_i == 80 ? '' : ":#{url_fragments[3]}"
username = username_from_url_fragments(url_fragments)
{
username: username,
organization_name: nil,
# url_fragments[2]: Host
user_profile_url: "#{protocol}://#{url_fragments[2]}#{port_fragment}/u/#{username}"
}
end
def username_from_url_fragments(url_fragments)
path_fragments = url_fragments[5].split('/')
raise UrlFRagmentsError.new("Username not found at url") if path_fragments.length < 3 || path_fragments[2].length == 0
path_fragments[2]
end
def extract_uuid_from_string(str)
# TODO: move this method to app/helpers/carto/uuidhelper.rb. Not used yet because this changed was pushed before
# UUIDTools::UUID_REGEXP cannot be reused because of /^ $/
matches = /([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{2})([0-9a-f]{2})-([0-9a-f]{12})/.match(str)
if matches
matches[0]
else
nil
end
end
end
class UrlFRagmentsError < StandardError
end
end
end
@@ -0,0 +1,78 @@
module Carto
module Api
class OrganizationAssetsController < ::Api::ApplicationController
include Carto::ControllerHelper
ssl_required :index, :show, :create, :destroy
before_filter :load_organization,
:organization_members_only
before_filter :organization_owners_only, only: [:create, :destroy]
before_filter :load_asset, only: [:show, :destroy]
before_filter :load_resource, only: :create
rescue_from LoadError,
UnprocesableEntityError,
UnauthorizedError, with: :rescue_from_carto_error
def index
presentation = @organization.assets.map do |asset|
AssetPresenter.new(asset).to_hash
end
render json: presentation
end
def show
render json: AssetPresenter.new(@asset).to_hash
end
def create
asset = Asset.for_organization(organization: @organization,
resource: @resource)
asset.save!
render json: AssetPresenter.new(asset), status: :created
rescue ActiveRecord::RecordInvalid => exception
raise UnprocesableEntityError.with_full_messages(exception)
end
def destroy
@asset.destroy
head :no_content
end
private
def load_organization
@organization = Organization.find(params[:organization_id])
rescue ActiveRecord::RecordNotFound
raise LoadError.new('Organization not found')
end
def organization_members_only
unless current_viewer.belongs_to_organization?(@organization)
raise UnauthorizedError.new
end
end
def organization_owners_only
raise UnauthorizedError.new unless @organization.owner?(current_viewer)
end
def load_asset
@asset = Asset.find(params[:id])
rescue ActiveRecord::RecordNotFound
raise LoadError.new('Asset not found')
end
def load_resource
@resource = params[:resource]
unless @resource.present?
raise UnprocesableEntityError.new('Missing resource for asset')
end
end
end
end
end
@@ -0,0 +1,45 @@
module Carto
module Api
class OrganizationNotificationsController < ::Api::ApplicationController
include Carto::ControllerHelper
extend Carto::DefaultRescueFroms
ssl_required :create, :destroy
before_filter :owners_only, :load_organization
before_filter :load_notification, only: [:destroy]
setup_default_rescues
respond_to :json
def create
notification_parameters = params.require(:notification).permit(:icon, :body, :recipients)
notification = @organization.notifications.create!(notification_parameters)
render_jsonp(NotificationPresenter.new(notification).to_hash, :created)
end
def destroy
@notification.destroy
head :no_content
end
private
def load_organization
@organization = Carto::Organization.find(current_user.organization_id)
unless [@organization.name, @organization.id].include?(params[:organization_id])
raise Carto::LoadError.new('Cannot find organization')
end
end
def owners_only
raise Carto::UnauthorizedError.new('Only organization owners') unless current_user.organization_owner?
end
def load_notification
@notification = @organization.notifications.find(params[:id])
end
end
end
end
@@ -0,0 +1,62 @@
require_relative 'group_presenter'
module Carto
module Api
class OrganizationPresenter
def initialize(organization)
@organization = organization
end
def to_poro
return {} if @organization.nil?
filtered_user ||= @organization.owner
{
created_at: @organization.created_at,
description: @organization.description,
discus_shortname: @organization.discus_shortname,
display_name: @organization.display_name,
id: @organization.id,
name: @organization.name,
owner: {
id: @organization.owner ? @organization.owner.id : nil,
username: @organization.owner ? @organization.owner.username : nil,
avatar_url: @organization.owner ? @organization.owner.avatar_url : nil,
email: @organization.owner ? @organization.owner.email : nil,
groups: @organization.owner && @organization.owner.groups ? @organization.owner.groups.map { |g| Carto::Api::GroupPresenter.new(g).to_poro } : []
},
admins: @organization.users.where(org_admin: true).map { |u| { id: u.id } },
quota_in_bytes: @organization.quota_in_bytes,
unassigned_quota: @organization.unassigned_quota,
geocoding_quota: @organization.geocoding_quota,
here_isolines_quota: @organization.here_isolines_quota,
here_isolines_block_price: @organization.here_isolines_block_price,
obs_snapshot_quota: @organization.obs_snapshot_quota,
obs_snapshot_block_price: @organization.obs_snapshot_block_price,
obs_general_quota: @organization.obs_general_quota,
obs_general_block_price: @organization.obs_general_block_price,
mapzen_routing_quota: @organization.mapzen_routing_quota,
mapzen_routing_block_price: @organization.mapzen_routing_block_price,
geocoder_provider: @organization.geocoder_provider,
isolines_provider: @organization.isolines_provider,
routing_provider: @organization.routing_provider,
map_view_quota: @organization.map_view_quota,
twitter_datasource_quota: @organization.twitter_datasource_quota,
map_view_block_price: @organization.map_view_block_price,
geocoding_block_price: @organization.geocoding_block_price,
seats: @organization.seats,
viewer_seats: @organization.viewer_seats,
twitter_username: @organization.twitter_username,
location: @organization.location,
updated_at: @organization.updated_at,
website: @organization.website,
admin_email: @organization.admin_email,
avatar_url: @organization.avatar_url,
user_count: @organization.users.count,
password_expiration_in_d: @organization.password_expiration_in_d
}
end
end
end
end
@@ -0,0 +1,15 @@
module Carto
module Api
class OrganizationPublicPresenter
def initialize(organization)
@organization = organization
end
def to_hash
{
name: @organization.name
}
end
end
end
end
@@ -0,0 +1,176 @@
require_relative './user_presenter'
module Carto
module Api
class OrganizationUsersController < ::Api::ApplicationController
include OrganizationUsersHelper
ssl_required :index, :show, :create, :update, :destroy
before_filter :load_organization
before_filter :admins_only
before_filter :load_user, only: [:show, :update, :destroy]
before_filter :ensure_edit_permissions, only: [:show, :update, :destroy]
def index
presentations = @organization.users.map do |user|
Carto::Api::UserPresenter.new(user, current_viewer: current_viewer).to_eumapi_poro
end
render_jsonp presentations, 200
end
def show
presentation = Carto::Api::UserPresenter.new(@user, current_viewer: current_viewer)
.to_eumapi_poro
render_jsonp presentation, 200
end
def create
account_creator = CartoDB::UserAccountCreator.new(Carto::UserCreation::CREATED_VIA_API)
.with_organization(@organization)
account_creator.with_username(create_params[:username]) if create_params[:username].present?
account_creator.with_email(create_params[:email]) if create_params[:email].present?
account_creator.with_password(create_params[:password]) if create_params[:password].present?
account_creator.with_quota_in_bytes(create_params[:quota_in_bytes]) if create_params[:quota_in_bytes].present?
param_viewer = create_params[:viewer]
account_creator.with_viewer(param_viewer) if param_viewer
param_org_admin = create_params[:org_admin]
account_creator.with_org_admin(param_org_admin) if param_org_admin
if create_params[:soft_geocoding_limit].present?
account_creator.with_soft_geocoding_limit(create_params[:soft_geocoding_limit])
end
if create_params[:soft_here_isolines_limit].present?
account_creator.with_soft_here_isolines_limit(create_params[:soft_here_isolines_limit])
end
if create_params[:soft_obs_snapshot_limit].present?
account_creator.with_soft_obs_snapshot_limit(create_params[:soft_obs_snapshot_limit])
end
if create_params[:soft_obs_general_limit].present?
account_creator.with_soft_obs_general_limit(create_params[:soft_obs_general_limit])
end
if create_params[:soft_twitter_datasource_limit].present?
account_creator.with_soft_twitter_datasource_limit(create_params[:soft_twitter_datasource_limit])
end
if create_params[:soft_mapzen_routing_limit].present?
account_creator.with_soft_mapzen_routing_limit(create_params[:soft_mapzen_routing_limit])
end
if create_params[:force_password_change] == true
account_creator.with_force_password_change
end
unless account_creator.valid_creation?(current_viewer)
render_jsonp(account_creator.validation_errors.full_messages, 410)
return
end
account_creator.enqueue_creation(self)
presentation = Carto::Api::UserPresenter.new(account_creator.user,
current_viewer: current_viewer)
.to_eumapi_poro
render_jsonp presentation, 200
rescue => e
CartoDB.notify_exception(e, user: account_creator.user.inspect)
render_jsonp('An error has ocurred. Please contact support', 500)
end
def update
render_jsonp('No update params provided', 410) && return if update_params.empty?
params_to_update = update_params
# ::User validation requires confirmation
password = params_to_update[:password]
params_to_update[:password_confirmation] = password
# NOTE: Verify soft limits BEFORE updating the user
model_validation_ok = soft_limits_validation(@user, params_to_update)
model_validation_ok &&= @user.valid_password?(:password, password, password) if password.present?
@user.set_fields(params_to_update, params_to_update.keys)
model_validation_ok &&= @user.valid_update?(current_user)
unless model_validation_ok
render_jsonp(@user.errors.full_messages, 410)
return
end
@user.update_in_central
@user.save
presentation = Carto::Api::UserPresenter.new(@user, current_viewer: current_viewer)
.to_eumapi_poro
render_jsonp presentation, 200
rescue CartoDB::CentralCommunicationFailure => e
CartoDB.notify_exception(e)
render_jsonp 'Central comunication failure', 500
end
def destroy
if @organization.owner_id == @user.id
render_jsonp("Can't delete org owner", 401) and return
end
force_destroy = params[:force].present?
if !force_destroy && @user.has_shared_entities?
error_message = "Can't delete @user. 'Has shared entities"
render_jsonp(error_message, 410 ) and return
end
@user.set_force_destroy if force_destroy
@user.destroy
@user.delete_in_central
render_jsonp 'User deleted', 200
rescue CartoDB::CentralCommunicationFailure => e
CartoDB::Logger.error(exception: e, message: 'Central error deleting user from EUMAPI', user: @user)
render_jsonp "User couldn't be deleted", 500
rescue => e
render_jsonp "User couldn't be deleted: #{e.message}", 500
end
private
COMMON_MUTABLE_ATTRIBUTES = [
:email,
:password,
:quota_in_bytes,
:soft_geocoding_limit,
:soft_here_isolines_limit,
:soft_obs_general_limit,
:soft_obs_snapshot_limit,
:soft_twitter_datasource_limit,
:soft_mapzen_routing_limit,
:viewer,
:org_admin
].freeze
# TODO: Use native strong params when in Rails 4+
def create_params
@create_params ||=
permit(COMMON_MUTABLE_ATTRIBUTES + [:username, :force_password_change])
end
# TODO: Use native strong params when in Rails 4+
def update_params
@update_params ||= permit(COMMON_MUTABLE_ATTRIBUTES)
end
end
end
end
@@ -0,0 +1,35 @@
require_relative './user_presenter'
module Carto
module Api
class OrganizationsController < ::Api::ApplicationController
include OrganizationsHelper
include PagedSearcher
include Carto::ControllerHelper
ssl_required :users
before_filter :load_organization, :load_group
rescue_from Carto::ParamInvalidError, with: :rescue_from_carto_error
VALID_ORDER_PARAMS = [:username, :updated_at].freeze
def users
page, per_page, order, = page_per_page_order_params(VALID_ORDER_PARAMS, default_per_page: 50,
default_order: :username)
query = params[:q]
users_query = [@group, @organization].compact.first.users
users_query = users_query.where('(username like ? or email like ?)', "%#{query}%", "#{query}") if query
total_user_entries = users_query.count
users_query = users_query.offset(( page - 1 ) * per_page ).limit(per_page).order(order)
users = users_query.all
render_jsonp({ users: users.map { |u|
Carto::Api::UserPresenter.new(u, current_viewer: current_user, fetch_db_size: false).to_poro
}, total_user_entries: total_user_entries, total_entries: users.count })
end
end
end
end
@@ -0,0 +1,30 @@
module Carto
module Api
class OverlayPresenter
PUBLIC_VALUES = [:id, :order, :type, :template, :options, :visualization_id].freeze
VIZJSON_VALUES = [:type, :order, :options, :template].freeze
def initialize(overlay)
@overlay = overlay
end
def to_poro
values(@overlay, PUBLIC_VALUES)
end
def to_vizjson_poro
values(@overlay, VIZJSON_VALUES)
end
alias_method :to_vizjson, :to_vizjson_poro
protected
def values(overlay, attribute_list)
Hash[attribute_list.map { |attribute| [attribute, overlay.send(attribute)] }]
end
end
end
end
@@ -0,0 +1,104 @@
require_dependency 'carto/controller_helper'
module Carto
module Api
class OverlaysController < ::Api::ApplicationController
include Carto::ControllerHelper
ssl_required :index, :show, :create, :update, :destroy
before_filter :logged_users_only
before_filter :load_visualization
before_filter :check_current_user_has_permissions_on_vis
before_filter :load_overlay, only: [:show, :update, :destroy]
rescue_from StandardError, with: :rescue_from_standard_error
rescue_from Carto::CartoError, with: :rescue_from_carto_error
def index
collection = @visualization.overlays.map do |overlay|
Carto::Api::OverlayPresenter.new(overlay).to_poro
end
render_jsonp(collection)
end
def show
render_jsonp(Carto::Api::OverlayPresenter.new(@overlay).to_poro)
end
def create
@stats_aggregator.timing('overlays.create') do
begin
overlay = Carto::Overlay.new(type: params[:type],
options: params[:options],
template: params[:template],
order: params[:order],
visualization_id: @visualization.id)
saved = @stats_aggregator.timing('save') do
overlay.save
end
if saved
render_jsonp(Carto::Api::OverlayPresenter.new(overlay).to_poro)
else
render_jsonp({ errors: overlay.errors }, :unprocessable_entity)
end
end
end
end
def update
@stats_aggregator.timing('overlays.update') do
begin
@overlay.type = params[:type] if params[:type]
@overlay.options = params[:options] if params[:options]
@overlay.template = params[:template] if params[:template]
@overlay.order = params[:order] if params[:order]
saved = @stats_aggregator.timing('save') do
@overlay.save
end
if saved
render_jsonp(Carto::Api::OverlayPresenter.new(@overlay).to_poro)
else
render_jsonp({ errors: @overlay.errors }, :unprocessable_entity)
end
end
end
end
def destroy
@stats_aggregator.timing('overlays.destroy') do
@stats_aggregator.timing('delete') do
@overlay.destroy
end
head :no_content
end
end
protected
def logged_users_only
raise Carto::UnauthorizedError.new if current_user.nil?
end
def load_visualization
visualization_id = uuid_parameter('visualization_id')
@visualization = Carto::Visualization.where(id: visualization_id).first
raise Carto::LoadError.new("Visualization not found: #{visualization_id}") unless @visualization
end
def check_current_user_has_permissions_on_vis
unless @visualization.writable_by?(current_user)
raise Carto::UnauthorizedError.new("#{current_user.id} doesn't own visualization #{@visualization.id}")
end
end
def load_overlay
overlay_id = uuid_parameter('id')
@overlay = @visualization.overlays.where(id: overlay_id).first
raise Carto::LoadError.new("Overlay not found: #{overlay_id}") unless @overlay
end
end
end
end
@@ -0,0 +1,68 @@
module Carto
module Api
module PagedSearcher
VALID_ORDER_DIRECTIONS = [:asc, :desc].freeze
def page_per_page_params(default_per_page: 20)
page = (params[:page].presence || 1).to_i
unless page > 0
raise Carto::ParamInvalidError.new('page')
end
per_page = (params[:per_page].presence || default_per_page).to_i
unless per_page > 0
raise Carto::ParamInvalidError.new('per_page')
end
[page, per_page]
end
def page_per_page_order_params(valid_order_values, default_per_page: 20, default_order: 'updated_at',
default_order_direction: 'desc', valid_order_combinations: [])
page, per_page = page_per_page_params(default_per_page: default_per_page)
order = extract_param(name: :order, default_value: default_order, valid_values: valid_order_values,
valid_combinations: valid_order_combinations)
order_direction = extract_param(name: :order_direction, default_value: default_order_direction,
valid_values: VALID_ORDER_DIRECTIONS,
valid_combinations: VALID_ORDER_DIRECTIONS)
[page, per_page, order, order_direction]
end
def paged_result(result:, total_count:, page:, per_page:, params:)
last_page = (total_count / per_page.to_f).ceil
link_params = params.merge(per_page: per_page)
metadata = {
total: total_count,
count: result.count,
result: result,
_links: {
first: { href: yield(link_params.merge(page: 1)) },
last: { href: yield(link_params.merge(page: last_page)) }
}
}
metadata[:_links][:prev] = { href: yield(link_params.merge(page: page - 1)) } if page > 1
metadata[:_links][:next] = { href: yield(link_params.merge(page: page + 1)) } if last_page > page
metadata
end
def extract_param(name:, default_value:, valid_values:, valid_combinations:)
param = (params[name].presence || default_value).to_sym
param_values = param.to_s.split(',').map(&:to_sym)
single_parameter = valid_combinations.empty? || param_values.size == 1
if single_parameter && valid_values.exclude?(param)
raise Carto::ParamInvalidError.new(name, valid_values)
end
if !single_parameter && (param_values - valid_combinations).present?
raise Carto::ParamCombinationInvalidError.new(name, valid_combinations)
end
param
end
end
end
end
@@ -0,0 +1,107 @@
require_relative 'user_presenter'
module Carto
module Api
class PermissionPresenter
def initialize(permission, current_viewer: nil, fetch_user_groups: false)
@permission = permission
@presenter_cache = Carto::Api::PresenterCache.new
@current_viewer = current_viewer
@fetch_user_groups = fetch_user_groups
end
def with_presenter_cache(presenter_cache)
@presenter_cache = presenter_cache
self
end
def to_poro
return to_public_poro unless current_viewer && @permission.user_has_read_permission?(current_viewer)
owner = @presenter_cache.get_poro(@permission.owner) do
Carto::Api::UserPresenter.new(@permission.owner,
fetch_groups: fetch_user_groups,
current_viewer: current_viewer,
fetch_profile: false)
end
{
id: @permission.id,
owner: owner,
entity: {
id: @permission.visualization.id,
type: 'vis'
},
acl: @permission.acl.map { |entry|
entity = entity_decoration(entry)
if entity.blank?
nil
else
{
type: entry[:type],
entity: entity,
access: entry[:access]
}
end
}.reject(&:nil?),
created_at: @permission.created_at,
updated_at: @permission.updated_at
}
end
def to_public_poro
owner = @presenter_cache.get_poro(@permission.owner) do
Carto::Api::UserPresenter.new(@permission.owner,
fetch_groups: fetch_user_groups, current_viewer: current_viewer)
end
{
id: @permission.id,
owner: owner
}
end
def entity_decoration(entry)
if entry[:type] == Carto::Permission::TYPE_USER
user_decoration(entry[:id])
elsif entry[:type] == Carto::Permission::TYPE_ORGANIZATION
organization_decoration(entry[:id])
elsif entry[:type] == Carto::Permission::TYPE_GROUP
group_decoration(entry[:id])
else
raise "Unknown entity type: #{entry[:type]}"
end
end
def user_decoration(user_id)
user = ::User.where(id: user_id).first
return {} if user.nil?
Carto::Api::UserPresenter.new(user, fetch_groups: fetch_user_groups).to_public_poro
end
def organization_decoration(org_id)
org = Carto::Organization.where(id: org_id).first
return {} if org.nil?
{
id: org.id,
name: org.name,
avatar_url: org.avatar_url
}
end
def group_decoration(group_id)
group = Carto::Group.where(id: group_id).first
return {} if group.nil?
{
id: group.id,
name: group.name
}
end
private
attr_reader :current_viewer, :fetch_user_groups
end
end
end
@@ -0,0 +1,28 @@
class Carto::Api::PermissionsController < ::Api::ApplicationController
include Carto::ControllerHelper
extend Carto::DefaultRescueFroms
ssl_required :update
def update
permission = Carto::Permission.where(id: params[:id]).first
return head(404) if permission.nil?
return head(401) unless permission.is_owner?(current_user)
begin
acl = params[:acl]
acl ||= []
permission.acl = acl.map(&:deep_symbolize_keys)
rescue CartoDB::PermissionError => e
CartoDB::Logger.error(exception: e)
return head(400)
end
permission.save!
render json: Carto::Api::PermissionPresenter.new(permission,
current_viewer: current_viewer, fetch_user_groups: true).to_poro
end
end
@@ -0,0 +1,45 @@
module Carto
module Api
class PresenterCache
def initialize
@cache = Hash.new
end
# Caches to_poro of the presenter that is passed in a block, based on model class and id. Example:
# cache.get_poro(user) { Carto::Api::UserPresenter.new(user, { fetch_groups: false } ) }
def get_poro(model)
raise "no model given" unless model
model_class = model.class
model_id = model.id
if model_id.nil?
presenter = yield
raise "no presenter given" if presenter.nil?
return presenter.to_poro
end
class_cache = get_class_cache(model_class)
unless class_cache[model_id]
presenter = yield
raise "no presenter given" if presenter.nil?
class_cache[model_id] = presenter.to_poro
end
class_cache[model_id]
end
private
def get_class_cache(model_class)
unless @cache[model_class]
@cache[model_class] = Hash.new
end
@cache[model_class]
end
end
end
end
@@ -0,0 +1,42 @@
class Carto::Api::Public::ApplicationController < ::Api::ApplicationController
include Carto::Api::AuthApiAuthentication
# Override all CORS settings
skip_before_action :cors_preflight_check
skip_after_action :allow_cross_domain_access
before_action :allow_full_cross_domain_access
# Only allow API Key authorization
skip_before_action :http_header_authentication, :api_authorization_required
prepend_before_action :only_api_key_authorization
# Disable authorization check for OPTIONS
skip_before_action :only_api_key_authorization, only: [:options]
WARDEN_SCOPE = :$public_api_scope
def allow_full_cross_domain_access
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = '*'
response.headers['Access-Control-Allow-Headers'] = 'Authorization'
end
def options
head :ok
end
# Override current_user and current_viewer so they only return the user authenticated via API Key (ignore session)
def current_user
warden.user(WARDEN_SCOPE)
end
def current_viewer
current_user
end
private
def only_api_key_authorization
authenticate!(:any_auth_api, :api_authentication, scope: WARDEN_SCOPE)
end
end
@@ -0,0 +1,144 @@
require_relative '../visualization_searcher'
require_relative '../paged_searcher'
class Carto::Api::Public::CustomVisualizationsController < Carto::Api::Public::ApplicationController
include Carto::Api::VisualizationSearcher
include Carto::Api::PagedSearcher
CONTENT_LENGTH_LIMIT_IN_BYTES = 100000
VALID_ORDER_PARAMS = %i(name updated_at privacy).freeze
ALLOWED_PRIVACY_MODES = [
Carto::Visualization::PRIVACY_PUBLIC,
Carto::Visualization::PRIVACY_PROTECTED
].freeze
ssl_required
before_action :validate_mandatory_creation_params, only: [:create]
before_action :validate_input_parameters, only: [:create, :update]
before_action :get_kuviz, only: [:update, :delete]
before_action :get_user, only: [:create, :update, :delete]
before_action :check_for_permission, only: [:update, :delete]
def index
opts = { valid_order_combinations: VALID_ORDER_PARAMS }
page, per_page, order, order_direction = page_per_page_order_params(VALID_ORDER_PARAMS, opts)
params[:type] = Carto::Visualization::TYPE_KUVIZ
vqb = query_builder_with_filter_from_hash(params)
visualizations = vqb.with_order(order, order_direction)
.build_paged(page, per_page).map do |v|
Carto::Api::Public::KuvizPresenter.new(self, v.user, v).to_hash
end
response = {
visualizations: visualizations,
total_entries: vqb.build.size
}
render_jsonp(response)
rescue Carto::ParamInvalidError, Carto::ParamCombinationInvalidError => e
render_jsonp({ error: e.message }, e.status)
rescue StandardError => e
CartoDB::Logger.error(exception: e)
render_jsonp({ error: e.message }, 500)
end
def create
kuviz = create_visualization_metadata(@logged_user)
asset = Carto::Asset.for_visualization(visualization: kuviz,
resource: StringIO.new(Base64.decode64(params[:data])))
asset.save
render_jsonp(Carto::Api::Public::KuvizPresenter.new(self, @logged_user, kuviz).to_hash, 200)
rescue StandardError => e
CartoDB::Logger.error(message: 'Error creating kuviz', params: params, exception: e)
render_jsonp({ error: 'cant create the kuviz' }, 500)
end
def update
@kuviz.update_attributes!(params.permit(:name, :privacy, :password))
if params[:data].present?
@kuviz.asset.update_visualization_resource(StringIO.new(Base64.decode64(params[:data])))
# In case we only update the asset we need to invalidate the visualization
@kuviz.save
end
render_jsonp(Carto::Api::Public::KuvizPresenter.new(self, @logged_user, @kuviz).to_hash, 200)
end
def delete
@kuviz.destroy
head 204
rescue StandardError => exception
CartoDB::Logger.error(message: 'Error deleting kuviz', exception: exception,
visualization: @kuviz)
render_jsonp({ errors: [exception.message] }, 400)
end
private
def create_visualization_metadata(user)
kuviz = Carto::Visualization.new
kuviz.name = params[:name]
kuviz.privacy = params[:password].present? ? Carto::Visualization::PRIVACY_PROTECTED : Carto::Visualization::PRIVACY_PUBLIC
kuviz.password = params[:password]
kuviz.type = Carto::Visualization::TYPE_KUVIZ
kuviz.user = user
kuviz.save
kuviz
end
def get_user
@logged_user = current_viewer.present? ? Carto::User.find(current_viewer.id) : nil
end
def check_for_permission
head(403) unless @kuviz.has_permission?(@logged_user, Carto::Permission::ACCESS_READWRITE)
end
def validate_input_parameters
if request.content_length > CONTENT_LENGTH_LIMIT_IN_BYTES
return render_jsonp({ error: "visualization over the size limit (#{CONTENT_LENGTH_LIMIT_IN_BYTES})" }, 400)
end
if params[:privacy].present?
unless ALLOWED_PRIVACY_MODES.include?(params[:privacy])
return render_jsonp({ error: "privacy mode not allowed. Allowed ones are #{ALLOWED_PRIVACY_MODES}" }, 400)
end
if params[:privacy] == Carto::Visualization::PRIVACY_PROTECTED && !params[:password].present?
return render_jsonp({ error: 'Changing privacy to protected should come along with the password param' }, 400)
end
end
if params[:data].present?
begin
decoded_data = Base64.strict_decode64(params[:data])
return render_jsonp({ error: 'data parameter must be HTML' }, 400) unless html_param?(decoded_data)
rescue ArgumentError
return render_jsonp({ error: 'data parameter must be encoded in base64' }, 400)
end
end
end
def validate_mandatory_creation_params
if !params[:data].present?
render_jsonp({ error: 'missing data parameter' }, 400)
elsif !params[:name].present?
render_jsonp({ error: 'missing name parameter' }, 400)
end
end
def html_param?(data)
# FIXME this is a very naive implementantion. I'm trying to use
# Nokogiri to validate the HTML but it doesn't works as I want
# so
data.match(/\<html.*\>/).present?
end
def get_kuviz
@kuviz = Carto::Visualization.find(params[:id])
if @kuviz.nil?
raise Carto::LoadError.new('Kuviz doesn\'t exist', 404)
end
end
end
@@ -0,0 +1,158 @@
module Carto
module Api
module Public
class DataObservatoryController < Carto::Api::Public::ApplicationController
include Carto::ControllerHelper
include Carto::Api::PagedSearcher
extend Carto::DefaultRescueFroms
ssl_required
before_action :load_user
before_action :load_filters, only: [:subscriptions]
before_action :load_id, only: [:subscription_info, :subscribe, :unsubscribe]
before_action :load_type, only: [:subscription_info, :subscribe]
before_action :check_api_key_permissions
before_action :check_licensing_enabled, only: [:subscription_info, :subscribe, :unsubscribe]
setup_default_rescues
respond_to :json
BIGQUERY_KEY = 'bq'.freeze
VALID_TYPES = %w(dataset geography).freeze
DATASET_REGEX = /[\w\-]+\.[\w\-]+\.[\w\-]+/.freeze
VALID_ORDER_PARAMS = %i(id table dataset project type).freeze
METADATA_FIELDS = %i(id estimated_delivery_days subscription_list_price tos tos_link licenses licenses_link
rights type).freeze
TABLES_BY_TYPE = { 'dataset' => 'datasets', 'geography' => 'geographies' }.freeze
def token
response = Cartodb::Central.new.get_do_token(@user.username)
render(json: response)
end
def subscriptions
available_subscriptions = bq_subscriptions.select { |dataset| Time.parse(dataset['expires_at']) > Time.now }
response = present_subscriptions(available_subscriptions)
render(json: { subscriptions: response })
end
def subscription_info
response = present_metadata(subscription_metadata)
render(json: response)
end
def subscribe
metadata = subscription_metadata
response = present_metadata(metadata)
return render(json: response) if metadata[:estimated_delivery_days].positive?
license_info = {
dataset_id: metadata[:id],
available_in: metadata[:available_in],
price: metadata[:subscription_list_price],
expires_at: Time.now.round + 1.year
}
Carto::DoLicensingService.new(@user.username).subscribe([license_info])
render(json: response)
end
def unsubscribe
Carto::DoLicensingService.new(@user.username).unsubscribe(@id)
head :no_content
end
private
def load_user
@user = Carto::User.find(current_viewer.id)
end
def load_filters
_, _, @order, @direction = page_per_page_order_params(
VALID_ORDER_PARAMS, default_order: 'id', default_order_direction: 'asc'
)
load_type(required: false)
end
def load_id
@id = params[:id]
raise ParamInvalidError.new(:id) unless @id =~ DATASET_REGEX
end
def load_type(required: true)
@type = params[:type]
return if @type.nil? && !required
raise ParamInvalidError.new(:type, VALID_TYPES.join(', ')) unless VALID_TYPES.include?(@type)
end
def check_api_key_permissions
api_key = Carto::ApiKey.find_by_token(params["api_key"])
raise UnauthorizedError unless api_key&.master? || api_key&.data_observatory_permissions?
end
def check_licensing_enabled
raise UnauthorizedError.new('DO licensing not enabled') unless @user.has_feature_flag?('do-licensing')
end
def rescue_from_central_error(exception)
render_jsonp({ errors: exception.errors }, 500)
end
def bq_subscriptions
redis_key = "do:#{@user.username}:datasets"
redis_value = $users_metadata.hget(redis_key, BIGQUERY_KEY) || '[]'
JSON.parse(redis_value)
end
def present_subscriptions(subscriptions)
enriched_subscriptions = subscriptions.map do |subscription|
qualified_id = subscription['dataset_id']
project, dataset, table = qualified_id.split('.')
# FIXME: better save the type in Redis or look for it in the metadata tables
type = table.starts_with?('geography') ? 'geography' : 'dataset'
{ project: project, dataset: dataset, table: table, id: qualified_id, type: type }
end
enriched_subscriptions.select! { |subscription| subscription[:type] == @type } if @type
ordered_subscriptions = enriched_subscriptions.sort_by { |subscription| subscription[@order] }
@direction == :asc ? ordered_subscriptions : ordered_subscriptions.reverse
end
def present_metadata(metadata)
metadata[:estimated_delivery_days] = metadata[:estimated_delivery_days].to_f
metadata[:subscription_list_price] = metadata[:subscription_list_price].to_f
metadata.slice(*METADATA_FIELDS)
end
def subscription_metadata
metadata_user = ::User.where(username: 'do-metadata').first
raise Carto::LoadError.new('No Data Observatory metadata found') unless metadata_user
query = "SELECT *, '#{@type}' as type FROM #{TABLES_BY_TYPE[@type]} WHERE id = '#{@id}'"
result = metadata_user.in_database[query].first
validate_metadata(result)
end
def validate_metadata(result)
raise Carto::LoadError.new("No metadata found for #{@id}") unless result
valid_data = result[:available_in].present? && result[:estimated_delivery_days].present? &&
result[:subscription_list_price].present?
unless valid_data
CartoDB::Logger.info(message: 'Incomplete DO metadata', id: @id)
raise Carto::LoadError.new("Incomplete metadata found for #{@id}")
end
result
end
end
end
end
end
@@ -0,0 +1,126 @@
require_relative '../paged_searcher'
require_dependency 'carto/oauth_provider/errors'
module Carto
module Api
module Public
class DatasetsController < Carto::Api::Public::ApplicationController
include Carto::Api::PagedSearcher
include Carto::ControllerHelper
extend Carto::DefaultRescueFroms
ssl_required
before_action :load_user
before_action :load_params
before_action :check_permissions
before_action :engine_required
setup_default_rescues
rescue_from Carto::OauthProvider::Errors::ServerError, with: :rescue_oauth_errors
VALID_ORDER_PARAMS = %i(name type).freeze
VALID_TYPE_PARAMS = %w(table view matview).freeze
def index
tables = @user.in_database[select_tables_query].all
result = enrich_tables(tables)
total = @user.in_database[count_tables_query].first[:count]
render_paged(result, total)
end
private
def load_user
@user = ::User.where(id: current_viewer.id).first
end
def load_params
@page, @per_page, @order, @direction = page_per_page_order_params(
VALID_ORDER_PARAMS, default_order: 'name', default_order_direction: 'asc'
)
@offset = (@page - 1) * @per_page
@types = load_type
end
def load_type
types = params[:type]&.split(',').to_a
raise Carto::ParamInvalidError.new(:type, VALID_TYPE_PARAMS) if (types - VALID_TYPE_PARAMS).any?
types.empty? ? VALID_TYPE_PARAMS : types
end
def check_permissions
api_key = Carto::ApiKey.find_by_token(params["api_key"])
raise UnauthorizedError unless api_key.master? || api_key.dataset_metadata_permissions
end
def enrich_tables(tables)
table_names = tables.map { |table| table[:name] }
visualizations = table_visualizations(table_names)
tables.map do |table|
visualizations.find { |visualization| visualization[:name] == table[:name] } || table
end
end
def table_visualizations(names)
visualizations = Carto::VisualizationQueryBuilder.new
.with_user_id(@user.id)
.with_name(names)
.with_type(Carto::Visualization::TYPE_CANONICAL)
.build.all
visualizations.map do |visualization|
{
name: visualization.name,
cartodbfied: true,
type: 'table',
privacy: visualization.privacy,
updated_at: visualization.updated_at
}
end
end
def select_tables_query
%{
SELECT * FROM (#{tables_and_views_query}) AS tables_and_views
ORDER BY #{@order} #{@direction}
LIMIT #{@per_page}
OFFSET #{@offset}
}.squish
end
def count_tables_query
"SELECT COUNT(*) FROM (#{tables_and_views_query}) AS tables_and_views"
end
def tables_and_views_query
@types.map { |type|
%{
SELECT #{type}name AS name, '#{type}' AS type, false AS cartodbfied, NULL AS privacy, NULL AS updated_at
FROM pg_#{type}s
WHERE schemaname = '#{@user.database_schema}'
AND #{type}owner <> 'postgres'
}
}.join(' UNION ').squish
end
def render_paged(result, total)
enriched_response = paged_result(
result: result,
total_count: total,
page: @page,
per_page: @per_page,
params: params.except('controller', 'action')
) { |params| api_v4_datasets_url(params) }
render_jsonp(enriched_response, 200)
end
def rescue_oauth_errors(exception)
render json: { errors: exception.parameters[:error_description] }, status: 500
end
end
end
end
end
@@ -0,0 +1,27 @@
module Carto
module Api
module Public
class KuvizPresenter
def initialize(context, user, kuviz)
@context = context
@user = user
@kuviz = kuviz
end
def to_hash
{
id: @kuviz.id,
name: @kuviz.name,
privacy: @kuviz.privacy,
created_at: @kuviz.created_at,
updated_at: @kuviz.updated_at,
url: CartoDB.url(@context, 'kuviz_show',
params: { id: @kuviz.id },
user: @user)
}
end
end
end
end
end
@@ -0,0 +1,37 @@
module Carto
module Api
module Public
class OauthAppPresenter
PRIVATE_ATTRIBUTES = %i(
id user_id name created_at updated_at client_id client_secret
redirect_uris icon_url restricted description website_url
).freeze
PUBLIC_ATTRIBUTES = %i(id name created_at updated_at description website_url icon_url).freeze
def initialize(oauth_app, user: nil)
@oauth_app = oauth_app
@user = user
end
def to_hash(private_data: false)
private_data ? to_private_hash : to_public_hash
end
private
def to_private_hash
@oauth_app.slice(*PRIVATE_ATTRIBUTES).merge(username: @oauth_app.user.username)
end
def to_public_hash
oauth_app_user = @oauth_app.oauth_app_users.where(user: @user).first
scopes = Carto::OauthProvider::Scopes.scopes_by_category(oauth_app_user&.all_scopes)
@oauth_app.slice(*PUBLIC_ATTRIBUTES).merge(scopes: scopes)
end
end
end
end
end
@@ -0,0 +1,143 @@
require_relative '../paged_searcher'
require_dependency 'carto/oauth_provider/errors'
module Carto
module Api
module Public
class OauthAppsController < Carto::Api::Public::ApplicationController
include Carto::Api::PagedSearcher
include Carto::ControllerHelper
extend Carto::DefaultRescueFroms
ssl_required
before_action :load_user
before_action :load_owned_app, only: [:show, :update, :regenerate_secret, :destroy]
before_action :load_granted_app, only: :revoke
before_action :load_index_params, only: [:index, :index_granted]
before_action :engine_required
setup_default_rescues
rescue_from Carto::OauthProvider::Errors::ServerError, with: :rescue_oauth_errors
VALID_ORDER_PARAMS = [:name, :updated_at, :restricted, :user_id].freeze
def index
oauth_apps = user_or_organization_apps
render_paged(oauth_apps, private_data: true) { |params| api_v4_oauth_apps_url(params) }
end
def index_granted
oauth_apps = @user.granted_oauth_apps
render_paged(oauth_apps) { |params| api_v4_oauth_apps_index_granted_url(params) }
end
def show
render_jsonp(OauthAppPresenter.new(@oauth_app).to_hash(private_data: true), 200)
end
def create
create_params = permitted_params.merge(user: @user)
@oauth_app = OauthApp.create!(create_params)
track_event('CreatedOauthApp')
render_jsonp(OauthAppPresenter.new(@oauth_app).to_hash(private_data: true), 201)
end
def update
@oauth_app.update_attributes!(permitted_params)
render_jsonp(OauthAppPresenter.new(@oauth_app).to_hash(private_data: true), 200)
end
def regenerate_secret
@oauth_app.regenerate_client_secret!
render_jsonp(OauthAppPresenter.new(@oauth_app.reload).to_hash(private_data: true), 200)
end
def destroy
@oauth_app.destroy!
track_event('DeletedOauthApp')
head :no_content
end
def revoke
oauth_app_user = @oauth_app.oauth_app_users.where(user_id: @user.id).first
oauth_app_user.destroy!
track_event('DeletedOauthAppUser')
head :no_content
end
private
def load_user
@user = Carto::User.find(current_viewer.id)
end
def load_owned_app
@oauth_app = Carto::OauthApp.find(params[:id])
raise ActiveRecord::RecordNotFound.new unless owned?
end
def load_granted_app
@oauth_app = Carto::OauthApp.find(params[:id])
raise ActiveRecord::RecordNotFound.new unless granted?
end
def owned?
return true if @oauth_app.user_id == @user.id
@user.organization_admin? && @user.organization == @oauth_app.user.organization
end
def granted?
@user.granted_oauth_apps.include?(@oauth_app)
end
def load_index_params
@page, @per_page, @order = page_per_page_order_params(VALID_ORDER_PARAMS)
end
def user_or_organization_apps
return @user.oauth_apps unless @user.organization_admin?
org_users = @user.organization.users
Carto::OauthApp.where(user: org_users)
end
def permitted_params
params.permit(:name, :icon_url, :description, :website_url, redirect_uris: [])
end
def render_paged(oauth_apps, private_data: false)
filtered_oauth_apps = Carto::PagedModel.paged_association(oauth_apps, @page, @per_page, @order)
result = filtered_oauth_apps.map do |oauth_app|
OauthAppPresenter.new(oauth_app, user: @user).to_hash(private_data: private_data)
end
enriched_response = paged_result(
result: result,
total_count: oauth_apps.size,
page: @page,
per_page: @per_page,
params: params.except('controller', 'action')
) { |params| yield(params) }
render_jsonp(enriched_response, 200)
end
def rescue_oauth_errors(exception)
render json: { errors: exception.parameters[:error_description] }, status: 500
end
def track_event(event_name)
properties = {
user_id: @user.id,
app_id: @oauth_app.id,
app_name: @oauth_app.name
}
event_class = "Carto::Tracking::Events::#{event_name}".constantize
event_class.new(@user.id, properties).report
end
end
end
end
end
@@ -0,0 +1,19 @@
module Carto
module Api
module Public
class OrganizationPublicProfilePresenter
def initialize(organization)
@organization = organization
end
def to_hash
{
owner: {
username: @organization.owner.username
}
}
end
end
end
end
end
@@ -0,0 +1,29 @@
module Carto
module Api
module Public
class UserPublicPresenter
include SqlApiHelper
include MapsApiHelper
def initialize(user)
@user = user
end
def to_hash
base_rails_url = CartoDB.base_url(@user.username)
{
username: @user.username,
organization: @user.has_organization? ? OrganizationPublicPresenter.new(@user.organization).to_hash : nil,
api_endpoints: {
sql: sql_api_url(@user.username),
maps: maps_api_url(@user.username),
import: base_rails_url,
auth: base_rails_url
}
}
end
end
end
end
end
@@ -0,0 +1,22 @@
module Carto
module Api
module Public
class UserPublicProfilePresenter < UserPublicPresenter
def initialize(user)
@user = user
end
def to_hash
org_hash = OrganizationPublicProfilePresenter.new(@user.organization).to_hash if @user.has_organization?
super.deep_merge(
organization: org_hash,
avatar_url: @user.avatar_url,
first_name: @user.name,
last_name: @user.last_name
)
end
end
end
end
end
@@ -0,0 +1,19 @@
module Carto
module Api
module Public
class UsersController < Carto::Api::Public::ApplicationController
ssl_required
def me_public
presentation = if request_api_key.user_data.try(:include?, 'profile')
UserPublicProfilePresenter.new(request_api_key.user).to_hash
else
UserPublicPresenter.new(request_api_key.user).to_hash
end
render(json: presentation)
end
end
end
end
end
@@ -0,0 +1,17 @@
class Carto::Api::ReceivedNotificationPresenter
include Carto::HtmlSafe
def initialize(received_notification)
@received_notification = received_notification
end
def to_hash
{
id: @received_notification.id,
icon: @received_notification.icon,
html_body: markdown_html_safe(@received_notification.body),
received_at: @received_notification.received_at,
read_at: @received_notification.read_at
}
end
end
@@ -0,0 +1,41 @@
module Carto
module Api
class ReceivedNotificationsController < ::Api::ApplicationController
include Carto::ControllerHelper
extend Carto::DefaultRescueFroms
ssl_required :update
before_filter :load_user
before_filter :load_notification, only: [:update]
setup_default_rescues
def update
changed_notification = params[:notification]
if changed_notification
read_at = changed_notification[:read_at]
@received_notification.read_at = DateTime.parse(read_at) if read_at
@received_notification.save!
end
render_jsonp ReceivedNotificationPresenter.new(@received_notification).to_hash
rescue ArgumentError => e
render_jsonp({ errors: { read_at: 'invalid date format' } }, 422)
CartoDB::Logger.warning(exception: e)
end
private
def load_user
@user = Carto::User.find(current_user.id)
raise Carto::UnauthorizedError.new('Can only access own notifications') unless @user.id == params[:user_id]
end
def load_notification
@received_notification = @user.received_notifications.find(params[:id])
end
end
end
end
@@ -0,0 +1,89 @@
require_relative '../../../models/carto/permission'
module Carto
module Api
class RecordsController < ::Api::ApplicationController
ssl_required :show, :create, :update, :destroy
REJECT_PARAMS = %w{ format controller action row_id requestId column_id
api_key table_id oauth_token oauth_token_secret api_key user_domain }.freeze
before_filter :set_start_time
before_filter :load_user_table, only: [:show, :create, :update, :destroy]
before_filter :read_privileges?, only: [:show]
before_filter :write_privileges?, only: [:create, :update, :destroy]
# This endpoint is not used by the editor but by users. Do not remove
def show
render_jsonp(@user_table.service.record(params[:id]))
rescue => e
CartoDB::Logger.error(message: 'Error loading record', exception: e,
record_id: params[:id], user_table: @user_table)
render_jsonp({ errors: ["Record #{params[:id]} not found"] }, 404)
end
def create
primary_key = @user_table.service.insert_row!(filtered_row)
render_jsonp(@user_table.service.record(primary_key))
rescue => e
render_jsonp({ errors: [e.message] }, 400)
end
def update
if params[:cartodb_id].present?
begin
resp = @user_table.service.update_row!(params[:cartodb_id], filtered_row)
if resp > 0
render_jsonp(@user_table.service.record(params[:cartodb_id]))
else
render_jsonp({ errors: ["row identified with #{params[:cartodb_id]} not found"] }, 404)
end
rescue => e
CartoDB::Logger.warning(message: 'Error updating record', exception: e)
render_jsonp({ errors: [translate_error(e.message.split("\n").first)] }, 400)
end
else
render_jsonp({ errors: ["cartodb_id can't be blank"] }, 404)
end
end
def destroy
id = (params[:cartodb_id] =~ /\A\d+\z/ ? params[:cartodb_id] : params[:cartodb_id].to_s.split(','))
schema_name = current_user.database_schema
if current_user.id != @user_table.service.owner.id
schema_name = @user_table.service.owner.database_schema
end
current_user.in_database
.select
.from(Sequel.qualify(schema_name.to_sym, @user_table.service.name.to_sym))
.where(cartodb_id: id)
.delete
head :no_content
rescue
render_jsonp({ errors: ["row identified with #{params[:cartodb_id]} not found"] }, 404)
end
protected
def filtered_row
params.reject { |k, _| REJECT_PARAMS.include?(k) }.symbolize_keys
end
def load_user_table
@user_table = Carto::Helpers::TableLocator.new.get_by_id_or_name(params[:table_id], current_user)
raise RecordNotFound unless @user_table
end
def read_privileges?
head(401) unless current_user && @user_table.visualization.is_viewable_by_user?(current_user)
end
def write_privileges?
head(401) unless current_user && @user_table.visualization.writable_by?(current_user)
end
end
end
end
@@ -0,0 +1,41 @@
require_dependency 'carto/controller_helper'
class Carto::Api::SearchPreviewController < ::Api::ApplicationController
include Carto::ControllerHelper
ssl_required
before_filter :login_required
before_filter :load_parameters
rescue_from StandardError, with: :rescue_from_standard_error
rescue_from Carto::ParamCombinationInvalidError, with: :rescue_from_carto_error
DEFAULT_LIMIT = 4
DEFAULT_TYPES = "derived,table,remote,tag".freeze
VALID_TYPES = Carto::Visualization::VALID_TYPES + ["tag"]
def index
searcher_params = { user: current_viewer, pattern: @pattern, types: @types, limit: @limit }
searcher = Carto::DashboardPreviewSearcher.new(searcher_params)
result = searcher.search
presenter_params = { dashboard_search_result: result, limit: @limit, current_viewer: current_viewer, context: self }
presentation = Carto::Api::SearchPreviewPresenter.new(presenter_params).to_poro
render json: presentation
end
private
def load_parameters
@limit = params.fetch(:limit, DEFAULT_LIMIT).to_i
@pattern = params[:q]
@types = params.fetch(:types, DEFAULT_TYPES).split(',')
if (@types - VALID_TYPES).present?
raise Carto::ParamCombinationInvalidError.new(:types, VALID_TYPES)
end
end
end
@@ -0,0 +1,31 @@
class Carto::Api::SearchPreviewPresenter
def initialize(dashboard_search_result:, limit:, current_viewer:, context:)
@dashboard_search_result = dashboard_search_result
@limit = limit
@current_viewer = current_viewer
@context = context
end
def to_poro
result = (poro_tags + poro_visualizations).first(@limit)
{
result: result,
total_count: @dashboard_search_result.total_count
}
end
private
def poro_tags
@dashboard_search_result.tags.map do |tag|
Carto::Api::TagPresenter.new(tag, @current_viewer, @context).to_search_preview_poro
end
end
def poro_visualizations
@dashboard_search_result.visualizations.map do |visualization|
Carto::Api::VisualizationPresenter.new(visualization, @current_viewer, @context).to_search_preview_poro
end
end
end
@@ -0,0 +1,19 @@
module Carto
module Api
class SnapshotPresenter
def initialize(snapshot)
@snapshot = snapshot
end
def to_hash
{
id: @snapshot.id,
created_at: @snapshot.created_at,
updated_at: @snapshot.updated_at,
state: @snapshot.state,
user: Carto::Api::UserPresenter.new(@snapshot.user).to_public_poro
}
end
end
end
end
@@ -0,0 +1,84 @@
module Carto
module Api
class SnapshotsController < ::Api::ApplicationController
include ControllerHelper
ssl_required :index, :show, :create, :update, :destroy
before_filter :load_visualization,
:check_visualization_viewable
before_filter :load_snapshot,
:owners_only, only: [:show, :update, :destroy]
rescue_from LoadError,
UnauthorizedError,
UnprocesableEntityError, with: :rescue_from_carto_error
def index
snapshots = @visualization.snapshots.where(user_id: current_viewer.id)
presentation = snapshots.map do |snapshot|
SnapshotPresenter.new(snapshot).to_hash
end
render json: presentation
end
def show
render json: SnapshotPresenter.new(@snapshot).to_hash
end
def create
snapshot = Snapshot.create!(user_id: current_viewer.id,
visualization_id: @visualization.id,
state: params[:state])
render json: SnapshotPresenter.new(snapshot).to_hash, status: :created
rescue ActiveRecord::RecordInvalid => exception
message = exception.record.errors.full_messages.join(', ')
raise UnprocesableEntityError.new(message)
end
def update
@snapshot.update_attributes!(state: params[:state])
render json: SnapshotPresenter.new(@snapshot).to_hash
rescue ActiveRecord::RecordInvalid => exception
message = exception.record.errors.full_messages.join(', ')
raise UnprocesableEntityError.new(message)
end
def destroy
@snapshot.destroy
render json: Hash.new, status: :no_content
end
private
def load_visualization
@visualization = Visualization.find(params[:visualization_id])
rescue ActiveRecord::RecordNotFound
raise LoadError.new('Visualization not found')
end
def check_visualization_viewable
unless @visualization.is_viewable_by_user?(current_viewer)
raise UnauthorizedError.new
end
end
def load_snapshot
@snapshot = Snapshot.find(params[:id])
rescue ActiveRecord::RecordNotFound
raise LoadError.new('Snapshot not found')
end
def owners_only
unless @snapshot.user_id == current_viewer.id
raise UnauthorizedError.new
end
end
end
end
end
@@ -0,0 +1,19 @@
module Carto
module Api
class StatePresenter
def initialize(state)
@state = state
end
def to_hash
{
id: @state.id,
visualization_id: @state.visualization_id,
created_at: @state.created_at,
updated_at: @state.updated_at,
json: @state.json
}
end
end
end
end
@@ -0,0 +1,40 @@
module Carto
module Api
class StatesController < ::Api::ApplicationController
include Carto::ControllerHelper
ssl_required :update
before_filter :load_visualization,
:check_writer, only: :update
rescue_from LoadError,
UnauthorizedError,
UnprocesableEntityError, with: :rescue_from_carto_error
def update
@visualization.state.json = params[:json]
@visualization.state.save!
render json: StatePresenter.new(@visualization.state).to_hash
rescue ActiveRecord::RecordInvalid
message = @visualization.errors.full_messages.join(', ')
raise UnprocesableEntityError.new(message)
end
private
def load_visualization
@visualization = Visualization.find(params[:visualization_id])
rescue ActiveRecord::RecordNotFound
raise LoadError.new('Visualization not found')
end
def check_writer
unless @visualization.writable_by?(current_viewer)
raise UnauthorizedError.new
end
end
end
end
end
@@ -0,0 +1,31 @@
require_dependency 'carto/uuidhelper'
module Carto
module Api
class StaticNotificationsController < ::Api::ApplicationController
include Carto::ControllerHelper
ssl_required :update
before_filter :load_static_notifications, only: [:update]
rescue_from StandardError, with: :rescue_from_standard_error
rescue_from Carto::LoadError, with: :rescue_from_carto_error
def update
category = params[:category].to_sym
@notifications.notifications[category] = params[:notifications]
if @notifications.save
render_jsonp({ notifications: @notifications.notifications[category] }, 200)
else
render_jsonp({ errors: @notifications.errors.to_h }, 422)
end
end
private
def load_static_notifications
@notifications = Carto::User.find(current_user.id).static_notifications
end
end
end
end
@@ -0,0 +1,43 @@
module Carto
module Api
class SynchronizationPresenter
def initialize(synchronization)
@synchronization = synchronization
end
def to_poro
return nil if @synchronization.nil?
{
checksum: @synchronization.checksum,
created_at: @synchronization.created_at,
error_code: @synchronization.error_code,
error_message: @synchronization.error_message,
id: @synchronization.id,
interval: @synchronization.interval,
modified_at: @synchronization.modified_at,
name: @synchronization.name,
ran_at: @synchronization.ran_at,
retried_times: @synchronization.retried_times,
run_at: @synchronization.run_at,
service_item_id: @synchronization.service_item_id,
service_name: @synchronization.service_name,
state: @synchronization.state,
updated_at: @synchronization.updated_at,
url: @synchronization.url,
user_id: @synchronization.user_id,
content_guessing: @synchronization.content_guessing,
etag: @synchronization.etag,
log_id: @synchronization.log_id,
quoted_fields_guessing: @synchronization.quoted_fields_guessing,
type_guessing: @synchronization.type_guessing,
from_external_source: @synchronization.from_external_source?,
visualization_id: @synchronization.visualization_id
}
end
end
end
end
@@ -0,0 +1,48 @@
module Carto
module Api
class SynchronizationsController < ::Api::ApplicationController
ssl_required :show, :index, :syncing?
before_filter :load_synchronization, only: [:show, :syncing?]
def show
render_jsonp(@synchronization)
rescue => exception
CartoDB.notify_exception(exception)
head(404)
end
def index
synchronizations = Carto::Synchronization.where(user_id: current_user.id)
representation = synchronizations.map(&:to_hash)
response = {
synchronizations: representation,
total_entries: synchronizations.count
}
render_jsonp(response)
rescue => exception
CartoDB.notify_exception(exception)
head(404)
end
def syncing?
render_jsonp( { state: @synchronization.state } )
rescue => exception
CartoDB.notify_exception(exception)
head(404)
end
private
def load_synchronization
@synchronization = Carto::Synchronization::where(id: params[:id]).first
head(404) and return unless @synchronization
head(401) and return unless @synchronization.authorize?(current_user)
end
end
end
end
@@ -0,0 +1,45 @@
module Carto
module Api
class TablePresenter
def initialize(table, current_viewer, context)
@table = table
@current_viewer = current_viewer
@context = context
end
def to_poro
{
id: @table.id,
name: qualified_table_name,
privacy: @table.privacy_text,
schema: @table.schema,
updated_at: @table.updated_at,
rows_counted: @table.rows_estimated,
table_size: @table.table_size,
map_id: @table.map_id,
description: @table.description,
geometry_types: @table.geometry_types,
table_visualization: VisualizationPresenter.new(@table.table_visualization, @current_viewer, @context).to_poro,
dependent_visualizations: @table.fully_dependent_visualizations.map do |v|
VisualizationPresenter.new(v, @current_viewer, @context).to_summarized_poro
end,
non_dependent_visualizations: @table.partially_dependent_visualizations.map do |v|
VisualizationPresenter.new(v, @current_viewer, @context).to_summarized_poro
end,
synchronization: SynchronizationPresenter.new(@table.synchronization).to_poro
}
end
private
def qualified_table_name
owner = @table.owner
if @current_viewer && owner && owner.id != @current_viewer.id
"#{owner.sql_safe_database_schema}.#{@table.name}"
else
@table.name
end
end
end
end
end
@@ -0,0 +1,100 @@
require_relative '../../../models/carto/permission'
require_relative '../../../models/carto/user_table'
module Carto
module Api
class TablesController < ::Api::ApplicationController
ssl_required :show, :create, :update
before_filter :set_start_time
before_filter :load_user_table, only: [:show, :update]
before_filter :read_privileges?, only: [:show]
before_filter :write_privileges?, only: [:update]
def show
table = @user_table.service
render_jsonp(TablePresenter.new(table, current_user, self).to_poro.merge(schema: table.schema(reload: true)))
end
# Very basic controller method to simply make blank tables
# All other table creation things are controlled via the imports_controller#create
def create
table = ::Table.new
table.user_id = current_user.id
tables = Carto::Db::UserSchema.new(current_user).table_names
table.name = Carto::ValidTableNameProposer.new.propose_valid_table_name(params[:name], taken_names: tables)
table.description = params[:description] if params[:description]
table.the_geom_type = params[:the_geom_type] if params[:the_geom_type]
table.force_schema = params[:schema] if params[:schema]
table.tags = params[:tags] if params[:tags]
table.import_from_query = params[:from_query] if params[:from_query]
if table.valid? && table.save
render_jsonp(TablePresenter.new(table, current_user, self).to_poro, 200, location: "/tables/#{table.id}")
table_visualization = table.table_visualization
if table_visualization
current_viewer_id = current_viewer.id
Carto::Tracking::Events::CreatedDataset.new(current_viewer_id,
visualization_id: table_visualization.id,
user_id: current_viewer_id,
origin: 'blank').report
end
else
CartoDB::Logger.error(message: 'Error on tables#create', errors: table.errors.full_messages)
render_jsonp({ description: table.errors.full_messages, stack: table.errors.full_messages }, 400)
end
rescue CartoDB::QuotaExceeded
current_viewer_id = current_viewer.id
Carto::Tracking::Events::ExceededQuota.new(current_viewer_id,
user_id: current_viewer_id).report
render_jsonp({ errors: ['You have reached your table quota'] }, 400)
end
def update
# TODO This endpoint is only used to geocode from editor, passing `latitude_column` and `longitude_column`
# TODO It also supports attributes assignement, but this is not called from our frontend
table = @user_table.service
warnings = []
# TODO: this is bad, passing all params blindly to the table object
@user_table.assign_attributes(params.symbolize_keys.reject { |k, _| k == :name })
if params.keys.include?('latitude_column') && params.keys.include?('longitude_column')
latitude_column = params[:latitude_column] == 'nil' ? nil : params[:latitude_column].try(:to_sym)
longitude_column = params[:longitude_column] == 'nil' ? nil : params[:longitude_column].try(:to_sym)
table.georeference_from!(latitude_column: latitude_column, longitude_column: longitude_column)
table.update_bounding_box
render_jsonp(TablePresenter.new(table, current_user, self).to_poro.merge(warnings: warnings))
return
end
if @user_table.save
render_jsonp(TablePresenter.new(table, current_user, self).to_poro.merge(warnings: warnings))
else
render_jsonp({ errors: table.errors.full_messages }, 400)
end
rescue => e
CartoDB::Logger.error(message: 'Error updating table', exception: e)
render_jsonp({ errors: [translate_error(e.message.split("\n").first)] }, 400)
end
private
def load_user_table
@user_table = Carto::Helpers::TableLocator.new.get_by_id_or_name(params[:id], current_user)
raise RecordNotFound unless @user_table
end
def read_privileges?
head(403) unless current_user && @user_table.visualization.is_viewable_by_user?(current_user)
end
def write_privileges?
head(401) unless current_user && @user_table.visualization.writable_by?(current_user)
end
end
end
end
@@ -0,0 +1,17 @@
class Carto::Api::TagPresenter
def initialize(tag_name, current_viewer, context)
@tag_name = tag_name
@current_viewer = current_viewer
@context = context
end
def to_search_preview_poro
{
type: "tag",
name: @tag_name,
url: CartoDB.url(@context, "tag_search", user: @current_viewer, params: { q: @tag_name })
}
end
end
@@ -0,0 +1,61 @@
require_dependency 'carto/controller_helper'
module Carto
module Api
class TagsController < ::Api::ApplicationController
include Carto::ControllerHelper
include PagedSearcher
ssl_required
before_filter :load_parameters
rescue_from StandardError, with: :rescue_from_standard_error
rescue_from Carto::ParamCombinationInvalidError, with: :rescue_from_carto_error
DEFAULT_TAGS_PER_PAGE = 6
def index
query_builder = tag_query_builder
result = query_builder.build_paged(@page, @per_page)
total_count = query_builder.total_count
render json: format_response(result, total_count)
end
private
def tag_query_builder
query_builder = Carto::TagQueryBuilder.new
.with_types(@types)
.with_partial_match(@pattern)
return query_builder.with_owned_by_or_shared_with_user_id(current_viewer.id) if @include_shared
query_builder.with_owned_by_user_id(current_viewer.id)
end
def load_parameters
@page, @per_page = page_per_page_params(default_per_page: DEFAULT_TAGS_PER_PAGE)
@pattern = params[:q]
@include_shared = params[:include_shared] == 'true'
@types = params.fetch(:types, "").split(',')
if (@types - Carto::Visualization::VALID_TYPES).present?
raise Carto::ParamCombinationInvalidError.new(:types, Carto::Visualization::VALID_TYPES)
end
end
def format_response(result, total_count)
paged_result(
result: result,
total_count: total_count,
page: @page,
per_page: @per_page,
params: params
) { |params| api_v3_users_tags_url(params) }
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More