Initial commit
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user