Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
@@ -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