domainnameapi 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +47 -0
- data/LICENSE +201 -0
- data/README.md +995 -0
- data/lib/domainnameapi/client.rb +294 -0
- data/lib/domainnameapi/php.rb +322 -0
- data/lib/domainnameapi/rest.rb +1747 -0
- data/lib/domainnameapi/telemetry.rb +472 -0
- data/lib/domainnameapi/transport.rb +89 -0
- data/lib/domainnameapi/version.rb +6 -0
- data/lib/domainnameapi.rb +16 -0
- metadata +56 -0
|
@@ -0,0 +1,1747 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module DomainNameApi
|
|
6
|
+
# Raised inside the client when the gateway answers with an error status or
|
|
7
|
+
# the request fails in transport. Never escapes a public method: every
|
|
8
|
+
# method catches it and returns the error envelope. `code` is the HTTP
|
|
9
|
+
# status (or the curl-style transport error number).
|
|
10
|
+
class ApiError < StandardError
|
|
11
|
+
attr_reader :code
|
|
12
|
+
|
|
13
|
+
def initialize(message, code = 0)
|
|
14
|
+
super(message)
|
|
15
|
+
@code = code
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Port of php-dna's DNARest: one method per gateway operation, each
|
|
20
|
+
# returning php-dna's result structure (Hashes with String keys).
|
|
21
|
+
#
|
|
22
|
+
# Use DomainNameApi::Client, the public facade; Rest is its engine.
|
|
23
|
+
class Rest
|
|
24
|
+
include Telemetry
|
|
25
|
+
|
|
26
|
+
URL_PROD = 'https://api.domainresellerapi.com/api/v1'
|
|
27
|
+
URL_OTE = 'https://ote.domainresellerapi.com/api/v1'
|
|
28
|
+
USER_AGENT = "DomainNameAPI-Ruby/#{VERSION}"
|
|
29
|
+
|
|
30
|
+
RESULT_OK = 'OK'
|
|
31
|
+
RESULT_ERROR = 'ERROR'
|
|
32
|
+
RESULT_SUCCESS = 'SUCCESS'
|
|
33
|
+
|
|
34
|
+
DEFAULT_TIMEOUT = 30
|
|
35
|
+
DEFAULT_CONNECT_TIMEOUT = 10
|
|
36
|
+
DEFAULT_REASON = 'Owner request'
|
|
37
|
+
DEFAULT_NAMESERVERS = ['ns1.domainnameapi.com', 'ns2.domainnameapi.com'].freeze
|
|
38
|
+
|
|
39
|
+
# Minimum gap between two /domains/zones calls, in seconds.
|
|
40
|
+
ZONE_MIN_INTERVAL = 1.0
|
|
41
|
+
|
|
42
|
+
DEFAULT_ERRORS = {
|
|
43
|
+
'DOMAIN_DETAILS' => ['Invalid domain details! Details format is not valid',
|
|
44
|
+
'The provided domain details are not in the expected format'],
|
|
45
|
+
'CREDENTIALS' => ['Invalid reseller ID or API key', 'The provided API credentials are invalid'],
|
|
46
|
+
'DOMAIN_LIST' => ['The domain list is invalid or contains multiple entries!',
|
|
47
|
+
'The domain list response format is incorrect or unexpected'],
|
|
48
|
+
'TLD_LIST' => ['TLD info is not a valid array or more than one TLD info has returned!',
|
|
49
|
+
'The TLD list response is not in the expected format'],
|
|
50
|
+
'RESPONSE' => ['Invalid response received from server! Response is empty.', 'The API response is empty or null'],
|
|
51
|
+
'RESPONSE_FORMAT' => ['Invalid response received from server! Response format is not valid.',
|
|
52
|
+
'The API response format is not in the expected structure'],
|
|
53
|
+
'RESPONSE_COUNT' => ['Invalid parameters passed to function! Response data contains more than one result!',
|
|
54
|
+
'The API response contains multiple results when only one was expected'],
|
|
55
|
+
'RESPONSE_CODE' => ['Invalid parameters passed to function! Operation result or Error code not received from server',
|
|
56
|
+
'The API response is missing required operation result or error code fields'],
|
|
57
|
+
'RESPONSE_REST' => ['Invalid parameters passed to function! REST return is not a valid array!',
|
|
58
|
+
'The REST response is not in a valid array format'],
|
|
59
|
+
'CONTACT_INFO' => ['Invalid response received from server! Contact info is not a valid array or more than one contact info has returned!',
|
|
60
|
+
'The contact information response is not in the expected format'],
|
|
61
|
+
'CONTACT_SAVE' => ['Invalid response received from server! Contact info could not be saved!',
|
|
62
|
+
'The contact information could not be saved on the server'],
|
|
63
|
+
'DOMAIN_TRANSFER_REQUEST' => ['Invalid response received from server! Domain transfer request could not be completed!',
|
|
64
|
+
'The domain transfer request failed to complete'],
|
|
65
|
+
'DOMAIN_RENEW' => ['Invalid response received from server! Domain renew request could not be completed!',
|
|
66
|
+
'The domain renewal request failed to complete'],
|
|
67
|
+
'DOMAIN_REGISTER' => ['Invalid response received from server! Domain register request could not be completed!',
|
|
68
|
+
'The domain registration request failed to complete'],
|
|
69
|
+
'DOMAIN_SYNC' => ['Invalid response received from server! Domain sync request could not be completed!',
|
|
70
|
+
'The domain synchronization request failed to complete']
|
|
71
|
+
}.freeze
|
|
72
|
+
|
|
73
|
+
# Gateway DomainStatus enum -> label.
|
|
74
|
+
DOMAIN_STATUS_MAP = {
|
|
75
|
+
0 => 'Active', 1 => 'WaitingForRegistration', 2 => 'WaitingForDocument',
|
|
76
|
+
3 => 'WaitingForIncomingTransfer', 4 => 'TransferredOut', 7 => 'PendingDelete',
|
|
77
|
+
8 => 'Deleted', 9 => 'ConfirmationEmailSend', 11 => 'WaitingForOutgoingTransfer',
|
|
78
|
+
12 => 'PendingHold', 15 => 'ModificationPending', 18 => 'InCase', 19 => 'PendingRestore'
|
|
79
|
+
}.freeze
|
|
80
|
+
|
|
81
|
+
# Zone record types the gateway accepts (probed against the live gateway).
|
|
82
|
+
ZONE_RECORD_TYPES = %w[A AAAA ALIAS CAA CNAME DS MX NAPTR PTR SRV TLSA TXT].freeze
|
|
83
|
+
|
|
84
|
+
# Redirect types accepted by /domains/forwards.
|
|
85
|
+
FORWARD_TYPES = %w[Standard Frame].freeze
|
|
86
|
+
|
|
87
|
+
HTTP_REASONS = {
|
|
88
|
+
400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden',
|
|
89
|
+
404 => 'Not Found', 405 => 'Method Not Allowed', 408 => 'Request Timeout', 409 => 'Conflict',
|
|
90
|
+
422 => 'Unprocessable Entity', 429 => 'Too Many Requests', 500 => 'Internal Server Error',
|
|
91
|
+
502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout'
|
|
92
|
+
}.freeze
|
|
93
|
+
|
|
94
|
+
BOILERPLATE_RE = /Exception of type '[^']*' was thrown/i.freeze
|
|
95
|
+
|
|
96
|
+
attr_accessor :zone_min_interval
|
|
97
|
+
|
|
98
|
+
# @param reseller_id [String] Reseller ID (UUID)
|
|
99
|
+
# @param api_key [String] API key
|
|
100
|
+
# @param test_mode [Boolean] use the OTE (test) gateway
|
|
101
|
+
# @param transport [#call, nil] HTTP transport (see NetHttpTransport)
|
|
102
|
+
# @param sentry_dsn [String, nil] Sentry DSN override ('' disables)
|
|
103
|
+
# @param zone_min_interval [Float, nil] pacing between /domains/zones calls
|
|
104
|
+
def initialize(reseller_id, api_key, test_mode = false, transport: nil, sentry_dsn: nil, zone_min_interval: nil)
|
|
105
|
+
@service_url = test_mode ? URL_OTE : URL_PROD
|
|
106
|
+
@reseller_id = reseller_id
|
|
107
|
+
@token = api_key
|
|
108
|
+
@transport = transport || NetHttpTransport.new
|
|
109
|
+
@ssl_verify = true
|
|
110
|
+
@error_reporting_enabled = true
|
|
111
|
+
@telemetry_minimal = false
|
|
112
|
+
@sentry_dsn = sentry_dsn.nil? ? DEFAULT_SENTRY_DSN : sentry_dsn.to_s
|
|
113
|
+
@telemetry_sender = nil
|
|
114
|
+
@zone_min_interval = zone_min_interval.nil? ? ZONE_MIN_INTERVAL : zone_min_interval.to_f
|
|
115
|
+
@last_zone_request_at = 0.0
|
|
116
|
+
@last_request = {}
|
|
117
|
+
@last_response = {}
|
|
118
|
+
@last_response_headers = {}
|
|
119
|
+
@last_parsed_response = {}
|
|
120
|
+
@last_function = ''
|
|
121
|
+
@start_at = monotonic
|
|
122
|
+
@start_wall = Time.now
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# --- settings and diagnostics -----------------------------------------
|
|
126
|
+
|
|
127
|
+
def set_ssl_verification(enabled)
|
|
128
|
+
@ssl_verify = enabled ? true : false
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def set_transport(transport)
|
|
133
|
+
@transport = transport
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def get_request_data
|
|
138
|
+
@last_request
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def set_request_data(request)
|
|
142
|
+
@last_request = request
|
|
143
|
+
self
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def get_response_data
|
|
147
|
+
@last_response
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def set_response_data(response)
|
|
151
|
+
@last_response = response
|
|
152
|
+
self
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def get_response_headers
|
|
156
|
+
@last_response_headers
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def set_response_headers(headers)
|
|
160
|
+
@last_response_headers = headers
|
|
161
|
+
self
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def get_parsed_response_data
|
|
165
|
+
@last_parsed_response
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def set_parsed_response_data(response)
|
|
169
|
+
@last_parsed_response = response
|
|
170
|
+
self
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def get_last_function
|
|
174
|
+
@last_function
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def set_last_function(function)
|
|
178
|
+
@last_function = function
|
|
179
|
+
self
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def get_service_url
|
|
183
|
+
@service_url
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def set_service_url(url)
|
|
187
|
+
@service_url = url
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# --- account -------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def get_reseller_details
|
|
194
|
+
response = request('GET', 'deposit/accounts/me')
|
|
195
|
+
resp = {}
|
|
196
|
+
if Php.isset?(response, 'resellerId')
|
|
197
|
+
resp['result'] = RESULT_OK
|
|
198
|
+
resp['id'] = response['resellerId']
|
|
199
|
+
resp['active'] = true
|
|
200
|
+
resp['name'] = Php.get(response, 'resellerName') || ''
|
|
201
|
+
usd = Php.number_format(Php.get(response, 'usdBalance') || 0, 2)
|
|
202
|
+
resp['balance'] = usd
|
|
203
|
+
resp['currency'] = 'USD'
|
|
204
|
+
resp['symbol'] = '$'
|
|
205
|
+
resp['balances'] = [
|
|
206
|
+
{ 'balance' => usd, 'currency' => 'USD', 'symbol' => '$' },
|
|
207
|
+
{ 'balance' => Php.number_format(Php.get(response, 'tryBalance') || 0, 2), 'currency' => 'TL', 'symbol' => 'TL' }
|
|
208
|
+
]
|
|
209
|
+
else
|
|
210
|
+
resp['result'] = RESULT_ERROR
|
|
211
|
+
resp['error'] = set_error('CREDENTIALS', 'Invalid response format', 'Response does not contain required fields')
|
|
212
|
+
end
|
|
213
|
+
resp
|
|
214
|
+
rescue ApiError => e
|
|
215
|
+
error_envelope(e, 'RESELLER_DETAILS')
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
CURRENCY_MAP = {
|
|
219
|
+
'USD' => { 'id' => 2, 'name' => 'USD', 'symbol' => '$' },
|
|
220
|
+
'TRY' => { 'id' => 1, 'name' => 'TL', 'symbol' => 'TL' },
|
|
221
|
+
'EUR' => { 'id' => 3, 'name' => 'EUR', 'symbol' => '€' },
|
|
222
|
+
'GBP' => { 'id' => 4, 'name' => 'GBP', 'symbol' => '£' }
|
|
223
|
+
}.freeze
|
|
224
|
+
|
|
225
|
+
def get_current_balance(currency_id = 'USD')
|
|
226
|
+
currency_name = Php.upper(currency_id)
|
|
227
|
+
mapped = CURRENCY_MAP[currency_name] || { 'id' => 0, 'name' => currency_name, 'symbol' => '' }
|
|
228
|
+
begin
|
|
229
|
+
response = request('GET', 'deposit/accounts/me', { 'currency' => currency_name })
|
|
230
|
+
balance_key = "#{Php.lower(currency_id)}Balance"
|
|
231
|
+
unless response.is_a?(Hash) && response.key?(balance_key)
|
|
232
|
+
# The account only reports USD and TRY balances.
|
|
233
|
+
return {
|
|
234
|
+
'ErrorCode' => 'BALANCE',
|
|
235
|
+
'OperationMessage' => "No #{currency_name} balance is reported for this account. Available: USD, TRY.",
|
|
236
|
+
'OperationResult' => 'FAILED',
|
|
237
|
+
'Balance' => '0.00',
|
|
238
|
+
'CurrencyId' => mapped['id'],
|
|
239
|
+
'CurrencyInfo' => nil,
|
|
240
|
+
'CurrencyName' => mapped['name'],
|
|
241
|
+
'CurrencySymbol' => mapped['symbol']
|
|
242
|
+
}
|
|
243
|
+
end
|
|
244
|
+
{
|
|
245
|
+
'ErrorCode' => 0,
|
|
246
|
+
'OperationMessage' => 'Command completed succesfully.',
|
|
247
|
+
'OperationResult' => 'SUCCESS',
|
|
248
|
+
'Balance' => Php.number_format(Php.get(response, balance_key) || 0, 2),
|
|
249
|
+
'CurrencyId' => mapped['id'],
|
|
250
|
+
'CurrencyInfo' => nil,
|
|
251
|
+
'CurrencyName' => mapped['name'],
|
|
252
|
+
'CurrencySymbol' => mapped['symbol']
|
|
253
|
+
}
|
|
254
|
+
rescue ApiError => e
|
|
255
|
+
{
|
|
256
|
+
'ErrorCode' => format_error_code(e.code),
|
|
257
|
+
'OperationMessage' => e.message,
|
|
258
|
+
'OperationResult' => 'FAILED',
|
|
259
|
+
'Balance' => '0.00',
|
|
260
|
+
'CurrencyId' => mapped['id'],
|
|
261
|
+
'CurrencyInfo' => nil,
|
|
262
|
+
'CurrencyName' => mapped['name'],
|
|
263
|
+
'CurrencySymbol' => mapped['symbol']
|
|
264
|
+
}
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# --- search and lists ------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
def check_availability(domains, extensions, period, command = 'create')
|
|
271
|
+
queries = []
|
|
272
|
+
Php.values(domains).each do |domain|
|
|
273
|
+
Php.values(extensions).each do |tld|
|
|
274
|
+
queries << { 'domainName' => "#{Php.to_s(domain)}.#{Php.ltrim(tld, '.')}" }
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
if queries.empty?
|
|
279
|
+
return {
|
|
280
|
+
'result' => RESULT_ERROR,
|
|
281
|
+
'error' => set_error('AVAILABILITY', 'Domain names not found.', 'No domain names provided for availability check')
|
|
282
|
+
}
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
response = request('POST', 'domains/bulk-search', queries)
|
|
286
|
+
|
|
287
|
+
data = []
|
|
288
|
+
items = Php.get(response, 'infos')
|
|
289
|
+
items = response if items.nil?
|
|
290
|
+
if Php.array?(items)
|
|
291
|
+
Php.values(items).each do |item|
|
|
292
|
+
next unless Php.array?(item)
|
|
293
|
+
|
|
294
|
+
raw_tld = Php.get(item, 'tld')
|
|
295
|
+
if raw_tld.nil?
|
|
296
|
+
name = Php.to_s(Php.get(item, 'domainName') || '')
|
|
297
|
+
dot = name.rindex('.')
|
|
298
|
+
raw_tld = dot ? name[(dot + 1)..] : ''
|
|
299
|
+
end
|
|
300
|
+
tld = Php.lower(raw_tld)
|
|
301
|
+
domain_raw = Php.get(item, 'domainName') || ''
|
|
302
|
+
lower = Php.lower(domain_raw)
|
|
303
|
+
suffix = ".#{tld}"
|
|
304
|
+
domain_name = tld != '' && lower.end_with?(suffix) ? lower[0, lower.length - suffix.length] : lower
|
|
305
|
+
status = Php.lower(Php.get(item, 'status') || '')
|
|
306
|
+
available = %w[available 1 true].include?(status)
|
|
307
|
+
price = Php.get(item, 'price')
|
|
308
|
+
period_out = Php.get(item, 'period')
|
|
309
|
+
is_fee = Php.get(item, 'isPremium')
|
|
310
|
+
|
|
311
|
+
data << {
|
|
312
|
+
'TLD' => tld,
|
|
313
|
+
'DomainName' => domain_name,
|
|
314
|
+
'Status' => available ? 'available' : 'notavailable',
|
|
315
|
+
'Command' => command,
|
|
316
|
+
'Period' => period_out.nil? ? period : period_out,
|
|
317
|
+
'IsFee' => is_fee.nil? ? false : is_fee,
|
|
318
|
+
'Price' => price.nil? ? nil : Php.number_format(price, 4),
|
|
319
|
+
'Currency' => Php.get(item, 'currency'),
|
|
320
|
+
'Reason' => Php.get(item, 'reason')
|
|
321
|
+
}
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
data
|
|
325
|
+
rescue ApiError => e
|
|
326
|
+
error_envelope(e, 'AVAILABILITY')
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def get_list(extra_parameters = {})
|
|
330
|
+
params = Php.to_hash(extra_parameters)
|
|
331
|
+
params['MaxResultCount'] = Php.to_i(params['PageSize']) unless params['PageSize'].nil?
|
|
332
|
+
unless params['PageNumber'].nil?
|
|
333
|
+
page_size = Php.to_i(params['PageSize'].nil? ? 200 : params['PageSize'])
|
|
334
|
+
params['SkipCount'] = Php.to_i(params['PageNumber']) * page_size
|
|
335
|
+
end
|
|
336
|
+
unless params['OrderColumn'].nil?
|
|
337
|
+
direction = Php.upper(params['OrderDirection'].nil? ? 'ASC' : params['OrderDirection']) == 'DESC' ? 'DESC' : 'ASC'
|
|
338
|
+
params['Sorting'] = "#{Php.to_s(params['OrderColumn'])} #{direction}"
|
|
339
|
+
end
|
|
340
|
+
%w[PageNumber PageSize OrderColumn OrderDirection].each { |k| params.delete(k) }
|
|
341
|
+
|
|
342
|
+
merged = { 'MaxResultCount' => 200, 'SkipCount' => 0 }.merge(params)
|
|
343
|
+
response = request('GET', 'domains', merged)
|
|
344
|
+
|
|
345
|
+
items = Php.get(response, 'items')
|
|
346
|
+
domains = if items.nil?
|
|
347
|
+
[]
|
|
348
|
+
else
|
|
349
|
+
php_map(items) { |item| list_item(item) }
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
{
|
|
353
|
+
'data' => { 'Domains' => domains },
|
|
354
|
+
'result' => RESULT_OK,
|
|
355
|
+
'TotalCount' => Php.to_i(Php.get(response, 'totalCount') || 0)
|
|
356
|
+
}
|
|
357
|
+
rescue ApiError => e
|
|
358
|
+
error_envelope(e, 'DOMAIN_LIST')
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
PRICE_TYPES = {
|
|
362
|
+
'register' => 'registration', 'renew' => 'renew', 'transfer' => 'transfer',
|
|
363
|
+
'restore' => 'restore', 'refund' => 'refund', 'backorder' => 'backorder'
|
|
364
|
+
}.freeze
|
|
365
|
+
|
|
366
|
+
def get_tld_list(count = 20)
|
|
367
|
+
response = request('GET', 'products/tlds', { 'MaxResultCount' => count, 'SkipCount' => 0 })
|
|
368
|
+
|
|
369
|
+
tld_data = []
|
|
370
|
+
id_counter = 1
|
|
371
|
+
items = Php.get(response, 'items')
|
|
372
|
+
if Php.array?(items)
|
|
373
|
+
Php.values(items).each do |tld|
|
|
374
|
+
pricing = {}
|
|
375
|
+
currencies = {}
|
|
376
|
+
first_price = Php.get(tld, 'prices', 0)
|
|
377
|
+
if Php.array?(first_price)
|
|
378
|
+
PRICE_TYPES.each do |api_type, out_type|
|
|
379
|
+
api_value = Php.get(first_price, api_type)
|
|
380
|
+
next if api_value.nil?
|
|
381
|
+
|
|
382
|
+
if Php.array?(api_value) && Php.isset?(api_value, 0)
|
|
383
|
+
Php.values(api_value).each do |info|
|
|
384
|
+
next unless Php.array?(info)
|
|
385
|
+
|
|
386
|
+
add_price(pricing, currencies, out_type, info)
|
|
387
|
+
end
|
|
388
|
+
elsif Php.array?(api_value)
|
|
389
|
+
add_price(pricing, currencies, out_type, api_value)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
pricing.each_key do |type|
|
|
395
|
+
pricing[type] = pricing[type].sort_by { |k, _| k }.to_h { |k, v| [k.to_s, v] }
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
max_char = Php.get(tld, 'constraints', 'maxLenght')
|
|
399
|
+
max_period = Php.get(tld, 'maxRegistrationPeriod')
|
|
400
|
+
min_char = Php.get(tld, 'constraints', 'minLength')
|
|
401
|
+
min_period = Php.get(tld, 'minRegistrationPeriod')
|
|
402
|
+
status = Php.get(tld, 'status')
|
|
403
|
+
tld_data << {
|
|
404
|
+
'id' => id_counter,
|
|
405
|
+
'status' => status.nil? ? 'Active' : status,
|
|
406
|
+
'maxchar' => max_char.nil? ? 63 : max_char,
|
|
407
|
+
'maxperiod' => max_period.nil? ? 10 : max_period,
|
|
408
|
+
'minchar' => min_char.nil? ? 1 : min_char,
|
|
409
|
+
'minperiod' => min_period.nil? ? 1 : min_period,
|
|
410
|
+
'tld' => Php.get(tld, 'name'),
|
|
411
|
+
'pricing' => pricing,
|
|
412
|
+
'currencies' => currencies
|
|
413
|
+
}
|
|
414
|
+
id_counter += 1
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
{ 'data' => tld_data, 'result' => RESULT_OK }
|
|
419
|
+
rescue ApiError => e
|
|
420
|
+
error_envelope(e, 'TLD_LIST')
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# --- domain ----------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
def get_details(domain_name)
|
|
426
|
+
response = request('GET', 'domains/info', { 'DomainName' => domain_name })
|
|
427
|
+
parse_domain_info(response)
|
|
428
|
+
rescue ApiError => e
|
|
429
|
+
error_envelope(e, 'DOMAIN_DETAILS')
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def sync_from_registry(domain_name)
|
|
433
|
+
get_details(domain_name)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def modify_name_server(domain_name, name_servers)
|
|
437
|
+
payload = { 'domainName' => domain_name, 'nameServers' => normalize_host_name_list(name_servers) }
|
|
438
|
+
response = request('PUT', 'domains/dns/name-server', payload)
|
|
439
|
+
ns = Php.get(response, 'nameServers')
|
|
440
|
+
{ 'result' => RESULT_OK, 'data' => { 'NameServers' => ns.nil? ? payload['nameServers'] : ns } }
|
|
441
|
+
rescue ApiError => e
|
|
442
|
+
error_envelope(e, 'MODIFY_NS')
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def enable_theft_protection_lock(domain_name)
|
|
446
|
+
request('POST', 'domains/lock', { 'domainName' => domain_name })
|
|
447
|
+
{ 'result' => RESULT_OK, 'data' => { 'LockStatus' => true } }
|
|
448
|
+
rescue ApiError => e
|
|
449
|
+
error_envelope(e, 'ENABLE_LOCK')
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def disable_theft_protection_lock(domain_name)
|
|
453
|
+
request('POST', 'domains/unlock', { 'domainName' => domain_name })
|
|
454
|
+
{ 'result' => RESULT_OK, 'data' => { 'LockStatus' => false } }
|
|
455
|
+
rescue ApiError => e
|
|
456
|
+
error_envelope(e, 'DISABLE_LOCK')
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# `reason` is kept for signature compatibility only: the gateway's
|
|
460
|
+
# DomainPrivacyInput has no reason field, so it is not sent.
|
|
461
|
+
def modify_privacy_protection_status(domain_name, status, _reason = DEFAULT_REASON)
|
|
462
|
+
enabled = if status == true || status == false
|
|
463
|
+
status
|
|
464
|
+
elsif Php.array?(status)
|
|
465
|
+
false
|
|
466
|
+
else
|
|
467
|
+
Php.filter_bool(status)
|
|
468
|
+
end
|
|
469
|
+
request('POST', 'domains/privacy', { 'domainName' => domain_name, 'privacyStatus' => enabled })
|
|
470
|
+
{ 'data' => { 'PrivacyProtectionStatus' => enabled }, 'result' => RESULT_OK }
|
|
471
|
+
rescue ApiError => e
|
|
472
|
+
error_envelope(e, 'MODIFY_PRIVACY')
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def renew(domain_name, period)
|
|
476
|
+
response = request('POST', 'domains/renew', { 'domainName' => domain_name, 'period' => period })
|
|
477
|
+
failed = operation_failure(response, 'RENEW_DOMAIN')
|
|
478
|
+
return failed if failed
|
|
479
|
+
|
|
480
|
+
expiration = Php.get(response, 'expirationDate')
|
|
481
|
+
if Php.truthy?(expiration)
|
|
482
|
+
{ 'result' => RESULT_OK, 'data' => { 'ExpirationDate' => format_api_date(expiration) } }
|
|
483
|
+
else
|
|
484
|
+
send_error_to_sentry_async(ApiError.new("[DOMAIN_RENEW] #{DEFAULT_ERRORS['DOMAIN_RENEW'][1]}", 0))
|
|
485
|
+
{ 'result' => RESULT_ERROR, 'error' => set_error('DOMAIN_RENEW') }
|
|
486
|
+
end
|
|
487
|
+
rescue ApiError => e
|
|
488
|
+
error_envelope(e, 'RENEW_DOMAIN')
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def register_with_contact_info(domain_name, period, contacts, name_servers = [], epp_lock = true,
|
|
492
|
+
privacy_lock = false, additional_attributes = {})
|
|
493
|
+
payload_contacts = Php.pairs(Php.stringify_keys(contacts)).map do |type, details|
|
|
494
|
+
parse_contact(details, Php.ucfirst(Php.lower(type)))
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
ns = normalize_host_name_list(name_servers)
|
|
498
|
+
ns = DEFAULT_NAMESERVERS.dup if ns.empty?
|
|
499
|
+
|
|
500
|
+
attrs = Php.to_hash(Php.to_array(Php.stringify_keys(additional_attributes)))
|
|
501
|
+
attrs = normalize_tr_attributes(attrs) if Php.lower(domain_name).end_with?('.tr')
|
|
502
|
+
attrs = stringify_attributes(attrs)
|
|
503
|
+
|
|
504
|
+
payload = {
|
|
505
|
+
'domainName' => domain_name,
|
|
506
|
+
'period' => period,
|
|
507
|
+
'nameServers' => ns,
|
|
508
|
+
'isLocked' => epp_lock,
|
|
509
|
+
'privacyEnabled' => privacy_lock,
|
|
510
|
+
'contacts' => payload_contacts,
|
|
511
|
+
'tldAttributes' => attrs
|
|
512
|
+
}
|
|
513
|
+
response = request('POST', 'domains/register-with-contacts', payload)
|
|
514
|
+
failed = operation_failure(response, 'REGISTER_DOMAIN')
|
|
515
|
+
return failed if failed
|
|
516
|
+
|
|
517
|
+
parse_domain_info(response)
|
|
518
|
+
rescue ApiError => e
|
|
519
|
+
error_envelope(e, 'REGISTER_DOMAIN')
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# --- contacts --------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
CONTACT_TYPE_IN = { 'Admin' => 'Administrative', 'Billing' => 'Billing', 'Tech' => 'Technical',
|
|
525
|
+
'Registrant' => 'Registrant' }.freeze
|
|
526
|
+
# get_contacts: matched case-insensitively, long and short spellings.
|
|
527
|
+
CONTACT_TYPE_LOOKUP = {
|
|
528
|
+
'admin' => 'Administrative', 'administrative' => 'Administrative', 'billing' => 'Billing',
|
|
529
|
+
'tech' => 'Technical', 'technical' => 'Technical', 'registrant' => 'Registrant', 'owner' => 'Registrant'
|
|
530
|
+
}.freeze
|
|
531
|
+
CONTACT_TYPE_OUT = { 'Administrative' => 'Admin', 'Technical' => 'Tech', 'Billing' => 'Billing',
|
|
532
|
+
'Registrant' => 'Registrant' }.freeze
|
|
533
|
+
|
|
534
|
+
def get_contacts(domain_name)
|
|
535
|
+
info = request('GET', 'domains/info', { 'DomainName' => domain_name })
|
|
536
|
+
contacts = {}
|
|
537
|
+
list = Php.get(info, 'contacts')
|
|
538
|
+
if Php.array?(list)
|
|
539
|
+
Php.values(list).each do |item|
|
|
540
|
+
next unless Php.array?(item)
|
|
541
|
+
|
|
542
|
+
api_type = Php.trim(Php.to_s(first_set(Php.get(item, 'contactType'), '')))
|
|
543
|
+
key = CONTACT_TYPE_LOOKUP[Php.lower(api_type)] || Php.ucfirst(Php.lower(api_type))
|
|
544
|
+
next if key == ''
|
|
545
|
+
|
|
546
|
+
merged = Php.to_hash(item).merge('type' => key)
|
|
547
|
+
contacts[key] = parse_contact_info(merged)
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
ordered = {}
|
|
552
|
+
%w[Administrative Billing Registrant Technical].each do |key|
|
|
553
|
+
ordered[key] = contacts.delete(key) unless contacts[key].nil?
|
|
554
|
+
end
|
|
555
|
+
contacts.each { |key, contact| ordered[key] = contact }
|
|
556
|
+
{ 'data' => { 'contacts' => ordered }, 'result' => RESULT_OK }
|
|
557
|
+
rescue ApiError => e
|
|
558
|
+
error_envelope(e, 'GET_CONTACTS')
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def save_contacts(domain_name, contacts)
|
|
562
|
+
payload_contacts = Php.pairs(Php.stringify_keys(contacts)).map do |type, details|
|
|
563
|
+
parse_contact(details, Php.ucfirst(Php.lower(type)))
|
|
564
|
+
end
|
|
565
|
+
response = request('PUT', 'domains/contacts/update', { 'domainName' => domain_name, 'contacts' => payload_contacts })
|
|
566
|
+
|
|
567
|
+
parsed = {}
|
|
568
|
+
list = Php.get(response, 'contacts')
|
|
569
|
+
if Php.array?(list)
|
|
570
|
+
Php.values(list).each do |contact|
|
|
571
|
+
parsed[Php.ucfirst(Php.lower(Php.get(contact, 'type')))] = parse_contact_info(contact)
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
{ 'result' => RESULT_OK, 'data' => { 'contacts' => parsed } }
|
|
575
|
+
rescue ApiError => e
|
|
576
|
+
error_envelope(e, 'SAVE_CONTACTS')
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# --- child nameservers -----------------------------------------------------
|
|
580
|
+
|
|
581
|
+
def add_child_name_server(domain_name, name_server, ip_address)
|
|
582
|
+
payload = {
|
|
583
|
+
'domainName' => domain_name,
|
|
584
|
+
'hostName' => normalize_host_name(name_server),
|
|
585
|
+
'ipAddresses' => [{ 'ipAddress' => ip_address, 'ipVersion' => Php.ipv4?(ip_address) ? 'v4' : 'v6' }]
|
|
586
|
+
}
|
|
587
|
+
request('POST', 'domains/dns/host', payload)
|
|
588
|
+
{ 'result' => RESULT_OK, 'data' => { 'NameServer' => name_server, 'IPAdresses' => [ip_address] } }
|
|
589
|
+
rescue ApiError => e
|
|
590
|
+
error_envelope(e, 'ADD_CHILD_NS')
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def delete_child_name_server(domain_name, name_server)
|
|
594
|
+
request('DELETE', 'domains/dns/host', { 'domainName' => domain_name, 'hostName' => normalize_host_name(name_server) })
|
|
595
|
+
{ 'result' => RESULT_OK, 'data' => { 'NameServer' => name_server } }
|
|
596
|
+
rescue ApiError => e
|
|
597
|
+
error_envelope(e, 'DELETE_CHILD_NS')
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def modify_child_name_server(domain_name, name_server, ip_address)
|
|
601
|
+
host = normalize_host_name(name_server)
|
|
602
|
+
payload = {
|
|
603
|
+
'domainName' => domain_name,
|
|
604
|
+
'hostName' => host,
|
|
605
|
+
'newHostName' => host,
|
|
606
|
+
'ipAddresses' => [{ 'ipAddress' => ip_address, 'ipVersion' => Php.ipv4?(ip_address) ? 'v4' : 'v6' }]
|
|
607
|
+
}
|
|
608
|
+
response = request('PUT', 'domains/dns/host', payload)
|
|
609
|
+
host_out = Php.get(response, 'hostName')
|
|
610
|
+
ips_out = Php.get(response, 'ipAddresses')
|
|
611
|
+
{
|
|
612
|
+
'result' => RESULT_OK,
|
|
613
|
+
'data' => { 'NameServer' => host_out.nil? ? host : host_out, 'IPAdresses' => ips_out.nil? ? [ip_address] : ips_out }
|
|
614
|
+
}
|
|
615
|
+
rescue ApiError => e
|
|
616
|
+
error_envelope(e, 'MODIFY_CHILD_NS')
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# --- transfers -------------------------------------------------------------
|
|
620
|
+
|
|
621
|
+
# Contacts are never sent with a transfer: the registrar handles them, so
|
|
622
|
+
# the request always carries an empty contact list.
|
|
623
|
+
def transfer(domain_name, epp_code, period)
|
|
624
|
+
payload = { 'domainName' => domain_name, 'authCode' => epp_code, 'period' => period, 'contacts' => [] }
|
|
625
|
+
response = request('POST', 'domains/transfer', payload)
|
|
626
|
+
failed = operation_failure(response, 'TRANSFER_DOMAIN')
|
|
627
|
+
return failed if failed
|
|
628
|
+
|
|
629
|
+
parse_domain_info(response)
|
|
630
|
+
rescue ApiError => e
|
|
631
|
+
error_envelope(e, 'TRANSFER_DOMAIN')
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def cancel_transfer(domain_name)
|
|
635
|
+
transfer_action(domain_name, 'cancel', 'Cancelled', 'CANCEL_TRANSFER')
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def approve_transfer(domain_name)
|
|
639
|
+
transfer_action(domain_name, 'approve', 'Approved', 'APPROVE_TRANSFER')
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def reject_transfer(domain_name)
|
|
643
|
+
transfer_action(domain_name, 'reject', 'Rejected', 'REJECT_TRANSFER')
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def check_transfer(domain_name, auth_code)
|
|
647
|
+
response = request('POST', 'domains/transfers/check', { 'domainName' => domain_name, 'authCode' => auth_code })
|
|
648
|
+
|
|
649
|
+
available = Php.truthy?(Php.get(response, 'transferAvailabilityStatus'))
|
|
650
|
+
additional = Php.get(response, 'additionalAttributes')
|
|
651
|
+
message = Php.get(response, 'message')
|
|
652
|
+
message_key = Php.get(response, 'messageKey')
|
|
653
|
+
envelope = {
|
|
654
|
+
'result' => available ? RESULT_OK : RESULT_ERROR,
|
|
655
|
+
'data' => {
|
|
656
|
+
'TransferAvailabilityStatus' => available,
|
|
657
|
+
'AuthCodeIsRequired' => Php.truthy?(Php.get(response, 'authCodeIsRequired')),
|
|
658
|
+
'AuthCodeIsValid' => Php.truthy?(Php.get(response, 'authCodeIsValid')),
|
|
659
|
+
'UserTransferRequired' => Php.truthy?(Php.get(response, 'userTransferRequired')),
|
|
660
|
+
'TransferLock' => Php.truthy?(Php.get(response, 'transferLock')),
|
|
661
|
+
'Message' => Php.to_s(message.nil? ? '' : message),
|
|
662
|
+
'MessageKey' => Php.to_s(message_key.nil? ? '' : message_key),
|
|
663
|
+
'Additional' => additional.nil? ? {} : Php.to_array(additional)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
unless available
|
|
667
|
+
envelope['error'] = set_error(
|
|
668
|
+
'TRANSFER_NOT_AVAILABLE',
|
|
669
|
+
message.nil? ? 'Domain is not transferable' : message,
|
|
670
|
+
message_key.nil? ? (message.nil? ? '' : message) : message_key
|
|
671
|
+
)
|
|
672
|
+
end
|
|
673
|
+
envelope
|
|
674
|
+
rescue ApiError => e
|
|
675
|
+
error_envelope(e, 'CHECK_TRANSFER')
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# --- DNS zone --------------------------------------------------------------
|
|
679
|
+
|
|
680
|
+
def get_resource_records(domain_name)
|
|
681
|
+
response = request('GET', 'domains/zones', { 'domainName' => domain_name })
|
|
682
|
+
|
|
683
|
+
records = []
|
|
684
|
+
Php.values(Php.to_array(response)).each do |set|
|
|
685
|
+
next unless Php.array?(set)
|
|
686
|
+
|
|
687
|
+
name = Php.get(set, 'name') || ''
|
|
688
|
+
type = Php.get(set, 'type') || ''
|
|
689
|
+
ttl = Php.to_i(Php.get(set, 'ttl') || 0)
|
|
690
|
+
rows = Php.get(set, 'records') || []
|
|
691
|
+
if !Php.array?(rows) || rows.empty?
|
|
692
|
+
records << { 'Name' => name, 'Type' => type, 'TTL' => ttl, 'Content' => '', 'Disabled' => false }
|
|
693
|
+
next
|
|
694
|
+
end
|
|
695
|
+
Php.values(rows).each do |rec|
|
|
696
|
+
content = Php.get(rec, 'content')
|
|
697
|
+
records << {
|
|
698
|
+
'Name' => name, 'Type' => type, 'TTL' => ttl,
|
|
699
|
+
'Content' => content.nil? ? '' : content,
|
|
700
|
+
'Disabled' => Php.truthy?(Php.get(rec, 'disabled'))
|
|
701
|
+
}
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
{ 'result' => RESULT_OK, 'data' => { 'records' => records } }
|
|
706
|
+
rescue ApiError => e
|
|
707
|
+
error_envelope(e, 'GET_ZONE')
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def add_resource_record(domain_name, name, type, content, ttl = 3600, apply = true)
|
|
711
|
+
if (why = unsupported_zone_type(type))
|
|
712
|
+
return { 'result' => RESULT_ERROR, 'error' => set_error('ADD_ZONE', why, why) }
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
name = relativize_record_name(name, domain_name)
|
|
716
|
+
body = { 'zoneStruct' => build_zone_struct(name, type, content, ttl) }
|
|
717
|
+
request('POST', "domains/zones?#{Php.http_build_query({ 'domainName' => domain_name })}", body)
|
|
718
|
+
if Php.truthy?(apply)
|
|
719
|
+
applied = apply_resource_records(domain_name)
|
|
720
|
+
return applied if applied['result'] != RESULT_OK
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
{ 'result' => RESULT_OK,
|
|
724
|
+
'data' => { 'Name' => name, 'Type' => body['zoneStruct']['type'], 'TTL' => body['zoneStruct']['ttl'], 'Content' => content } }
|
|
725
|
+
rescue ApiError => e
|
|
726
|
+
error_envelope(e, 'ADD_ZONE')
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def edit_resource_record(domain_name, record_name, name, type, content, ttl = 3600, apply = true)
|
|
730
|
+
if (why = unsupported_zone_type(type))
|
|
731
|
+
return { 'result' => RESULT_ERROR, 'error' => set_error('EDIT_ZONE', why, why) }
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
name = relativize_record_name(name, domain_name)
|
|
735
|
+
body = { 'zoneStruct' => build_zone_struct(name, type, content, ttl) }
|
|
736
|
+
query = Php.http_build_query({ 'domainName' => domain_name, 'recordName' => record_name })
|
|
737
|
+
request('PUT', "domains/zones?#{query}", body)
|
|
738
|
+
if Php.truthy?(apply)
|
|
739
|
+
applied = apply_resource_records(domain_name)
|
|
740
|
+
return applied if applied['result'] != RESULT_OK
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
{ 'result' => RESULT_OK,
|
|
744
|
+
'data' => { 'Name' => name, 'Type' => body['zoneStruct']['type'], 'TTL' => body['zoneStruct']['ttl'], 'Content' => content } }
|
|
745
|
+
rescue ApiError => e
|
|
746
|
+
error_envelope(e, 'EDIT_ZONE')
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
def delete_resource_record(domain_name, name, record, record_type, apply = true)
|
|
750
|
+
request('DELETE', 'domains/zones', {
|
|
751
|
+
'domainName' => domain_name, 'Name' => name, 'Record' => record, 'RecordType' => record_type
|
|
752
|
+
})
|
|
753
|
+
if Php.truthy?(apply)
|
|
754
|
+
applied = apply_resource_records(domain_name)
|
|
755
|
+
return applied if applied['result'] != RESULT_OK
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
{ 'result' => RESULT_OK, 'data' => { 'Name' => name, 'Type' => record_type, 'Content' => record } }
|
|
759
|
+
rescue ApiError => e
|
|
760
|
+
error_envelope(e, 'DELETE_ZONE')
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def apply_resource_records(domain_name)
|
|
764
|
+
request('POST', "domains/zones/apply?#{Php.http_build_query({ 'domainName' => domain_name })}", [])
|
|
765
|
+
{ 'result' => RESULT_OK, 'data' => { 'DomainName' => domain_name } }
|
|
766
|
+
rescue ApiError => e
|
|
767
|
+
error_envelope(e, 'APPLY_ZONE')
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def set_resource_record_set(domain_name, name, type, contents, ttl = 3600, apply = true)
|
|
771
|
+
raise TypeError, 'contents must be an Array' unless Php.array?(contents)
|
|
772
|
+
|
|
773
|
+
if (why = unsupported_zone_type(type))
|
|
774
|
+
return { 'result' => RESULT_ERROR, 'error' => set_error('SET_ZONE', why, why) }
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
name = relativize_record_name(name, domain_name)
|
|
778
|
+
contents = Php.values(contents).map { |v| Php.to_s(v) }.reject { |v| Php.trim(v) == '' }
|
|
779
|
+
|
|
780
|
+
return delete_resource_record_set(domain_name, name, type, apply) if contents.empty?
|
|
781
|
+
|
|
782
|
+
body = { 'zoneStruct' => build_zone_struct(name, type, contents, ttl) }
|
|
783
|
+
request('POST', "domains/zones?#{Php.http_build_query({ 'domainName' => domain_name })}", body)
|
|
784
|
+
if Php.truthy?(apply)
|
|
785
|
+
applied = apply_resource_records(domain_name)
|
|
786
|
+
return applied if applied['result'] != RESULT_OK
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
{ 'result' => RESULT_OK,
|
|
790
|
+
'data' => { 'Name' => name, 'Type' => body['zoneStruct']['type'], 'TTL' => body['zoneStruct']['ttl'], 'Contents' => contents } }
|
|
791
|
+
rescue ApiError => e
|
|
792
|
+
error_envelope(e, 'SET_ZONE')
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
def delete_resource_record_set(domain_name, name, type, apply = true, known_value = '')
|
|
796
|
+
type = Php.upper(Php.trim(type))
|
|
797
|
+
value = Php.to_s(known_value)
|
|
798
|
+
|
|
799
|
+
if value == ''
|
|
800
|
+
current = get_resource_records(domain_name)
|
|
801
|
+
return current if current['result'] != RESULT_OK
|
|
802
|
+
|
|
803
|
+
fqdn = qualify_record_name(name, domain_name)
|
|
804
|
+
current['data']['records'].each do |rec|
|
|
805
|
+
next unless Php.casecmp_eq?(rec['Name'], fqdn) && Php.casecmp_eq?(rec['Type'], type)
|
|
806
|
+
|
|
807
|
+
value = Php.to_s(rec['Content'])
|
|
808
|
+
break
|
|
809
|
+
end
|
|
810
|
+
return { 'result' => RESULT_OK, 'data' => { 'Name' => name, 'Type' => type, 'Removed' => false } } if value == ''
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
delete_resource_record(domain_name, qualify_record_name(name, domain_name), value, type, apply)
|
|
814
|
+
rescue ApiError => e
|
|
815
|
+
error_envelope(e, 'DELETE_ZONE')
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def add_resource_record_value(domain_name, name, type, value, ttl = nil, apply = true)
|
|
819
|
+
if (why = unsupported_zone_type(type))
|
|
820
|
+
return { 'result' => RESULT_ERROR, 'error' => set_error('ADD_ZONE', why, why) }
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
name = relativize_record_name(name, domain_name)
|
|
824
|
+
set = read_resource_record_set(domain_name, name, type)
|
|
825
|
+
return set if set.key?('result')
|
|
826
|
+
|
|
827
|
+
if set['contents'].any? { |existing| same_record_value?(type, existing, value) }
|
|
828
|
+
return { 'result' => RESULT_OK,
|
|
829
|
+
'data' => { 'Name' => name, 'Type' => Php.upper(type), 'Contents' => set['contents'], 'Changed' => false } }
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
contents = set['contents'] + [Php.to_s(value)]
|
|
833
|
+
set_resource_record_set(domain_name, name, type, contents, ttl.nil? ? fallback_ttl(set) : ttl, apply)
|
|
834
|
+
rescue ApiError => e
|
|
835
|
+
error_envelope(e, 'ADD_ZONE')
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
def remove_resource_record_value(domain_name, name, type, value, apply = true)
|
|
839
|
+
if (why = unsupported_zone_type(type))
|
|
840
|
+
return { 'result' => RESULT_ERROR, 'error' => set_error('DELETE_ZONE', why, why) }
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
name = relativize_record_name(name, domain_name)
|
|
844
|
+
set = read_resource_record_set(domain_name, name, type)
|
|
845
|
+
return set if set.key?('result')
|
|
846
|
+
|
|
847
|
+
remaining = []
|
|
848
|
+
found = false
|
|
849
|
+
set['contents'].each do |existing|
|
|
850
|
+
if !found && same_record_value?(type, existing, value)
|
|
851
|
+
found = true
|
|
852
|
+
next
|
|
853
|
+
end
|
|
854
|
+
remaining << existing
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
unless found
|
|
858
|
+
return { 'result' => RESULT_OK,
|
|
859
|
+
'data' => { 'Name' => name, 'Type' => Php.upper(type), 'Contents' => set['contents'], 'Changed' => false } }
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
return delete_resource_record_set(domain_name, name, type, apply, Php.to_s(value)) if remaining.empty?
|
|
863
|
+
|
|
864
|
+
set_resource_record_set(domain_name, name, type, remaining, fallback_ttl(set), apply)
|
|
865
|
+
rescue ApiError => e
|
|
866
|
+
error_envelope(e, 'DELETE_ZONE')
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def replace_resource_record_value(domain_name, name, type, old_value, new_value, ttl = nil, apply = true)
|
|
870
|
+
if (why = unsupported_zone_type(type))
|
|
871
|
+
return { 'result' => RESULT_ERROR, 'error' => set_error('EDIT_ZONE', why, why) }
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
name = relativize_record_name(name, domain_name)
|
|
875
|
+
set = read_resource_record_set(domain_name, name, type)
|
|
876
|
+
return set if set.key?('result')
|
|
877
|
+
|
|
878
|
+
contents = []
|
|
879
|
+
found = false
|
|
880
|
+
set['contents'].each do |existing|
|
|
881
|
+
if !found && same_record_value?(type, existing, old_value)
|
|
882
|
+
found = true
|
|
883
|
+
contents << Php.to_s(new_value)
|
|
884
|
+
next
|
|
885
|
+
end
|
|
886
|
+
contents << existing
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
unless found
|
|
890
|
+
why = format('No %<type>s record "%<name>s" with value "%<value>s" to edit.',
|
|
891
|
+
type: Php.upper(type), name: name, value: Php.to_s(old_value))
|
|
892
|
+
return { 'result' => RESULT_ERROR, 'error' => set_error('EDIT_ZONE', why, why) }
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
set_resource_record_set(domain_name, name, type, contents, ttl.nil? ? fallback_ttl(set) : ttl, apply)
|
|
896
|
+
rescue ApiError => e
|
|
897
|
+
error_envelope(e, 'EDIT_ZONE')
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
# --- forwarding ------------------------------------------------------------
|
|
901
|
+
|
|
902
|
+
def get_forwarding(domain_name)
|
|
903
|
+
response = request('GET', 'domains/forwards', { 'domainName' => domain_name })
|
|
904
|
+
alias_ = Php.get(response, 'redirectAlias')
|
|
905
|
+
type = Php.get(response, 'redirectType')
|
|
906
|
+
{
|
|
907
|
+
'result' => RESULT_OK,
|
|
908
|
+
'data' => {
|
|
909
|
+
'Enabled' => Php.truthy?(alias_),
|
|
910
|
+
'RedirectAddress' => Php.to_s(alias_.nil? ? '' : alias_),
|
|
911
|
+
'ForwardType' => Php.to_s(type.nil? ? '' : type)
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
rescue ApiError => e
|
|
915
|
+
raw = Php.get(@last_response, 'raw_response')
|
|
916
|
+
raw = e.message if raw.nil?
|
|
917
|
+
if forward_not_set?(e)
|
|
918
|
+
return { 'result' => RESULT_OK, 'data' => { 'Enabled' => false, 'RedirectAddress' => '', 'ForwardType' => '' } }
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
details = Php.get(@last_parsed_response, 'Details')
|
|
922
|
+
{ 'result' => RESULT_ERROR, 'error' => set_error(format_error_code(e.code), e.message, details.nil? ? raw : details) }
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
def set_forwarding(domain_name, redirect_address, forward_type = 'Standard')
|
|
926
|
+
type = Php.ucfirst(Php.lower(Php.trim(forward_type)))
|
|
927
|
+
unless FORWARD_TYPES.include?(type)
|
|
928
|
+
why = format('Forward type "%<type>s" is not supported. Supported types: %<list>s.',
|
|
929
|
+
type: Php.to_s(forward_type), list: FORWARD_TYPES.join(', '))
|
|
930
|
+
return { 'result' => RESULT_ERROR, 'error' => set_error('SET_FORWARD', why, why) }
|
|
931
|
+
end
|
|
932
|
+
if Php.trim(redirect_address) == ''
|
|
933
|
+
why = 'Redirect address is required.'
|
|
934
|
+
return { 'result' => RESULT_ERROR, 'error' => set_error('SET_FORWARD', why, why) }
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
request('POST', 'domains/forwards', {
|
|
938
|
+
'domainName' => domain_name, 'redirectAddress' => redirect_address, 'forwardType' => type
|
|
939
|
+
})
|
|
940
|
+
{ 'result' => RESULT_OK, 'data' => { 'RedirectAddress' => redirect_address, 'ForwardType' => type } }
|
|
941
|
+
rescue ApiError => e
|
|
942
|
+
error_envelope(e, 'SET_FORWARD')
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
def delete_forwarding(domain_name)
|
|
946
|
+
request('DELETE', 'domains/forwards', { 'DomainName' => domain_name })
|
|
947
|
+
{ 'result' => RESULT_OK, 'data' => { 'DomainName' => domain_name, 'Removed' => true } }
|
|
948
|
+
rescue ApiError => e
|
|
949
|
+
# Nothing configured: deleting is a no-op.
|
|
950
|
+
return { 'result' => RESULT_OK, 'data' => { 'DomainName' => domain_name, 'Removed' => false } } if forward_not_set?(e)
|
|
951
|
+
|
|
952
|
+
error_envelope(e, 'DELETE_FORWARD')
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
# --- utilities (no network) --------------------------------------------------
|
|
956
|
+
|
|
957
|
+
def is_tr_tld(domain)
|
|
958
|
+
str = Php.to_s(domain).b
|
|
959
|
+
tail = str.bytesize >= 3 ? str.byteslice(-3, 3) : str
|
|
960
|
+
Php.lower(tail) == '.tr'
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
VALIDATE_DEFAULTS = {
|
|
964
|
+
'FirstName' => 'Isimyok',
|
|
965
|
+
'LastName' => 'Isimyok',
|
|
966
|
+
'AddressLine1' => 'Addres yok',
|
|
967
|
+
'City' => 'ISTANBUL',
|
|
968
|
+
'Country' => 'TR',
|
|
969
|
+
'ZipCode' => '34000',
|
|
970
|
+
'Phone' => '5555555555',
|
|
971
|
+
'PhoneCountryCode' => '90'
|
|
972
|
+
}.freeze
|
|
973
|
+
|
|
974
|
+
def validate_contact(contact)
|
|
975
|
+
contact = Php.to_hash(contact)
|
|
976
|
+
VALIDATE_DEFAULTS.each { |key, value| contact[key] = value if contact[key].nil? }
|
|
977
|
+
# No State default: a missing state falls back to the city below.
|
|
978
|
+
contact['State'] = '' if contact['State'].nil?
|
|
979
|
+
|
|
980
|
+
contact['FirstName'] = sanitize_person_name(contact['FirstName'])
|
|
981
|
+
contact['LastName'] = sanitize_person_name(contact['LastName'])
|
|
982
|
+
|
|
983
|
+
contact['FirstName'] = VALIDATE_DEFAULTS['FirstName'] if Php.trim(contact['FirstName']).empty?
|
|
984
|
+
contact['LastName'] = contact['FirstName'] if Php.trim(contact['LastName']).empty?
|
|
985
|
+
contact['AddressLine1'] = VALIDATE_DEFAULTS['AddressLine1'] if Php.trim(contact['AddressLine1']).empty?
|
|
986
|
+
contact['City'] = VALIDATE_DEFAULTS['City'] if Php.trim(contact['City']).empty?
|
|
987
|
+
contact['State'] = contact['City'] if Php.trim(contact['State']).empty?
|
|
988
|
+
contact['Country'] = normalize_country_code(contact['Country'])
|
|
989
|
+
contact['Country'] = VALIDATE_DEFAULTS['Country'] if Php.trim(contact['Country']).empty?
|
|
990
|
+
contact['ZipCode'] = VALIDATE_DEFAULTS['ZipCode'] if Php.trim(contact['ZipCode']).empty?
|
|
991
|
+
|
|
992
|
+
phone, phone_cc = normalize_phone(contact['Phone'], contact['PhoneCountryCode'])
|
|
993
|
+
contact['Phone'] = phone
|
|
994
|
+
contact['PhoneCountryCode'] = phone_cc
|
|
995
|
+
contact['PhoneCountryCode'] = VALIDATE_DEFAULTS['PhoneCountryCode'] if Php.trim(contact['PhoneCountryCode']).empty?
|
|
996
|
+
contact['Phone'] = VALIDATE_DEFAULTS['Phone'] if Php.trim(contact['Phone']).empty?
|
|
997
|
+
|
|
998
|
+
fax, fax_cc = normalize_phone(contact['Fax'].nil? ? '' : contact['Fax'],
|
|
999
|
+
contact['FaxCountryCode'].nil? ? '' : contact['FaxCountryCode'])
|
|
1000
|
+
contact['Fax'] = fax
|
|
1001
|
+
contact['FaxCountryCode'] = fax == '' ? '' : fax_cc
|
|
1002
|
+
|
|
1003
|
+
contact
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
private
|
|
1007
|
+
|
|
1008
|
+
# --- HTTP --------------------------------------------------------------------
|
|
1009
|
+
|
|
1010
|
+
def request(method, endpoint, data = {})
|
|
1011
|
+
pace_zone_request(endpoint)
|
|
1012
|
+
|
|
1013
|
+
@transport_failed = false
|
|
1014
|
+
@last_gateway_code = ''
|
|
1015
|
+
|
|
1016
|
+
@start_at = monotonic
|
|
1017
|
+
@start_wall = Time.now
|
|
1018
|
+
@last_function = "request:#{method} #{endpoint.to_s.split('?', 2).first}"
|
|
1019
|
+
|
|
1020
|
+
url = "#{@service_url}/#{endpoint.to_s.sub(%r{\A/+}, '')}"
|
|
1021
|
+
@last_request = { 'url' => url, 'method' => method, 'payload' => data }
|
|
1022
|
+
|
|
1023
|
+
headers = {
|
|
1024
|
+
'Content-Type' => 'application/json',
|
|
1025
|
+
'Accept' => 'application/json',
|
|
1026
|
+
'X-API-KEY' => @token.to_s,
|
|
1027
|
+
'__reseller' => @reseller_id.to_s,
|
|
1028
|
+
'User-Agent' => USER_AGENT
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
body = nil
|
|
1032
|
+
if %w[GET DELETE].include?(method)
|
|
1033
|
+
url += "?#{Php.http_build_query(data)}" unless Php.empty?(data)
|
|
1034
|
+
else
|
|
1035
|
+
body = Php.json_encode(data)
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
begin
|
|
1039
|
+
raw = @transport.call(
|
|
1040
|
+
method: method, url: url, headers: headers, body: body,
|
|
1041
|
+
timeout: DEFAULT_TIMEOUT, connect_timeout: DEFAULT_CONNECT_TIMEOUT, verify_ssl: @ssl_verify
|
|
1042
|
+
)
|
|
1043
|
+
rescue TransportError => e
|
|
1044
|
+
transport_failure(url, e.code, e.message)
|
|
1045
|
+
rescue StandardError => e
|
|
1046
|
+
transport_failure(url, 0, e.message)
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
status, response_body, response_headers = unpack_response(raw)
|
|
1050
|
+
@last_response_headers = {
|
|
1051
|
+
'url' => url, 'http_code' => status, 'headers' => response_headers,
|
|
1052
|
+
'total_time' => (monotonic - @start_at).round(6)
|
|
1053
|
+
}
|
|
1054
|
+
decoded = Php.json_decode(response_body)
|
|
1055
|
+
@last_response = Php.array?(decoded) ? decoded : { 'raw_response' => response_body }
|
|
1056
|
+
|
|
1057
|
+
if status >= 200 && status <= 299
|
|
1058
|
+
parsed = decoded
|
|
1059
|
+
@last_parsed_response = parsed
|
|
1060
|
+
maybe_send_performance
|
|
1061
|
+
return parsed
|
|
1062
|
+
end
|
|
1063
|
+
|
|
1064
|
+
parsed = decoded
|
|
1065
|
+
if [301, 302].include?(status)
|
|
1066
|
+
parsed = { 'message' => 'Invalid API credentials', 'code' => 'CREDENTIALS',
|
|
1067
|
+
'details' => 'Authentication failed. Check your API key and reseller ID.' }
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
message = first_set(Php.get(parsed, 'message'), Php.get(parsed, 'error', 'message'), response_body)
|
|
1071
|
+
code = first_set(Php.get(parsed, 'code'), Php.get(parsed, 'error', 'code'), "HTTP_#{status}")
|
|
1072
|
+
api_details = first_set(Php.get(parsed, 'details'), Php.get(parsed, 'error', 'details'))
|
|
1073
|
+
details = first_set(api_details, response_body)
|
|
1074
|
+
# The gateway's own code is kept as GatewayCode next to API_<status>.
|
|
1075
|
+
if code.is_a?(String) && code != '' && !code.start_with?('HTTP_') && code != 'CREDENTIALS'
|
|
1076
|
+
@last_gateway_code = code
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
if Php.trim(message) == ''
|
|
1080
|
+
message = "HTTP #{status} - #{http_status_reason(status)}"
|
|
1081
|
+
details = message if Php.trim(details) == ''
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
if BOILERPLATE_RE.match?(Php.to_s(message)) || Php.to_s(message) == Php.to_s(code)
|
|
1085
|
+
# Only the gateway's details field may replace the message, never the raw body.
|
|
1086
|
+
clean = Php.trim(Php.to_s(api_details).sub(/\ADna\s+/, ''))
|
|
1087
|
+
message = if clean != '' && clean != Php.to_s(message) && clean != Php.to_s(code) && !BOILERPLATE_RE.match?(clean)
|
|
1088
|
+
clean
|
|
1089
|
+
else
|
|
1090
|
+
"The operation could not be completed by the registry (error: #{Php.to_s(code)}). " \
|
|
1091
|
+
'Please try again or contact support.'
|
|
1092
|
+
end
|
|
1093
|
+
end
|
|
1094
|
+
|
|
1095
|
+
validation = first_set(Php.get(parsed, 'error', 'validationErrors'), Php.get(parsed, 'validationErrors'))
|
|
1096
|
+
if Php.array?(validation) && !validation.empty?
|
|
1097
|
+
flattened = flatten_validation_errors(validation)
|
|
1098
|
+
details = flattened unless flattened == ''
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
@last_parsed_response = set_error(code, message, details)
|
|
1102
|
+
error = ApiError.new(Php.to_s(message), status)
|
|
1103
|
+
error.set_backtrace(caller)
|
|
1104
|
+
send_error_to_sentry_async(error)
|
|
1105
|
+
raise error
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
def transport_failure(url, errno, detail)
|
|
1109
|
+
@transport_failed = true
|
|
1110
|
+
message = "Curl error during request: #{detail}"
|
|
1111
|
+
message = "Could not reach the DomainNameAPI service: the connection timed out. (#{detail})" if errno == 28
|
|
1112
|
+
@last_response_headers = { 'url' => url, 'http_code' => 0, 'headers' => {},
|
|
1113
|
+
'total_time' => (monotonic - @start_at).round(6) }
|
|
1114
|
+
@last_response = { 'raw_response' => false }
|
|
1115
|
+
@last_parsed_response = set_error("CURL_#{errno}", message, message)
|
|
1116
|
+
error = ApiError.new(message, errno)
|
|
1117
|
+
error.set_backtrace(caller)
|
|
1118
|
+
send_error_to_sentry_async(error)
|
|
1119
|
+
raise error
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
def unpack_response(raw)
|
|
1123
|
+
if raw.is_a?(Hash)
|
|
1124
|
+
status = raw[:status] || raw['status']
|
|
1125
|
+
body = raw[:body] || raw['body']
|
|
1126
|
+
headers = raw[:headers] || raw['headers'] || {}
|
|
1127
|
+
else
|
|
1128
|
+
status = raw.status
|
|
1129
|
+
body = raw.body
|
|
1130
|
+
headers = raw.respond_to?(:headers) ? raw.headers || {} : {}
|
|
1131
|
+
end
|
|
1132
|
+
[status.to_i, body.nil? ? '' : body.to_s, headers]
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
def maybe_send_performance
|
|
1136
|
+
duration = (monotonic - @start_at) * 1000.0
|
|
1137
|
+
return unless duration > 1000 || rand(1..1000) <= PERFORMANCE_SAMPLE_RATE
|
|
1138
|
+
|
|
1139
|
+
send_performance_metrics_to_sentry(
|
|
1140
|
+
'operation' => @last_function,
|
|
1141
|
+
'duration' => duration,
|
|
1142
|
+
'success' => true,
|
|
1143
|
+
'timestamp' => Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
|
|
1144
|
+
'start_timestamp' => @start_wall.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
|
|
1145
|
+
)
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1148
|
+
# $a ?? $b ?? $c
|
|
1149
|
+
def first_set(*values)
|
|
1150
|
+
values.find { |v| !v.nil? }
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
def pace_zone_request(endpoint)
|
|
1154
|
+
return unless endpoint.to_s.sub(%r{\A/+}, '').start_with?('domains/zones')
|
|
1155
|
+
|
|
1156
|
+
if @last_zone_request_at.positive? && @zone_min_interval.positive?
|
|
1157
|
+
wait = @zone_min_interval - (monotonic - @last_zone_request_at)
|
|
1158
|
+
sleep(wait) if wait.positive?
|
|
1159
|
+
end
|
|
1160
|
+
@last_zone_request_at = monotonic
|
|
1161
|
+
end
|
|
1162
|
+
|
|
1163
|
+
def http_status_reason(status)
|
|
1164
|
+
return HTTP_REASONS[status] if HTTP_REASONS.key?(status)
|
|
1165
|
+
return 'Server Error' if status >= 500
|
|
1166
|
+
return 'Client Error' if status >= 400
|
|
1167
|
+
|
|
1168
|
+
'Unexpected Response'
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
# API_<status> for HTTP errors, CURL_<errno> for transport failures.
|
|
1172
|
+
def format_error_code(code)
|
|
1173
|
+
return Php.to_s(code) unless Php.numeric?(code)
|
|
1174
|
+
|
|
1175
|
+
"#{@transport_failed ? 'CURL_' : 'API_'}#{Php.to_s(code)}"
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
def set_error(code, message = '', details = '')
|
|
1179
|
+
if code.is_a?(String) && DEFAULT_ERRORS.key?(code)
|
|
1180
|
+
default_message, default_details = DEFAULT_ERRORS[code]
|
|
1181
|
+
result = {
|
|
1182
|
+
'Code' => code,
|
|
1183
|
+
'Message' => Php.trim(message) != '' ? message : default_message,
|
|
1184
|
+
'Details' => Php.trim(details) != '' ? details : default_details
|
|
1185
|
+
}
|
|
1186
|
+
else
|
|
1187
|
+
result = { 'Code' => code, 'Message' => message, 'Details' => details }
|
|
1188
|
+
end
|
|
1189
|
+
if result['Code'].is_a?(String) && result['Code'].match?(/\AAPI_\d+\z/) && !Php.empty?(@last_gateway_code) &&
|
|
1190
|
+
@last_gateway_code != ''
|
|
1191
|
+
result['GatewayCode'] = @last_gateway_code
|
|
1192
|
+
end
|
|
1193
|
+
result
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
# register/transfer/renew can answer HTTP 200 with {"success": false}:
|
|
1197
|
+
# a refusal, returned as an error envelope (nil when it succeeded).
|
|
1198
|
+
def operation_failure(response, code)
|
|
1199
|
+
return nil unless response.is_a?(Hash) && response.key?('success') && response['success'] == false
|
|
1200
|
+
|
|
1201
|
+
message = Php.trim(Php.to_s(first_set(response['message'], '')))
|
|
1202
|
+
message = 'The registry did not accept the request.' if message == ''
|
|
1203
|
+
{ 'result' => RESULT_ERROR, 'error' => set_error(code, message, message) }
|
|
1204
|
+
end
|
|
1205
|
+
|
|
1206
|
+
FORWARD_NOT_SET = 'redirect information not found'
|
|
1207
|
+
|
|
1208
|
+
# The gateway answers "Redirect information not found" when no forwarding is set.
|
|
1209
|
+
def forward_not_set?(error)
|
|
1210
|
+
error.message.downcase.include?(FORWARD_NOT_SET) ||
|
|
1211
|
+
Php.to_s(first_set(Php.get(@last_response, 'raw_response'), '')).downcase.include?(FORWARD_NOT_SET) ||
|
|
1212
|
+
Php.json_encode(@last_response.nil? ? [] : @last_response).downcase.include?(FORWARD_NOT_SET)
|
|
1213
|
+
rescue StandardError
|
|
1214
|
+
false
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
# The catch block shared by every network method.
|
|
1218
|
+
def error_envelope(error, fallback)
|
|
1219
|
+
code = format_error_code(error.code)
|
|
1220
|
+
code = fallback if Php.empty?(code)
|
|
1221
|
+
details = Php.get(@last_parsed_response, 'Details')
|
|
1222
|
+
details = Php.get(@last_response, 'raw_response') if details.nil?
|
|
1223
|
+
details = error.message if details.nil?
|
|
1224
|
+
{ 'result' => RESULT_ERROR, 'error' => set_error(code, error.message, details) }
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1227
|
+
# array_map() keeping the container type.
|
|
1228
|
+
def php_map(collection, &block)
|
|
1229
|
+
case collection
|
|
1230
|
+
when Hash then collection.transform_values(&block)
|
|
1231
|
+
when Array then collection.map(&block)
|
|
1232
|
+
else []
|
|
1233
|
+
end
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
# --- response parsing ------------------------------------------------------
|
|
1237
|
+
|
|
1238
|
+
def list_item(item)
|
|
1239
|
+
status_code = Php.get(item, 'statusCode')
|
|
1240
|
+
status_text = Php.get(item, 'statusText')
|
|
1241
|
+
status = if !status_code.nil? && status_code != '' && !Php.numeric?(status_code)
|
|
1242
|
+
Php.to_s(status_code)
|
|
1243
|
+
elsif !status_text.nil? && status_text != '' && !Php.numeric?(status_text)
|
|
1244
|
+
Php.to_s(status_text)
|
|
1245
|
+
else
|
|
1246
|
+
normalize_domain_status(first_set(Php.get(item, 'status'), ''))
|
|
1247
|
+
end
|
|
1248
|
+
ns = Php.get(item, 'nameServers')
|
|
1249
|
+
{
|
|
1250
|
+
'ID' => Php.to_i(first_set(Php.get(item, 'id'), 0)),
|
|
1251
|
+
'Status' => status,
|
|
1252
|
+
'DomainName' => first_set(Php.get(item, 'domainName'), ''),
|
|
1253
|
+
'AuthCode' => first_set(Php.get(item, 'authCode'), ''),
|
|
1254
|
+
'LockStatus' => Php.truthy?(Php.get(item, 'lockStatus')) ? 'true' : 'false',
|
|
1255
|
+
'PrivacyProtectionStatus' => Php.truthy?(Php.get(item, 'privacyProtectionStatus')) ? 'true' : 'false',
|
|
1256
|
+
'IsChildNameServer' => Php.truthy?(Php.get(item, 'hosts')) ? 'true' : 'false',
|
|
1257
|
+
'Contacts' => {
|
|
1258
|
+
'Billing' => { 'ID' => '' },
|
|
1259
|
+
'Technical' => { 'ID' => '' },
|
|
1260
|
+
'Administrative' => { 'ID' => '' },
|
|
1261
|
+
'Registrant' => { 'ID' => '' }
|
|
1262
|
+
},
|
|
1263
|
+
'Dates' => {
|
|
1264
|
+
'Start' => format_api_date(Php.get(item, 'startDate')),
|
|
1265
|
+
'Expiration' => format_api_date(Php.get(item, 'expirationDate')),
|
|
1266
|
+
'RemainingDays' => Php.to_i(first_set(Php.get(item, 'remainingDay'), 0))
|
|
1267
|
+
},
|
|
1268
|
+
'NameServers' => ns.nil? ? [] : ns,
|
|
1269
|
+
'Additional' => {},
|
|
1270
|
+
'ChildNameServers' => []
|
|
1271
|
+
}
|
|
1272
|
+
end
|
|
1273
|
+
|
|
1274
|
+
def add_price(pricing, currencies, out_type, info)
|
|
1275
|
+
period = Php.to_i(first_set(Php.get(info, 'period'), 1))
|
|
1276
|
+
period = 1 if period < 1
|
|
1277
|
+
price = Php.get(info, 'price')
|
|
1278
|
+
(pricing[out_type] ||= {})[period] = price.nil? ? '0.0000' : Php.number_format(price, 4)
|
|
1279
|
+
currencies[out_type] = first_set(Php.get(info, 'currency'), '')
|
|
1280
|
+
end
|
|
1281
|
+
|
|
1282
|
+
def normalize_domain_status(value)
|
|
1283
|
+
if Php.numeric?(value)
|
|
1284
|
+
DOMAIN_STATUS_MAP.fetch(Php.to_i(value)) { Php.to_s(value) }
|
|
1285
|
+
else
|
|
1286
|
+
Php.to_s(value)
|
|
1287
|
+
end
|
|
1288
|
+
end
|
|
1289
|
+
|
|
1290
|
+
def parse_domain_info(data)
|
|
1291
|
+
return { 'result' => RESULT_ERROR, 'error' => set_error('RESPONSE') } if Php.empty?(data)
|
|
1292
|
+
|
|
1293
|
+
remaining = Php.get(data, 'remainingDay')
|
|
1294
|
+
nameservers = Php.get(data, 'nameservers')
|
|
1295
|
+
additional = Php.get(data, 'additionalAttributes')
|
|
1296
|
+
hosts = Php.get(data, 'hosts')
|
|
1297
|
+
{
|
|
1298
|
+
'data' => {
|
|
1299
|
+
'ID' => Php.to_i(first_set(Php.get(data, 'id'), 0)),
|
|
1300
|
+
'Status' => normalize_domain_status(first_set(Php.get(data, 'status'), '')),
|
|
1301
|
+
'DomainName' => Php.to_s(first_set(Php.get(data, 'domainName'), Php.get(data, 'name'), '')),
|
|
1302
|
+
'AuthCode' => Php.to_s(first_set(Php.get(data, 'authCode'), Php.get(data, 'eppCode'), '')),
|
|
1303
|
+
'LockStatus' => Php.truthy?(Php.get(data, 'lockStatus')) ? 'true' : 'false',
|
|
1304
|
+
'PrivacyProtectionStatus' => Php.truthy?(Php.get(data, 'privacyProtectionStatus')) ? 'true' : 'false',
|
|
1305
|
+
'IsChildNameServer' => Php.truthy?(hosts) ? 'true' : 'false',
|
|
1306
|
+
'Contacts' => parse_contact_ids(first_set(Php.get(data, 'contacts'), [])),
|
|
1307
|
+
'Dates' => {
|
|
1308
|
+
'Start' => format_api_date(Php.get(data, 'startDate')),
|
|
1309
|
+
'Expiration' => format_api_date(Php.get(data, 'expirationDate')),
|
|
1310
|
+
'RemainingDays' => if remaining.nil?
|
|
1311
|
+
remaining_days_from_date(Php.get(data, 'expirationDate'))
|
|
1312
|
+
else
|
|
1313
|
+
Php.to_i(remaining)
|
|
1314
|
+
end
|
|
1315
|
+
},
|
|
1316
|
+
'NameServers' => nameservers.nil? ? [] : php_map(nameservers) { |v| Php.to_s(v) },
|
|
1317
|
+
'Additional' => additional.nil? ? {} : Php.to_array(additional),
|
|
1318
|
+
'ChildNameServers' => hosts.nil? ? [] : php_map(hosts) { |ns| child_name_server(ns) }
|
|
1319
|
+
},
|
|
1320
|
+
'result' => RESULT_OK
|
|
1321
|
+
}
|
|
1322
|
+
end
|
|
1323
|
+
|
|
1324
|
+
def child_name_server(host)
|
|
1325
|
+
ips = Php.values(first_set(Php.get(host, 'ipAddresses'), [])).map { |ip| Php.get(ip, 'ipAddress') }
|
|
1326
|
+
{ 'ns' => Php.get(host, 'name'), 'ip' => ips.empty? ? '' : ips.last }
|
|
1327
|
+
end
|
|
1328
|
+
|
|
1329
|
+
def parse_contact_ids(contacts)
|
|
1330
|
+
result = {
|
|
1331
|
+
'Billing' => { 'ID' => 0 },
|
|
1332
|
+
'Technical' => { 'ID' => 0 },
|
|
1333
|
+
'Administrative' => { 'ID' => 0 },
|
|
1334
|
+
'Registrant' => { 'ID' => 0 }
|
|
1335
|
+
}
|
|
1336
|
+
return result unless Php.array?(contacts)
|
|
1337
|
+
|
|
1338
|
+
Php.values(contacts).each do |c|
|
|
1339
|
+
api_type = first_set(Php.get(c, 'ContactType'), Php.get(c, 'contactType'), '')
|
|
1340
|
+
key = api_type.is_a?(String) ? CONTACT_TYPE_IN[api_type] : nil
|
|
1341
|
+
result[key]['ID'] = first_set(Php.get(c, 'handle'), Php.get(c, 'id'), 0) if key
|
|
1342
|
+
end
|
|
1343
|
+
result
|
|
1344
|
+
end
|
|
1345
|
+
|
|
1346
|
+
def parse_contact_info(data)
|
|
1347
|
+
return [] if Php.empty?(data)
|
|
1348
|
+
|
|
1349
|
+
g = ->(*keys) { first_set(*keys.map { |k| Php.get(data, k) }, '') }
|
|
1350
|
+
{
|
|
1351
|
+
'ID' => g.call('handle', 'id'),
|
|
1352
|
+
'Status' => first_set(Php.get(data, 'status'), 'Active'),
|
|
1353
|
+
'AuthCode' => g.call('authCode'),
|
|
1354
|
+
'FirstName' => g.call('firstName'),
|
|
1355
|
+
'LastName' => g.call('lastName'),
|
|
1356
|
+
'Company' => g.call('companyName', 'organizationName', 'company'),
|
|
1357
|
+
'EMail' => g.call('eMail', 'emailAddress', 'email'),
|
|
1358
|
+
'Type' => g.call('type'),
|
|
1359
|
+
'Address' => {
|
|
1360
|
+
'Line1' => g.call('address', 'addressLine1'),
|
|
1361
|
+
'Line2' => g.call('addressLine2'),
|
|
1362
|
+
'Line3' => g.call('addressLine3'),
|
|
1363
|
+
'State' => g.call('state', 'stateOrProvince'),
|
|
1364
|
+
'City' => g.call('city'),
|
|
1365
|
+
'Country' => g.call('country', 'countryCode'),
|
|
1366
|
+
'ZipCode' => g.call('postalCode', 'zipCode')
|
|
1367
|
+
},
|
|
1368
|
+
'Phone' => {
|
|
1369
|
+
'Phone' => { 'Number' => g.call('phone', 'phoneNumber'), 'CountryCode' => g.call('phoneCountryCode') },
|
|
1370
|
+
'Fax' => { 'Number' => g.call('fax', 'faxNumber'), 'CountryCode' => g.call('faxCountryCode') }
|
|
1371
|
+
},
|
|
1372
|
+
'Additional' => first_set(Php.get(data, 'additionalAttributes'), {})
|
|
1373
|
+
}
|
|
1374
|
+
end
|
|
1375
|
+
|
|
1376
|
+
# Build a gateway contact (ContactLiteDto) from the flat or nested shape.
|
|
1377
|
+
def parse_contact(contact, type)
|
|
1378
|
+
api_type = CONTACT_TYPE_OUT[type] || type
|
|
1379
|
+
|
|
1380
|
+
address = first_set(Php.get(contact, 'AddressLine1'), Php.get(contact, 'Address', 'Line1'), '')
|
|
1381
|
+
line2 = first_set(Php.get(contact, 'AddressLine2'), Php.get(contact, 'Address', 'Line2'), '')
|
|
1382
|
+
address = "#{Php.to_s(address)} #{Php.to_s(line2)}" unless Php.empty?(line2)
|
|
1383
|
+
|
|
1384
|
+
phone = first_set(Php.get(contact, 'Phone'), Php.get(contact, 'Phone', 'Phone', 'Number'), '')
|
|
1385
|
+
phone_cc = first_set(Php.get(contact, 'PhoneCountryCode'), Php.get(contact, 'Phone', 'Phone', 'CountryCode'), '')
|
|
1386
|
+
fax = first_set(Php.get(contact, 'Fax'), Php.get(contact, 'Phone', 'Fax', 'Number'), '')
|
|
1387
|
+
fax_cc = first_set(Php.get(contact, 'FaxCountryCode'), Php.get(contact, 'Phone', 'Fax', 'CountryCode'), '')
|
|
1388
|
+
|
|
1389
|
+
if Php.array?(phone)
|
|
1390
|
+
phone_cc = first_set(Php.get(phone, 'Phone', 'CountryCode'), '')
|
|
1391
|
+
phone = first_set(Php.get(phone, 'Phone', 'Number'), '')
|
|
1392
|
+
end
|
|
1393
|
+
if Php.array?(fax)
|
|
1394
|
+
fax_cc = first_set(Php.get(fax, 'Fax', 'CountryCode'), '')
|
|
1395
|
+
fax = first_set(Php.get(fax, 'Fax', 'Number'), '')
|
|
1396
|
+
end
|
|
1397
|
+
|
|
1398
|
+
validated = validate_contact(
|
|
1399
|
+
'FirstName' => first_set(Php.get(contact, 'FirstName'), ''),
|
|
1400
|
+
'LastName' => first_set(Php.get(contact, 'LastName'), ''),
|
|
1401
|
+
'AddressLine1' => address,
|
|
1402
|
+
'City' => first_set(Php.get(contact, 'City'), Php.get(contact, 'Address', 'City'), ''),
|
|
1403
|
+
'State' => first_set(Php.get(contact, 'State'), Php.get(contact, 'Address', 'State'), ''),
|
|
1404
|
+
'Country' => first_set(Php.get(contact, 'Country'), Php.get(contact, 'Address', 'Country'), ''),
|
|
1405
|
+
'ZipCode' => first_set(Php.get(contact, 'ZipCode'), Php.get(contact, 'Address', 'ZipCode'), ''),
|
|
1406
|
+
'Phone' => phone,
|
|
1407
|
+
'PhoneCountryCode' => phone_cc,
|
|
1408
|
+
'Fax' => fax,
|
|
1409
|
+
'FaxCountryCode' => fax_cc
|
|
1410
|
+
)
|
|
1411
|
+
|
|
1412
|
+
{
|
|
1413
|
+
'contactType' => api_type,
|
|
1414
|
+
'firstName' => validated['FirstName'],
|
|
1415
|
+
'lastName' => validated['LastName'],
|
|
1416
|
+
'companyName' => first_set(Php.get(contact, 'Company'), ''),
|
|
1417
|
+
'eMail' => first_set(Php.get(contact, 'EMail'), ''),
|
|
1418
|
+
'address' => validated['AddressLine1'],
|
|
1419
|
+
'city' => validated['City'],
|
|
1420
|
+
'state' => validated['State'],
|
|
1421
|
+
'country' => validated['Country'],
|
|
1422
|
+
'postalCode' => validated['ZipCode'],
|
|
1423
|
+
'phoneCountryCode' => validated['PhoneCountryCode'],
|
|
1424
|
+
'phone' => validated['Phone'],
|
|
1425
|
+
'faxCountryCode' => validated['FaxCountryCode'],
|
|
1426
|
+
'fax' => validated['Fax'],
|
|
1427
|
+
'isHidden' => Php.truthy?(first_set(Php.get(contact, 'IsHidden'), false))
|
|
1428
|
+
}
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
def sanitize_person_name(name)
|
|
1432
|
+
str = Php.to_s(name)
|
|
1433
|
+
clean = if str.dup.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
1434
|
+
str.dup.force_encoding(Encoding::UTF_8).gsub(/[^\p{L}[[:space:]]'\-]/, '')
|
|
1435
|
+
else
|
|
1436
|
+
str.b.gsub(/[^a-zA-Z\s'\-]/n, '').force_encoding(Encoding::UTF_8)
|
|
1437
|
+
end
|
|
1438
|
+
Php.trim(clean.gsub(/[[:space:]]+/, ' '))
|
|
1439
|
+
end
|
|
1440
|
+
|
|
1441
|
+
COUNTRY_NAMES = {
|
|
1442
|
+
'turkey' => 'TR', 'türkiye' => 'TR', 'turkiye' => 'TR',
|
|
1443
|
+
'united states' => 'US', 'united states of america' => 'US', 'usa' => 'US',
|
|
1444
|
+
'united kingdom' => 'GB', 'great britain' => 'GB', 'england' => 'GB',
|
|
1445
|
+
'germany' => 'DE', 'deutschland' => 'DE', 'france' => 'FR', 'spain' => 'ES',
|
|
1446
|
+
'italy' => 'IT', 'netherlands' => 'NL', 'india' => 'IN', 'china' => 'CN',
|
|
1447
|
+
'russia' => 'RU', 'canada' => 'CA', 'australia' => 'AU', 'brazil' => 'BR'
|
|
1448
|
+
}.freeze
|
|
1449
|
+
|
|
1450
|
+
def normalize_country_code(country)
|
|
1451
|
+
country = Php.trim(country)
|
|
1452
|
+
return '' if country == ''
|
|
1453
|
+
return Php.upper(country) if country.bytesize == 2
|
|
1454
|
+
|
|
1455
|
+
COUNTRY_NAMES.fetch(Php.lower(country)) { country }
|
|
1456
|
+
end
|
|
1457
|
+
|
|
1458
|
+
TWO_DIGIT_CALLING_CODES = %w[20 27 30 31 32 33 34 36 39 40 41 43 44 45 46 47 48 49 51 52 53 54 55 56 57 58
|
|
1459
|
+
60 61 62 63 64 65 66 81 82 84 86 90 91 92 93 94 95 98].freeze
|
|
1460
|
+
|
|
1461
|
+
# E.164 calling code at the start of an international number (digits only).
|
|
1462
|
+
def calling_code_prefix(digits)
|
|
1463
|
+
return '' if digits == '' || digits[0] == '0'
|
|
1464
|
+
return digits[0] if %w[1 7].include?(digits[0])
|
|
1465
|
+
return digits[0, 2] if TWO_DIGIT_CALLING_CODES.include?(digits[0, 2])
|
|
1466
|
+
|
|
1467
|
+
digits.length >= 3 ? digits[0, 3] : ''
|
|
1468
|
+
end
|
|
1469
|
+
|
|
1470
|
+
# Split a free-form phone number into [subscriber number, country code].
|
|
1471
|
+
def normalize_phone(number, country_code)
|
|
1472
|
+
number_s = Php.to_s(number).b
|
|
1473
|
+
intl = number_s.match?(/\A\s*(\+|00)/n)
|
|
1474
|
+
digits = number_s.gsub(/\D/n, '')
|
|
1475
|
+
cc = Php.to_s(country_code).b.gsub(/\D/n, '')
|
|
1476
|
+
|
|
1477
|
+
return ['', cc] if digits == ''
|
|
1478
|
+
|
|
1479
|
+
if intl
|
|
1480
|
+
digits = digits[2..] if digits.start_with?('00')
|
|
1481
|
+
if cc != '' && digits.start_with?(cc) && digits.length > cc.length
|
|
1482
|
+
digits = digits[cc.length..]
|
|
1483
|
+
else
|
|
1484
|
+
# The number carries its own calling code and it differs from the given one.
|
|
1485
|
+
own = calling_code_prefix(digits)
|
|
1486
|
+
if own != '' && digits.length > own.length
|
|
1487
|
+
cc = own
|
|
1488
|
+
digits = digits[own.length..]
|
|
1489
|
+
end
|
|
1490
|
+
end
|
|
1491
|
+
elsif cc != '' && digits.start_with?(cc) && digits.length > 10
|
|
1492
|
+
digits = digits[cc.length..]
|
|
1493
|
+
end
|
|
1494
|
+
|
|
1495
|
+
if cc == ''
|
|
1496
|
+
digits = digits[2..] if digits.length == 12 && digits.start_with?('90')
|
|
1497
|
+
cc = '90'
|
|
1498
|
+
end
|
|
1499
|
+
|
|
1500
|
+
digits = digits.sub(/\A0+/, '') if digits.length > 1 && digits[0] == '0'
|
|
1501
|
+
|
|
1502
|
+
[digits.dup.force_encoding(Encoding::UTF_8), cc.dup.force_encoding(Encoding::UTF_8)]
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
def normalize_host_name(host)
|
|
1506
|
+
Php.lower(Php.rtrim(Php.trim(host), '.'))
|
|
1507
|
+
end
|
|
1508
|
+
|
|
1509
|
+
def normalize_host_name_list(hosts)
|
|
1510
|
+
return [] unless Php.array?(hosts)
|
|
1511
|
+
|
|
1512
|
+
out = []
|
|
1513
|
+
Php.values(hosts).each do |host|
|
|
1514
|
+
next unless host.is_a?(String)
|
|
1515
|
+
|
|
1516
|
+
host = normalize_host_name(host)
|
|
1517
|
+
out << host if host != '' && !out.include?(host)
|
|
1518
|
+
end
|
|
1519
|
+
out
|
|
1520
|
+
end
|
|
1521
|
+
|
|
1522
|
+
TR_KEY_MAP = {
|
|
1523
|
+
'TRABISDOMAINCATEGORY' => 'TRABISDOMAINCATEGORY',
|
|
1524
|
+
'TRABISCITIZENID' => 'TRABISCITIZENID',
|
|
1525
|
+
'TRABISCITIZIENID' => 'TRABISCITIZENID',
|
|
1526
|
+
'TRABISTAXOFFICE' => 'TRABISTAXOFFICE',
|
|
1527
|
+
'TRABISTAXNUMBER' => 'TRABISTAXNUMBER'
|
|
1528
|
+
}.freeze
|
|
1529
|
+
|
|
1530
|
+
def normalize_tr_attributes(attrs)
|
|
1531
|
+
return attrs if attrs.empty?
|
|
1532
|
+
|
|
1533
|
+
normalized = {}
|
|
1534
|
+
attrs.each do |key, value|
|
|
1535
|
+
next unless key.is_a?(String) && TR_KEY_MAP.key?(key)
|
|
1536
|
+
next if key == 'TRABISCITIZIENID' && normalized.key?('TRABISCITIZENID')
|
|
1537
|
+
|
|
1538
|
+
normalized[TR_KEY_MAP[key]] = value
|
|
1539
|
+
end
|
|
1540
|
+
|
|
1541
|
+
unless normalized['TRABISDOMAINCATEGORY'].nil?
|
|
1542
|
+
cat = Php.lower(Php.trim(normalized['TRABISDOMAINCATEGORY']))
|
|
1543
|
+
if %w[1 owner personal individual].include?(cat)
|
|
1544
|
+
normalized['TRABISDOMAINCATEGORY'] = 'Owner'
|
|
1545
|
+
elsif %w[0 corporate company].include?(cat)
|
|
1546
|
+
normalized['TRABISDOMAINCATEGORY'] = 'Corporate'
|
|
1547
|
+
end
|
|
1548
|
+
end
|
|
1549
|
+
normalized
|
|
1550
|
+
end
|
|
1551
|
+
|
|
1552
|
+
def stringify_attributes(attrs)
|
|
1553
|
+
attrs.each_with_object({}) do |(key, value), out|
|
|
1554
|
+
out[key.to_s] = case value
|
|
1555
|
+
when true then 'true'
|
|
1556
|
+
when false then 'false'
|
|
1557
|
+
when nil then ''
|
|
1558
|
+
when String, Integer, Float then Php.to_s(value)
|
|
1559
|
+
else Php.json_encode(value)
|
|
1560
|
+
end
|
|
1561
|
+
end
|
|
1562
|
+
end
|
|
1563
|
+
|
|
1564
|
+
def flatten_validation_errors(errors)
|
|
1565
|
+
parts = []
|
|
1566
|
+
Php.values(errors).each do |ve|
|
|
1567
|
+
if ve.is_a?(String)
|
|
1568
|
+
parts << Php.trim(ve) if Php.trim(ve) != ''
|
|
1569
|
+
next
|
|
1570
|
+
end
|
|
1571
|
+
next unless Php.array?(ve)
|
|
1572
|
+
|
|
1573
|
+
message = Php.isset?(ve, 'message') ? Php.trim(Php.to_s(Php.get(ve, 'message'))) : ''
|
|
1574
|
+
members = []
|
|
1575
|
+
member_list = Php.get(ve, 'members')
|
|
1576
|
+
if Php.array?(member_list)
|
|
1577
|
+
Php.values(member_list).each do |member|
|
|
1578
|
+
label = humanize_validation_member(Php.to_s(member))
|
|
1579
|
+
members << label if label != ''
|
|
1580
|
+
end
|
|
1581
|
+
end
|
|
1582
|
+
|
|
1583
|
+
if !members.empty? && message != ''
|
|
1584
|
+
parts << "#{members.join(', ')} - #{message}"
|
|
1585
|
+
elsif !members.empty?
|
|
1586
|
+
parts << members.join(', ')
|
|
1587
|
+
elsif message != ''
|
|
1588
|
+
parts << message
|
|
1589
|
+
end
|
|
1590
|
+
end
|
|
1591
|
+
parts.uniq.join(' | ')
|
|
1592
|
+
end
|
|
1593
|
+
|
|
1594
|
+
VALIDATION_ROLES = {
|
|
1595
|
+
'administrativecontact' => 'Admin', 'administrative' => 'Admin',
|
|
1596
|
+
'technicalcontact' => 'Tech', 'technical' => 'Tech',
|
|
1597
|
+
'registrantcontact' => 'Registrant', 'registrant' => 'Registrant',
|
|
1598
|
+
'billingcontact' => 'Billing', 'billing' => 'Billing'
|
|
1599
|
+
}.freeze
|
|
1600
|
+
|
|
1601
|
+
def humanize_validation_member(member)
|
|
1602
|
+
member = Php.trim(member)
|
|
1603
|
+
return '' if member == ''
|
|
1604
|
+
|
|
1605
|
+
member = Php.ltrim(member, '$.')
|
|
1606
|
+
member = member.gsub(/\[\d+\]/, '')
|
|
1607
|
+
segments = member.split(%r{[./]+}).reject(&:empty?)
|
|
1608
|
+
return '' if segments.empty?
|
|
1609
|
+
|
|
1610
|
+
segments.map { |seg| VALIDATION_ROLES[Php.lower(seg)] || Php.ucfirst(seg) }.join(' ').then { |s| Php.trim(s) }
|
|
1611
|
+
end
|
|
1612
|
+
|
|
1613
|
+
# --- dates -----------------------------------------------------------------
|
|
1614
|
+
|
|
1615
|
+
ISO_DATE_RE = /\A\s*(-?\d{4,})-(\d{1,2})-(\d{1,2})(?:[T\s]+(\d{1,2}):(\d{2})(?::(\d{2})(?:[.,](\d+))?)?)?\s*(Z|[+-]\d{2}:?\d{2}|[+-]\d{2})?\s*\z/i.freeze
|
|
1616
|
+
|
|
1617
|
+
# strtotime() for the date shapes the gateway sends; nil when unparseable.
|
|
1618
|
+
def parse_api_time(value)
|
|
1619
|
+
return nil if value.nil? || value == ''
|
|
1620
|
+
|
|
1621
|
+
str = Php.to_s(value)
|
|
1622
|
+
if (m = ISO_DATE_RE.match(str))
|
|
1623
|
+
year, month, day = m[1].to_i, m[2].to_i, m[3].to_i
|
|
1624
|
+
hour, min, sec = m[4].to_i, m[5].to_i, m[6].to_i
|
|
1625
|
+
return nil if month < 1 || month > 12 || day < 1 || day > 31 || hour > 24 || min > 59 || sec > 59
|
|
1626
|
+
|
|
1627
|
+
time = Time.utc(year, month, day, hour, min, sec)
|
|
1628
|
+
if m[8] && m[8].upcase != 'Z'
|
|
1629
|
+
sign = m[8].start_with?('-') ? -1 : 1
|
|
1630
|
+
digits = m[8].delete(':+-')
|
|
1631
|
+
offset = (digits[0, 2].to_i * 3600) + (digits[2, 2].to_i * 60)
|
|
1632
|
+
time -= sign * offset
|
|
1633
|
+
end
|
|
1634
|
+
return time
|
|
1635
|
+
end
|
|
1636
|
+
return nil unless str.match?(/\d/)
|
|
1637
|
+
|
|
1638
|
+
Time.parse(str).utc
|
|
1639
|
+
rescue ArgumentError, RangeError
|
|
1640
|
+
nil
|
|
1641
|
+
end
|
|
1642
|
+
|
|
1643
|
+
def format_api_date(value)
|
|
1644
|
+
time = parse_api_time(value)
|
|
1645
|
+
time.nil? ? '' : time.strftime('%Y-%m-%dT%H:%M:%S')
|
|
1646
|
+
end
|
|
1647
|
+
|
|
1648
|
+
def remaining_days_from_date(value)
|
|
1649
|
+
time = parse_api_time(value)
|
|
1650
|
+
return 0 if time.nil?
|
|
1651
|
+
|
|
1652
|
+
[0, ((time - Time.now) / 86_400.0).ceil].max
|
|
1653
|
+
end
|
|
1654
|
+
|
|
1655
|
+
# --- zone helpers ------------------------------------------------------------
|
|
1656
|
+
|
|
1657
|
+
def unsupported_zone_type(type)
|
|
1658
|
+
type = Php.upper(Php.trim(type))
|
|
1659
|
+
return 'Record type is required.' if type == ''
|
|
1660
|
+
return nil if ZONE_RECORD_TYPES.include?(type)
|
|
1661
|
+
|
|
1662
|
+
"Record type \"#{type}\" is not supported by the DNS zone API. Supported types: #{ZONE_RECORD_TYPES.join(', ')}."
|
|
1663
|
+
end
|
|
1664
|
+
|
|
1665
|
+
def relativize_record_name(name, domain_name)
|
|
1666
|
+
name = Php.trim(name)
|
|
1667
|
+
domain = Php.rtrim(Php.lower(Php.trim(domain_name)), '.')
|
|
1668
|
+
return '' if name == '' || name == '@'
|
|
1669
|
+
|
|
1670
|
+
bare = Php.rtrim(name, '.')
|
|
1671
|
+
return '' if Php.casecmp_eq?(bare, domain)
|
|
1672
|
+
|
|
1673
|
+
suffix = ".#{domain}"
|
|
1674
|
+
if bare.bytesize > suffix.bytesize && Php.casecmp_eq?(bare.b[-suffix.bytesize..], suffix.b)
|
|
1675
|
+
return bare.b[0, bare.bytesize - suffix.bytesize].force_encoding(bare.encoding)
|
|
1676
|
+
end
|
|
1677
|
+
|
|
1678
|
+
bare
|
|
1679
|
+
end
|
|
1680
|
+
|
|
1681
|
+
def qualify_record_name(name, domain_name)
|
|
1682
|
+
name = Php.trim(name)
|
|
1683
|
+
domain = Php.rtrim(Php.lower(Php.trim(domain_name)), '.')
|
|
1684
|
+
apex = "#{domain}."
|
|
1685
|
+
return apex if name == '' || name == '@'
|
|
1686
|
+
|
|
1687
|
+
bare = Php.rtrim(name, '.')
|
|
1688
|
+
return "#{bare}." if Php.casecmp_eq?(bare, domain) || Php.lower(bare).end_with?(".#{domain}")
|
|
1689
|
+
|
|
1690
|
+
"#{bare}.#{apex}"
|
|
1691
|
+
end
|
|
1692
|
+
|
|
1693
|
+
def build_zone_struct(name, type, content, ttl)
|
|
1694
|
+
ttl = Php.to_i(ttl)
|
|
1695
|
+
ttl = 3600 if ttl < 1
|
|
1696
|
+
ttl = 86_400 if ttl > 86_400
|
|
1697
|
+
{
|
|
1698
|
+
'name' => Php.to_s(name),
|
|
1699
|
+
'ttl' => ttl,
|
|
1700
|
+
'type' => Php.upper(type),
|
|
1701
|
+
'contents' => Php.values(Php.to_array(content)).map { |v| Php.to_s(v) }
|
|
1702
|
+
}
|
|
1703
|
+
end
|
|
1704
|
+
|
|
1705
|
+
def read_resource_record_set(domain_name, name, type)
|
|
1706
|
+
current = get_resource_records(domain_name)
|
|
1707
|
+
return current if current['result'] != RESULT_OK
|
|
1708
|
+
|
|
1709
|
+
fqdn = qualify_record_name(name, domain_name)
|
|
1710
|
+
type = Php.upper(Php.trim(type))
|
|
1711
|
+
contents = []
|
|
1712
|
+
ttl = 0
|
|
1713
|
+
current['data']['records'].each do |rec|
|
|
1714
|
+
next unless Php.casecmp_eq?(rec['Name'], fqdn) && Php.casecmp_eq?(rec['Type'], type)
|
|
1715
|
+
|
|
1716
|
+
contents << Php.to_s(rec['Content']) if Php.to_s(rec['Content']) != ''
|
|
1717
|
+
ttl = Php.to_i(rec['TTL'])
|
|
1718
|
+
end
|
|
1719
|
+
{ 'ttl' => ttl, 'contents' => contents }
|
|
1720
|
+
end
|
|
1721
|
+
|
|
1722
|
+
def fallback_ttl(set)
|
|
1723
|
+
Php.truthy?(set['ttl']) ? set['ttl'] : 3600
|
|
1724
|
+
end
|
|
1725
|
+
|
|
1726
|
+
def same_record_value?(type, a, b)
|
|
1727
|
+
normalize = lambda do |v|
|
|
1728
|
+
v = Php.trim(Php.to_s(v))
|
|
1729
|
+
if Php.upper(type) == 'TXT'
|
|
1730
|
+
v = v[1...-1] if v.length > 1 && v.start_with?('"') && v.end_with?('"')
|
|
1731
|
+
v
|
|
1732
|
+
else
|
|
1733
|
+
Php.lower(Php.rtrim(v, '.'))
|
|
1734
|
+
end
|
|
1735
|
+
end
|
|
1736
|
+
normalize.call(a) == normalize.call(b)
|
|
1737
|
+
end
|
|
1738
|
+
|
|
1739
|
+
def transfer_action(domain_name, action, default_status, fallback)
|
|
1740
|
+
response = request('POST', "domains/transfers/#{action}", { 'domainName' => domain_name })
|
|
1741
|
+
status = Php.get(response, 'status')
|
|
1742
|
+
{ 'result' => RESULT_OK, 'data' => { 'DomainName' => domain_name, 'Status' => status.nil? ? default_status : status } }
|
|
1743
|
+
rescue ApiError => e
|
|
1744
|
+
error_envelope(e, fallback)
|
|
1745
|
+
end
|
|
1746
|
+
end
|
|
1747
|
+
end
|