domainnameapi 1.0.0__py3-none-any.whl
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.
- domainnameapi/__init__.py +38 -0
- domainnameapi/_php.py +482 -0
- domainnameapi/_version.py +4 -0
- domainnameapi/client.py +275 -0
- domainnameapi/py.typed +0 -0
- domainnameapi/rest.py +1689 -0
- domainnameapi/shared.py +182 -0
- domainnameapi/telemetry.py +471 -0
- domainnameapi/transport.py +217 -0
- domainnameapi-1.0.0.dist-info/METADATA +1637 -0
- domainnameapi-1.0.0.dist-info/RECORD +13 -0
- domainnameapi-1.0.0.dist-info/WHEEL +4 -0
- domainnameapi-1.0.0.dist-info/licenses/LICENSE +201 -0
domainnameapi/rest.py
ADDED
|
@@ -0,0 +1,1689 @@
|
|
|
1
|
+
"""REST client for the DomainNameAPI gateway.
|
|
2
|
+
|
|
3
|
+
Faithful port of php-dna ``DomainNameApi/DNARest.php``: same endpoints, same
|
|
4
|
+
request bodies and query strings, same return envelopes. Every public method
|
|
5
|
+
catches gateway failures and returns ``{'result': 'ERROR', 'error': {...}}``
|
|
6
|
+
instead of raising.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
import unicodedata
|
|
13
|
+
|
|
14
|
+
from . import _php as php
|
|
15
|
+
from .shared import (
|
|
16
|
+
DEFAULT_CONNECT_TIMEOUT,
|
|
17
|
+
DEFAULT_ERRORS,
|
|
18
|
+
DEFAULT_NAMESERVERS,
|
|
19
|
+
DEFAULT_TIMEOUT,
|
|
20
|
+
RESULT_ERROR,
|
|
21
|
+
RESULT_OK,
|
|
22
|
+
USER_AGENT,
|
|
23
|
+
ApiError,
|
|
24
|
+
format_error_code,
|
|
25
|
+
normalize_host_name,
|
|
26
|
+
normalize_host_name_list,
|
|
27
|
+
set_error,
|
|
28
|
+
)
|
|
29
|
+
from .telemetry import Telemetry
|
|
30
|
+
from .transport import CURLE_OPERATION_TIMEDOUT, HttpRequest, TransportError, UrllibTransport
|
|
31
|
+
|
|
32
|
+
URL_PROD = "https://api.domainresellerapi.com/api/v1"
|
|
33
|
+
URL_OTE = "https://ote.domainresellerapi.com/api/v1"
|
|
34
|
+
|
|
35
|
+
# Gateway DomainStatus enum -> textual label.
|
|
36
|
+
DOMAIN_STATUS_MAP = {
|
|
37
|
+
0: "Active",
|
|
38
|
+
1: "WaitingForRegistration",
|
|
39
|
+
2: "WaitingForDocument",
|
|
40
|
+
3: "WaitingForIncomingTransfer",
|
|
41
|
+
4: "TransferredOut",
|
|
42
|
+
7: "PendingDelete",
|
|
43
|
+
8: "Deleted",
|
|
44
|
+
9: "ConfirmationEmailSend",
|
|
45
|
+
11: "WaitingForOutgoingTransfer",
|
|
46
|
+
12: "PendingHold",
|
|
47
|
+
15: "ModificationPending",
|
|
48
|
+
18: "InCase",
|
|
49
|
+
19: "PendingRestore",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Gateway DomainRenewalMode enum -> textual label.
|
|
53
|
+
DOMAIN_RENEWAL_MODE_MAP = {
|
|
54
|
+
1: "AutoRenew",
|
|
55
|
+
2: "AutoExpire",
|
|
56
|
+
3: "AutoDelete",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Zone record types the gateway accepts (probed against the live gateway).
|
|
60
|
+
ZONE_RECORD_TYPES = ["A", "AAAA", "ALIAS", "CAA", "CNAME", "DS", "MX", "NAPTR", "PTR", "SRV", "TLSA", "TXT"]
|
|
61
|
+
|
|
62
|
+
# Redirect types accepted by /domains/forwards.
|
|
63
|
+
FORWARD_TYPES = ["Standard", "Frame"]
|
|
64
|
+
|
|
65
|
+
# Minimum gap between two /domains/zones calls, in seconds.
|
|
66
|
+
ZONE_MIN_INTERVAL = 1.0
|
|
67
|
+
|
|
68
|
+
_HTTP_REASONS = {
|
|
69
|
+
400: "Bad Request",
|
|
70
|
+
401: "Unauthorized",
|
|
71
|
+
402: "Payment Required",
|
|
72
|
+
403: "Forbidden",
|
|
73
|
+
404: "Not Found",
|
|
74
|
+
405: "Method Not Allowed",
|
|
75
|
+
408: "Request Timeout",
|
|
76
|
+
409: "Conflict",
|
|
77
|
+
422: "Unprocessable Entity",
|
|
78
|
+
429: "Too Many Requests",
|
|
79
|
+
500: "Internal Server Error",
|
|
80
|
+
502: "Bad Gateway",
|
|
81
|
+
503: "Service Unavailable",
|
|
82
|
+
504: "Gateway Timeout",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_BOILERPLATE_RE = re.compile(r"Exception of type '[^']*' was thrown", re.IGNORECASE)
|
|
86
|
+
_IPV4_RE = re.compile(r"^(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})$")
|
|
87
|
+
_PCRE_SPACE = " \t\n\x0b\f\r"
|
|
88
|
+
|
|
89
|
+
_COUNTRY_MAP = {
|
|
90
|
+
"turkey": "TR", "türkiye": "TR", "turkiye": "TR",
|
|
91
|
+
"united states": "US", "united states of america": "US", "usa": "US",
|
|
92
|
+
"united kingdom": "GB", "great britain": "GB", "england": "GB",
|
|
93
|
+
"germany": "DE", "deutschland": "DE", "france": "FR", "spain": "ES",
|
|
94
|
+
"italy": "IT", "netherlands": "NL", "india": "IN", "china": "CN",
|
|
95
|
+
"russia": "RU", "canada": "CA", "australia": "AU", "brazil": "BR",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _http_status_reason(status):
|
|
100
|
+
status = php.intval(status)
|
|
101
|
+
if status in _HTTP_REASONS:
|
|
102
|
+
return _HTTP_REASONS[status]
|
|
103
|
+
if status >= 500:
|
|
104
|
+
return "Server Error"
|
|
105
|
+
if status >= 400:
|
|
106
|
+
return "Client Error"
|
|
107
|
+
return "Unexpected Response"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _format_api_date(value):
|
|
111
|
+
"""'Y-m-d\\TH:i:s', or '' when missing or unparseable."""
|
|
112
|
+
if value is None or value == "":
|
|
113
|
+
return ""
|
|
114
|
+
ts = php.strtotime(php.strval(value))
|
|
115
|
+
return "" if ts is None else php.format_timestamp(ts)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _remaining_days_from_date(value):
|
|
119
|
+
if value is None or value == "":
|
|
120
|
+
return 0
|
|
121
|
+
ts = php.strtotime(php.strval(value))
|
|
122
|
+
if ts is None:
|
|
123
|
+
return 0
|
|
124
|
+
return max(0, int(math.ceil((ts - php.now()) / 86400.0)))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _is_ipv4(value):
|
|
128
|
+
if not isinstance(value, str):
|
|
129
|
+
value = php.strval(value)
|
|
130
|
+
m = _IPV4_RE.match(value)
|
|
131
|
+
return bool(m) and all(int(g) <= 255 for g in m.groups())
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _normalize_domain_status(value):
|
|
135
|
+
if php.is_numeric(value):
|
|
136
|
+
return DOMAIN_STATUS_MAP.get(php.intval(value), php.strval(value))
|
|
137
|
+
return php.strval(value)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _sanitize_person_name(name):
|
|
141
|
+
"""Keep letters (any alphabet), whitespace, apostrophes and hyphens."""
|
|
142
|
+
kept = []
|
|
143
|
+
for ch in php.strval(name):
|
|
144
|
+
if ch in "'-" or ch.isspace() or unicodedata.category(ch).startswith("L"):
|
|
145
|
+
kept.append(ch)
|
|
146
|
+
clean = "".join(kept)
|
|
147
|
+
clean = re.sub(r"\s+", " ", clean)
|
|
148
|
+
return php.trim(clean)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _normalize_country_code(country):
|
|
152
|
+
country = php.trim(country)
|
|
153
|
+
if country == "":
|
|
154
|
+
return ""
|
|
155
|
+
if php.strlen(country) == 2:
|
|
156
|
+
return php.strtoupper(country)
|
|
157
|
+
key = php.strtolower(country)
|
|
158
|
+
return _COUNTRY_MAP.get(key, country)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _normalize_phone(number, country_code):
|
|
162
|
+
"""Split a free-form number into (subscriber digits, country code)."""
|
|
163
|
+
number = php.strval(number)
|
|
164
|
+
has_intl_prefix = bool(re.match(r"^[" + _PCRE_SPACE + r"]*(\+|00)", number))
|
|
165
|
+
digits = re.sub(r"[^0-9]", "", number)
|
|
166
|
+
cc = re.sub(r"[^0-9]", "", php.strval(country_code))
|
|
167
|
+
|
|
168
|
+
if digits == "":
|
|
169
|
+
return "", cc
|
|
170
|
+
|
|
171
|
+
if has_intl_prefix:
|
|
172
|
+
if digits.startswith("00"):
|
|
173
|
+
digits = digits[2:]
|
|
174
|
+
if cc != "" and digits.startswith(cc) and len(digits) > len(cc):
|
|
175
|
+
digits = digits[len(cc):]
|
|
176
|
+
else:
|
|
177
|
+
# The number carries its own country code and it differs from the given one.
|
|
178
|
+
own = _calling_code_prefix(digits)
|
|
179
|
+
if own != "" and len(digits) > len(own):
|
|
180
|
+
cc = own
|
|
181
|
+
digits = digits[len(own):]
|
|
182
|
+
elif cc != "" and digits.startswith(cc) and len(digits) > 10:
|
|
183
|
+
digits = digits[len(cc):]
|
|
184
|
+
|
|
185
|
+
if cc == "":
|
|
186
|
+
if len(digits) == 12 and digits.startswith("90"):
|
|
187
|
+
cc = "90"
|
|
188
|
+
digits = digits[2:]
|
|
189
|
+
else:
|
|
190
|
+
cc = "90"
|
|
191
|
+
|
|
192
|
+
if len(digits) > 1 and digits[0] == "0":
|
|
193
|
+
digits = digits.lstrip("0")
|
|
194
|
+
|
|
195
|
+
return digits, cc
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
_TWO_DIGIT_CODES = (
|
|
199
|
+
"20", "27", "30", "31", "32", "33", "34", "36", "39", "40", "41", "43", "44", "45", "46", "47",
|
|
200
|
+
"48", "49", "51", "52", "53", "54", "55", "56", "57", "58", "60", "61", "62", "63", "64", "65",
|
|
201
|
+
"66", "81", "82", "84", "86", "90", "91", "92", "93", "94", "95", "98",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _calling_code_prefix(digits):
|
|
206
|
+
"""E.164 country calling code at the start of an international number."""
|
|
207
|
+
if digits == "" or digits[0] == "0":
|
|
208
|
+
return ""
|
|
209
|
+
if digits[0] in ("1", "7"):
|
|
210
|
+
return digits[0]
|
|
211
|
+
if digits[:2] in _TWO_DIGIT_CODES:
|
|
212
|
+
return digits[:2]
|
|
213
|
+
return digits[:3] if len(digits) >= 3 else ""
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _flatten_validation_errors(validation_errors):
|
|
217
|
+
parts = []
|
|
218
|
+
for ve in php.values(validation_errors):
|
|
219
|
+
if isinstance(ve, str):
|
|
220
|
+
if php.trim(ve) != "":
|
|
221
|
+
parts.append(php.trim(ve))
|
|
222
|
+
continue
|
|
223
|
+
if not php.is_array(ve):
|
|
224
|
+
continue
|
|
225
|
+
message = php.trim(php.strval(php.get(ve, "message"))) if php.get(ve, "message") is not None else ""
|
|
226
|
+
members = []
|
|
227
|
+
member_list = php.get(ve, "members")
|
|
228
|
+
if php.is_array(member_list):
|
|
229
|
+
for member in php.values(member_list):
|
|
230
|
+
label = _humanize_validation_member(php.strval(member))
|
|
231
|
+
if label != "":
|
|
232
|
+
members.append(label)
|
|
233
|
+
if members and message != "":
|
|
234
|
+
parts.append(", ".join(members) + " - " + message)
|
|
235
|
+
elif members:
|
|
236
|
+
parts.append(", ".join(members))
|
|
237
|
+
elif message != "":
|
|
238
|
+
parts.append(message)
|
|
239
|
+
|
|
240
|
+
unique = []
|
|
241
|
+
for p in parts:
|
|
242
|
+
if p not in unique:
|
|
243
|
+
unique.append(p)
|
|
244
|
+
return " | ".join(unique)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
_ROLES = {
|
|
248
|
+
"administrativecontact": "Admin", "administrative": "Admin",
|
|
249
|
+
"technicalcontact": "Tech", "technical": "Tech",
|
|
250
|
+
"registrantcontact": "Registrant", "registrant": "Registrant",
|
|
251
|
+
"billingcontact": "Billing", "billing": "Billing",
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _humanize_validation_member(member):
|
|
256
|
+
member = php.trim(member)
|
|
257
|
+
if member == "":
|
|
258
|
+
return ""
|
|
259
|
+
member = member.lstrip("$.")
|
|
260
|
+
member = re.sub(r"\[\d+\]", "", member)
|
|
261
|
+
segments = [s for s in re.split(r"[./]+", member) if s != ""]
|
|
262
|
+
if not segments:
|
|
263
|
+
return ""
|
|
264
|
+
out = []
|
|
265
|
+
for seg in segments:
|
|
266
|
+
out.append(_ROLES.get(php.strtolower(seg), php.ucfirst(seg)))
|
|
267
|
+
return php.trim(" ".join(out))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _stringify_attributes(attrs):
|
|
271
|
+
out = {}
|
|
272
|
+
for key, value in php.items(attrs):
|
|
273
|
+
if isinstance(value, bool):
|
|
274
|
+
out[key] = "true" if value else "false"
|
|
275
|
+
elif value is None:
|
|
276
|
+
out[key] = ""
|
|
277
|
+
elif isinstance(value, (str, int, float)):
|
|
278
|
+
out[key] = php.strval(value)
|
|
279
|
+
else:
|
|
280
|
+
out[key] = php.json_encode(value)
|
|
281
|
+
return out
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
_TR_KEY_MAP = {
|
|
285
|
+
"TRABISDOMAINCATEGORY": "TRABISDOMAINCATEGORY",
|
|
286
|
+
"TRABISCITIZENID": "TRABISCITIZENID",
|
|
287
|
+
"TRABISCITIZIENID": "TRABISCITIZENID", # long-standing typo -> canonical
|
|
288
|
+
"TRABISTAXOFFICE": "TRABISTAXOFFICE",
|
|
289
|
+
"TRABISTAXNUMBER": "TRABISTAXNUMBER",
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _normalize_tr_attributes(attrs):
|
|
294
|
+
"""Reduce legacy .tr TRABIS attributes to the REST schema."""
|
|
295
|
+
if not php.is_array(attrs) or not attrs:
|
|
296
|
+
return attrs if php.is_array(attrs) else []
|
|
297
|
+
normalized = {}
|
|
298
|
+
for key, value in php.items(attrs):
|
|
299
|
+
if not isinstance(key, str) or key not in _TR_KEY_MAP:
|
|
300
|
+
continue
|
|
301
|
+
if key == "TRABISCITIZIENID" and "TRABISCITIZENID" in normalized:
|
|
302
|
+
continue
|
|
303
|
+
normalized[_TR_KEY_MAP[key]] = value
|
|
304
|
+
if normalized.get("TRABISDOMAINCATEGORY") is not None:
|
|
305
|
+
cat = php.strtolower(php.trim(php.strval(normalized["TRABISDOMAINCATEGORY"])))
|
|
306
|
+
if cat in ("1", "owner", "personal", "individual"):
|
|
307
|
+
normalized["TRABISDOMAINCATEGORY"] = "Owner"
|
|
308
|
+
elif cat in ("0", "corporate", "company"):
|
|
309
|
+
normalized["TRABISDOMAINCATEGORY"] = "Corporate"
|
|
310
|
+
return normalized
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _relativize_record_name(name, domain_name):
|
|
314
|
+
"""Bare label the gateway expects; the apex becomes ''."""
|
|
315
|
+
name = php.trim(name)
|
|
316
|
+
domain = php.rtrim(php.strtolower(php.trim(domain_name)), ".")
|
|
317
|
+
if name == "" or name == "@":
|
|
318
|
+
return ""
|
|
319
|
+
bare = php.rtrim(name, ".")
|
|
320
|
+
if php.strcasecmp_equal(bare, domain):
|
|
321
|
+
return ""
|
|
322
|
+
suffix = "." + domain
|
|
323
|
+
if len(bare) > len(suffix) and php.strcasecmp_equal(bare[-len(suffix):], suffix):
|
|
324
|
+
return bare[:-len(suffix)]
|
|
325
|
+
return bare
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _qualify_record_name(name, domain_name):
|
|
329
|
+
"""FQDN form the zone listing returns ("www" -> "www.example.com.")."""
|
|
330
|
+
name = php.trim(name)
|
|
331
|
+
domain = php.rtrim(php.strtolower(php.trim(domain_name)), ".")
|
|
332
|
+
apex = domain + "."
|
|
333
|
+
if name == "" or name == "@":
|
|
334
|
+
return apex
|
|
335
|
+
bare = php.rtrim(name, ".")
|
|
336
|
+
suffix = "." + domain
|
|
337
|
+
if php.strcasecmp_equal(bare, domain) or php.strtolower(bare)[-len(suffix):] == suffix:
|
|
338
|
+
return bare + "."
|
|
339
|
+
return bare + "." + apex
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _unsupported_zone_type(type_):
|
|
343
|
+
type_ = php.strtoupper(php.trim(type_))
|
|
344
|
+
if type_ == "":
|
|
345
|
+
return "Record type is required."
|
|
346
|
+
if type_ not in ZONE_RECORD_TYPES:
|
|
347
|
+
return 'Record type "%s" is not supported by the DNS zone API. Supported types: %s.' % (
|
|
348
|
+
type_, ", ".join(ZONE_RECORD_TYPES))
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _same_record_value(type_, a, b):
|
|
353
|
+
is_txt = php.strtoupper(type_) == "TXT"
|
|
354
|
+
|
|
355
|
+
def normalize(v):
|
|
356
|
+
v = php.trim(v)
|
|
357
|
+
if is_txt:
|
|
358
|
+
if len(v) > 1 and v[0] == '"' and v[-1] == '"':
|
|
359
|
+
v = v[1:-1]
|
|
360
|
+
return v
|
|
361
|
+
return php.strtolower(php.rtrim(v, "."))
|
|
362
|
+
|
|
363
|
+
return normalize(a) == normalize(b)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _build_zone_struct(name, type_, content, ttl):
|
|
367
|
+
ttl = php.intval(ttl)
|
|
368
|
+
if ttl < 1:
|
|
369
|
+
ttl = 3600
|
|
370
|
+
if ttl > 86400:
|
|
371
|
+
ttl = 86400
|
|
372
|
+
return {
|
|
373
|
+
"name": php.strval(name),
|
|
374
|
+
"ttl": ttl,
|
|
375
|
+
"type": php.strtoupper(php.strval(type_)),
|
|
376
|
+
"contents": [php.strval(v) for v in php.values(php.array_cast(content))],
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _parse_contact_ids(contacts):
|
|
381
|
+
result = {
|
|
382
|
+
"Billing": {"ID": 0},
|
|
383
|
+
"Technical": {"ID": 0},
|
|
384
|
+
"Administrative": {"ID": 0},
|
|
385
|
+
"Registrant": {"ID": 0},
|
|
386
|
+
}
|
|
387
|
+
if not php.is_array(contacts):
|
|
388
|
+
return result
|
|
389
|
+
type_map = {"Billing": "Billing", "Tech": "Technical", "Admin": "Administrative", "Registrant": "Registrant"}
|
|
390
|
+
for c in php.values(contacts):
|
|
391
|
+
api_type = php.coalesce(php.get(c, "ContactType"), php.get(c, "contactType"), "")
|
|
392
|
+
key = type_map.get(api_type) if isinstance(api_type, str) else None
|
|
393
|
+
if key:
|
|
394
|
+
result[key]["ID"] = php.coalesce(php.get(c, "handle"), php.get(c, "id"), 0)
|
|
395
|
+
return result
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _parse_contact_info(data):
|
|
399
|
+
if php.empty(data):
|
|
400
|
+
return []
|
|
401
|
+
g = lambda *k: php.get(data, *k) # noqa: E731
|
|
402
|
+
return {
|
|
403
|
+
"ID": php.coalesce(g("handle"), g("id"), ""),
|
|
404
|
+
"Status": php.coalesce(g("status"), "Active"),
|
|
405
|
+
"AuthCode": php.coalesce(g("authCode"), ""),
|
|
406
|
+
"FirstName": php.coalesce(g("firstName"), ""),
|
|
407
|
+
"LastName": php.coalesce(g("lastName"), ""),
|
|
408
|
+
"Company": php.coalesce(g("companyName"), g("organizationName"), g("company"), ""),
|
|
409
|
+
"EMail": php.coalesce(g("eMail"), g("emailAddress"), g("email"), ""),
|
|
410
|
+
"Type": php.coalesce(g("type"), ""),
|
|
411
|
+
"Address": {
|
|
412
|
+
"Line1": php.coalesce(g("address"), g("addressLine1"), ""),
|
|
413
|
+
"Line2": php.coalesce(g("addressLine2"), ""),
|
|
414
|
+
"Line3": php.coalesce(g("addressLine3"), ""),
|
|
415
|
+
"State": php.coalesce(g("state"), g("stateOrProvince"), ""),
|
|
416
|
+
"City": php.coalesce(g("city"), ""),
|
|
417
|
+
"Country": php.coalesce(g("country"), g("countryCode"), ""),
|
|
418
|
+
"ZipCode": php.coalesce(g("postalCode"), g("zipCode"), ""),
|
|
419
|
+
},
|
|
420
|
+
"Phone": {
|
|
421
|
+
"Phone": {
|
|
422
|
+
"Number": php.coalesce(g("phone"), g("phoneNumber"), ""),
|
|
423
|
+
"CountryCode": php.coalesce(g("phoneCountryCode"), ""),
|
|
424
|
+
},
|
|
425
|
+
"Fax": {
|
|
426
|
+
"Number": php.coalesce(g("fax"), g("faxNumber"), ""),
|
|
427
|
+
"CountryCode": php.coalesce(g("faxCountryCode"), ""),
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
"Additional": php.coalesce(g("additionalAttributes"), []),
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _map_values(value, fn):
|
|
435
|
+
"""PHP ``array_map`` (keys of an associative array are preserved)."""
|
|
436
|
+
if isinstance(value, dict):
|
|
437
|
+
return {k: fn(v) for k, v in value.items()}
|
|
438
|
+
return [fn(v) for v in php.values(value)]
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class DNARest(object):
|
|
442
|
+
"""Low-level REST client. Use :class:`domainnameapi.DomainNameAPI`."""
|
|
443
|
+
|
|
444
|
+
URL_PROD = URL_PROD
|
|
445
|
+
URL_OTE = URL_OTE
|
|
446
|
+
ZONE_RECORD_TYPES = ZONE_RECORD_TYPES
|
|
447
|
+
FORWARD_TYPES = FORWARD_TYPES
|
|
448
|
+
|
|
449
|
+
def __init__(self, reseller_id, token, test_mode=False, transport=None, sentry_dsn=None):
|
|
450
|
+
self.service_url = URL_OTE if test_mode else URL_PROD
|
|
451
|
+
self.reseller_id = reseller_id
|
|
452
|
+
self.token = token
|
|
453
|
+
self.transport = transport if transport is not None else UrllibTransport()
|
|
454
|
+
self.ssl_verify = True
|
|
455
|
+
self.timeout = DEFAULT_TIMEOUT
|
|
456
|
+
self.connect_timeout = DEFAULT_CONNECT_TIMEOUT
|
|
457
|
+
self.zone_min_interval = ZONE_MIN_INTERVAL
|
|
458
|
+
self._last_zone_request_at = 0.0
|
|
459
|
+
self._transport_failed = False # last request failed in transport, not with an HTTP status
|
|
460
|
+
self._last_gateway_code = "" # the gateway's own error code from the last failed request
|
|
461
|
+
|
|
462
|
+
self.last_request = {}
|
|
463
|
+
self.last_response = {}
|
|
464
|
+
self.last_response_headers = {}
|
|
465
|
+
self.last_parsed_response = {}
|
|
466
|
+
self.last_function = ""
|
|
467
|
+
self.started_at = time.time()
|
|
468
|
+
|
|
469
|
+
self.telemetry = Telemetry(self, sentry_dsn)
|
|
470
|
+
|
|
471
|
+
# ------------------------------------------------------------------
|
|
472
|
+
# Settings and diagnostics
|
|
473
|
+
# ------------------------------------------------------------------
|
|
474
|
+
|
|
475
|
+
def set_ssl_verification(self, enabled):
|
|
476
|
+
self.ssl_verify = bool(enabled)
|
|
477
|
+
|
|
478
|
+
def set_error_reporting_enabled(self, enabled):
|
|
479
|
+
self.telemetry.enabled = bool(enabled)
|
|
480
|
+
|
|
481
|
+
def set_telemetry_minimal(self, minimal):
|
|
482
|
+
self.telemetry.minimal = bool(minimal)
|
|
483
|
+
|
|
484
|
+
def get_request_data(self):
|
|
485
|
+
return self.last_request
|
|
486
|
+
|
|
487
|
+
def set_request_data(self, request):
|
|
488
|
+
self.last_request = request
|
|
489
|
+
return self
|
|
490
|
+
|
|
491
|
+
def get_response_data(self):
|
|
492
|
+
return self.last_response
|
|
493
|
+
|
|
494
|
+
def set_response_data(self, response):
|
|
495
|
+
self.last_response = response
|
|
496
|
+
return self
|
|
497
|
+
|
|
498
|
+
def get_response_headers(self):
|
|
499
|
+
return self.last_response_headers
|
|
500
|
+
|
|
501
|
+
def set_response_headers(self, headers):
|
|
502
|
+
self.last_response_headers = headers
|
|
503
|
+
return self
|
|
504
|
+
|
|
505
|
+
def get_parsed_response_data(self):
|
|
506
|
+
return self.last_parsed_response
|
|
507
|
+
|
|
508
|
+
def set_parsed_response_data(self, response):
|
|
509
|
+
self.last_parsed_response = response
|
|
510
|
+
return self
|
|
511
|
+
|
|
512
|
+
def get_last_function(self):
|
|
513
|
+
return self.last_function
|
|
514
|
+
|
|
515
|
+
def set_last_function(self, function):
|
|
516
|
+
self.last_function = function
|
|
517
|
+
return self
|
|
518
|
+
|
|
519
|
+
def get_service_url(self):
|
|
520
|
+
return self.service_url
|
|
521
|
+
|
|
522
|
+
def set_service_url(self, url):
|
|
523
|
+
self.service_url = url
|
|
524
|
+
|
|
525
|
+
# ------------------------------------------------------------------
|
|
526
|
+
# HTTP
|
|
527
|
+
# ------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
def _pace_zone_request(self, endpoint):
|
|
530
|
+
"""Keep consecutive /domains/zones calls ``zone_min_interval`` seconds apart."""
|
|
531
|
+
if not php.ltrim(endpoint, "/").startswith("domains/zones"):
|
|
532
|
+
return
|
|
533
|
+
if self._last_zone_request_at > 0.0 and self.zone_min_interval > 0:
|
|
534
|
+
wait = self.zone_min_interval - (time.time() - self._last_zone_request_at)
|
|
535
|
+
if wait > 0:
|
|
536
|
+
time.sleep(wait)
|
|
537
|
+
self._last_zone_request_at = time.time()
|
|
538
|
+
|
|
539
|
+
def _err(self, code, message="", details=""):
|
|
540
|
+
return set_error(code, message, details, self._last_gateway_code)
|
|
541
|
+
|
|
542
|
+
def _code(self, exc):
|
|
543
|
+
return format_error_code(exc.code, self._transport_failed)
|
|
544
|
+
|
|
545
|
+
def _error_details(self, exc, fallback=None):
|
|
546
|
+
"""Details for a catch block: parsed Details, else raw body, else the message."""
|
|
547
|
+
details = php.get(self.last_parsed_response, "Details")
|
|
548
|
+
if details is not None:
|
|
549
|
+
return details
|
|
550
|
+
raw = php.get(self.last_response, "raw_response")
|
|
551
|
+
if raw is not None:
|
|
552
|
+
return raw
|
|
553
|
+
return exc.message if fallback is None else fallback
|
|
554
|
+
|
|
555
|
+
def _fail(self, exc, default_code):
|
|
556
|
+
code = self._code(exc) or default_code
|
|
557
|
+
return {
|
|
558
|
+
"result": RESULT_ERROR,
|
|
559
|
+
"error": self._err(code, exc.message, self._error_details(exc)),
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
def _request(self, method, endpoint, data=None):
|
|
563
|
+
"""Send one gateway call; returns the decoded body or raises ApiError."""
|
|
564
|
+
if data is None:
|
|
565
|
+
data = {}
|
|
566
|
+
self._pace_zone_request(endpoint)
|
|
567
|
+
self._transport_failed = False
|
|
568
|
+
self._last_gateway_code = ""
|
|
569
|
+
self.started_at = time.time()
|
|
570
|
+
|
|
571
|
+
endpoint = php.strval(endpoint)
|
|
572
|
+
self.last_function = "request:" + method + " " + endpoint.lstrip("?").split("?", 1)[0]
|
|
573
|
+
url = php.strval(self.service_url) + "/" + endpoint.lstrip("/")
|
|
574
|
+
self.last_request = {"url": url, "method": method, "payload": data}
|
|
575
|
+
|
|
576
|
+
headers = {
|
|
577
|
+
"Content-Type": "application/json",
|
|
578
|
+
"Accept": "application/json",
|
|
579
|
+
"X-API-KEY": php.strval(self.token),
|
|
580
|
+
"__reseller": php.strval(self.reseller_id),
|
|
581
|
+
"User-Agent": USER_AGENT,
|
|
582
|
+
}
|
|
583
|
+
body = None
|
|
584
|
+
if method in ("GET", "DELETE"):
|
|
585
|
+
if not php.empty(data):
|
|
586
|
+
url += "?" + php.http_build_query(data)
|
|
587
|
+
else:
|
|
588
|
+
body = php.json_encode(data)
|
|
589
|
+
|
|
590
|
+
req = HttpRequest(method, url, headers, body, self.timeout, self.connect_timeout, self.ssl_verify)
|
|
591
|
+
try:
|
|
592
|
+
resp = self.transport.send(req)
|
|
593
|
+
except TransportError as err:
|
|
594
|
+
self._transport_failed = True
|
|
595
|
+
self.last_response_headers = {"url": url, "http_code": 0, "total_time": time.time() - self.started_at}
|
|
596
|
+
self.last_response = {"raw_response": False}
|
|
597
|
+
if err.errno == CURLE_OPERATION_TIMEDOUT:
|
|
598
|
+
message = "Could not reach the DomainNameAPI service: the connection timed out. (" + err.message + ")"
|
|
599
|
+
else:
|
|
600
|
+
message = "Curl error during request: " + err.message
|
|
601
|
+
self.last_parsed_response = self._err("CURL_%d" % err.errno, message, message)
|
|
602
|
+
error = ApiError(message, err.errno)
|
|
603
|
+
self.telemetry.send_error(error)
|
|
604
|
+
raise error
|
|
605
|
+
|
|
606
|
+
status = resp.status
|
|
607
|
+
body_text = resp.body
|
|
608
|
+
self.last_response_headers = {
|
|
609
|
+
"url": url,
|
|
610
|
+
"http_code": status,
|
|
611
|
+
"content_type": resp.headers.get("Content-Type", resp.headers.get("content-type")),
|
|
612
|
+
"total_time": time.time() - self.started_at,
|
|
613
|
+
"headers": dict(resp.headers),
|
|
614
|
+
}
|
|
615
|
+
decoded = php.json_decode(body_text)
|
|
616
|
+
self.last_response = decoded if decoded is not None else {"raw_response": body_text}
|
|
617
|
+
|
|
618
|
+
if 200 <= status <= 299:
|
|
619
|
+
parsed = decoded
|
|
620
|
+
self.last_parsed_response = parsed
|
|
621
|
+
duration = (time.time() - self.started_at) * 1000
|
|
622
|
+
if self.telemetry.enabled and self.telemetry.should_sample(duration):
|
|
623
|
+
self.telemetry.send_performance(duration, self.started_at, time.time())
|
|
624
|
+
return parsed
|
|
625
|
+
|
|
626
|
+
parsed = decoded
|
|
627
|
+
if status in (301, 302):
|
|
628
|
+
parsed = {
|
|
629
|
+
"message": "Invalid API credentials",
|
|
630
|
+
"code": "CREDENTIALS",
|
|
631
|
+
"details": "Authentication failed. Check your API key and reseller ID.",
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
error_message = php.coalesce(php.get(parsed, "message"), php.get(parsed, "error", "message"), body_text)
|
|
635
|
+
error_code = php.coalesce(php.get(parsed, "code"), php.get(parsed, "error", "code"), "HTTP_%d" % status)
|
|
636
|
+
api_details = php.coalesce(php.get(parsed, "details"), php.get(parsed, "error", "details"))
|
|
637
|
+
error_details = php.coalesce(api_details, body_text)
|
|
638
|
+
# The gateway's own code (e.g. Dna.DomainService:Domain:10007) is kept as GatewayCode.
|
|
639
|
+
if (isinstance(error_code, str) and error_code != "" and not error_code.startswith("HTTP_")
|
|
640
|
+
and error_code != "CREDENTIALS"):
|
|
641
|
+
self._last_gateway_code = error_code
|
|
642
|
+
|
|
643
|
+
if php.trim(error_message) == "":
|
|
644
|
+
error_message = "HTTP %d - %s" % (status, _http_status_reason(status))
|
|
645
|
+
if php.trim(error_details) == "":
|
|
646
|
+
error_details = error_message
|
|
647
|
+
|
|
648
|
+
is_boilerplate = bool(_BOILERPLATE_RE.search(php.strval(error_message)))
|
|
649
|
+
if is_boilerplate or php.strval(error_message) == php.strval(error_code):
|
|
650
|
+
details_clean = php.trim(re.sub(r"^Dna[" + _PCRE_SPACE + r"]+", "", php.strval(api_details), count=1))
|
|
651
|
+
if (details_clean != ""
|
|
652
|
+
and details_clean != php.strval(error_message)
|
|
653
|
+
and details_clean != php.strval(error_code)
|
|
654
|
+
and not _BOILERPLATE_RE.search(details_clean)):
|
|
655
|
+
error_message = details_clean
|
|
656
|
+
else:
|
|
657
|
+
error_message = ("The operation could not be completed by the registry (error: "
|
|
658
|
+
+ php.strval(error_code) + "). Please try again or contact support.")
|
|
659
|
+
|
|
660
|
+
validation_errors = php.coalesce(php.get(parsed, "error", "validationErrors"), php.get(parsed, "validationErrors"))
|
|
661
|
+
if php.is_array(validation_errors) and validation_errors:
|
|
662
|
+
flattened = _flatten_validation_errors(validation_errors)
|
|
663
|
+
if flattened != "":
|
|
664
|
+
error_details = flattened
|
|
665
|
+
|
|
666
|
+
self.last_parsed_response = self._err(error_code, error_message, error_details)
|
|
667
|
+
error = ApiError(php.strval(error_message), status)
|
|
668
|
+
self.telemetry.send_error(error)
|
|
669
|
+
raise error
|
|
670
|
+
|
|
671
|
+
# ------------------------------------------------------------------
|
|
672
|
+
# Account
|
|
673
|
+
# ------------------------------------------------------------------
|
|
674
|
+
|
|
675
|
+
def get_reseller_details(self):
|
|
676
|
+
try:
|
|
677
|
+
response = self._request("GET", "deposit/accounts/me")
|
|
678
|
+
resp = {}
|
|
679
|
+
if php.get(response, "resellerId") is not None:
|
|
680
|
+
usd = php.number_format(php.floatval(php.coalesce(php.get(response, "usdBalance"), 0)), 2)
|
|
681
|
+
resp["result"] = RESULT_OK
|
|
682
|
+
resp["id"] = php.get(response, "resellerId")
|
|
683
|
+
resp["active"] = True # the API returns no status
|
|
684
|
+
resp["name"] = php.coalesce(php.get(response, "resellerName"), "")
|
|
685
|
+
resp["balance"] = usd
|
|
686
|
+
resp["currency"] = "USD"
|
|
687
|
+
resp["symbol"] = "$"
|
|
688
|
+
resp["balances"] = [
|
|
689
|
+
{"balance": usd, "currency": "USD", "symbol": "$"},
|
|
690
|
+
{
|
|
691
|
+
"balance": php.number_format(php.floatval(php.coalesce(php.get(response, "tryBalance"), 0)), 2),
|
|
692
|
+
"currency": "TL",
|
|
693
|
+
"symbol": "TL",
|
|
694
|
+
},
|
|
695
|
+
]
|
|
696
|
+
else:
|
|
697
|
+
resp["result"] = RESULT_ERROR
|
|
698
|
+
resp["error"] = self._err("CREDENTIALS", "Invalid response format",
|
|
699
|
+
"Response does not contain required fields")
|
|
700
|
+
return resp
|
|
701
|
+
except ApiError as e:
|
|
702
|
+
return self._fail(e, "RESELLER_DETAILS")
|
|
703
|
+
|
|
704
|
+
def get_current_balance(self, currency_id="USD"):
|
|
705
|
+
currency_name = php.strtoupper(currency_id)
|
|
706
|
+
currency_map = {
|
|
707
|
+
"USD": {"id": 2, "name": "USD", "symbol": "$"},
|
|
708
|
+
"TRY": {"id": 1, "name": "TL", "symbol": "TL"},
|
|
709
|
+
"EUR": {"id": 3, "name": "EUR", "symbol": "€"},
|
|
710
|
+
"GBP": {"id": 4, "name": "GBP", "symbol": "£"},
|
|
711
|
+
}
|
|
712
|
+
mapped = currency_map.get(currency_name, {"id": 0, "name": currency_name, "symbol": ""})
|
|
713
|
+
try:
|
|
714
|
+
response = self._request("GET", "deposit/accounts/me", {"currency": currency_name})
|
|
715
|
+
balance_key = php.strtolower(currency_id) + "Balance"
|
|
716
|
+
if not isinstance(response, dict) or balance_key not in response:
|
|
717
|
+
# The account only reports USD and TRY balances.
|
|
718
|
+
return {
|
|
719
|
+
"ErrorCode": "BALANCE",
|
|
720
|
+
"OperationMessage": "No %s balance is reported for this account. Available: USD, TRY." % currency_name,
|
|
721
|
+
"OperationResult": "FAILED",
|
|
722
|
+
"Balance": "0.00",
|
|
723
|
+
"CurrencyId": mapped["id"],
|
|
724
|
+
"CurrencyInfo": None,
|
|
725
|
+
"CurrencyName": mapped["name"],
|
|
726
|
+
"CurrencySymbol": mapped["symbol"],
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
"ErrorCode": 0,
|
|
730
|
+
"OperationMessage": "Command completed succesfully.",
|
|
731
|
+
"OperationResult": "SUCCESS",
|
|
732
|
+
"Balance": php.number_format(php.floatval(php.coalesce(php.get(response, balance_key), 0)), 2),
|
|
733
|
+
"CurrencyId": mapped["id"],
|
|
734
|
+
"CurrencyInfo": None,
|
|
735
|
+
"CurrencyName": mapped["name"],
|
|
736
|
+
"CurrencySymbol": mapped["symbol"],
|
|
737
|
+
}
|
|
738
|
+
except ApiError as e:
|
|
739
|
+
return {
|
|
740
|
+
"ErrorCode": self._code(e) or "BALANCE",
|
|
741
|
+
"OperationMessage": e.message,
|
|
742
|
+
"OperationResult": "FAILED",
|
|
743
|
+
"Balance": "0.00",
|
|
744
|
+
"CurrencyId": mapped["id"],
|
|
745
|
+
"CurrencyInfo": None,
|
|
746
|
+
"CurrencyName": mapped["name"],
|
|
747
|
+
"CurrencySymbol": mapped["symbol"],
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
# ------------------------------------------------------------------
|
|
751
|
+
# Search and lists
|
|
752
|
+
# ------------------------------------------------------------------
|
|
753
|
+
|
|
754
|
+
def check_availability(self, domains, extensions, period, command="create"):
|
|
755
|
+
try:
|
|
756
|
+
queries = []
|
|
757
|
+
for domain in php.values(domains):
|
|
758
|
+
for tld in php.values(extensions):
|
|
759
|
+
queries.append({"domainName": php.strval(domain) + "." + php.ltrim(tld, ".")})
|
|
760
|
+
|
|
761
|
+
if not queries:
|
|
762
|
+
return {
|
|
763
|
+
"result": RESULT_ERROR,
|
|
764
|
+
"error": self._err("AVAILABILITY", "Domain names not found.",
|
|
765
|
+
"No domain names provided for availability check"),
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
response = self._request("POST", "domains/bulk-search", queries)
|
|
769
|
+
|
|
770
|
+
availability = []
|
|
771
|
+
items = php.coalesce(php.get(response, "infos"), response)
|
|
772
|
+
if php.is_array(items):
|
|
773
|
+
for item in php.values(items):
|
|
774
|
+
if not php.is_array(item):
|
|
775
|
+
continue
|
|
776
|
+
tld_value = php.get(item, "tld")
|
|
777
|
+
if tld_value is None:
|
|
778
|
+
name = php.strval(php.coalesce(php.get(item, "domainName"), ""))
|
|
779
|
+
pos = name.rfind(".")
|
|
780
|
+
tld_value = name[pos + 1:] if pos >= 0 else ""
|
|
781
|
+
tld = php.strtolower(tld_value)
|
|
782
|
+
domain_raw = php.coalesce(php.get(item, "domainName"), "")
|
|
783
|
+
# Strip only the trailing ".tld".
|
|
784
|
+
lower = php.strtolower(domain_raw)
|
|
785
|
+
suffix = "." + tld
|
|
786
|
+
domain_name = lower[:-len(suffix)] if (tld != "" and lower.endswith(suffix)) else lower
|
|
787
|
+
status = php.strtolower(php.coalesce(php.get(item, "status"), ""))
|
|
788
|
+
is_available = any(php.loose_equals_string(status, s) for s in ("available", "1", "true"))
|
|
789
|
+
price = php.get(item, "price")
|
|
790
|
+
availability.append({
|
|
791
|
+
"TLD": tld,
|
|
792
|
+
"DomainName": domain_name,
|
|
793
|
+
"Status": "available" if is_available else "notavailable",
|
|
794
|
+
"Command": command,
|
|
795
|
+
"Period": php.coalesce(php.get(item, "period"), period),
|
|
796
|
+
"IsFee": php.coalesce(php.get(item, "isPremium"), False),
|
|
797
|
+
"Price": php.number_format(php.floatval(price), 4) if price is not None else None,
|
|
798
|
+
"Currency": php.get(item, "currency"),
|
|
799
|
+
"Reason": php.get(item, "reason"),
|
|
800
|
+
})
|
|
801
|
+
return availability
|
|
802
|
+
except ApiError as e:
|
|
803
|
+
return self._fail(e, "AVAILABILITY")
|
|
804
|
+
|
|
805
|
+
def get_list(self, extra_parameters=None):
|
|
806
|
+
try:
|
|
807
|
+
extra = php.to_dict(extra_parameters)
|
|
808
|
+
# Legacy paging keys (PageNumber is 0-based) -> SkipCount / MaxResultCount / Sorting.
|
|
809
|
+
if extra.get("PageSize") is not None:
|
|
810
|
+
extra["MaxResultCount"] = php.intval(extra["PageSize"])
|
|
811
|
+
if extra.get("PageNumber") is not None:
|
|
812
|
+
page_size = php.intval(php.coalesce(extra.get("PageSize"), 200))
|
|
813
|
+
extra["SkipCount"] = php.intval(extra["PageNumber"]) * page_size
|
|
814
|
+
if extra.get("OrderColumn") is not None:
|
|
815
|
+
direction = "DESC" if php.strtoupper(php.strval(php.coalesce(extra.get("OrderDirection"), "ASC"))) == "DESC" else "ASC"
|
|
816
|
+
extra["Sorting"] = php.strval(extra["OrderColumn"]) + " " + direction
|
|
817
|
+
for key in ("PageNumber", "PageSize", "OrderColumn", "OrderDirection"):
|
|
818
|
+
extra.pop(key, None)
|
|
819
|
+
|
|
820
|
+
params = {"MaxResultCount": 200, "SkipCount": 0}
|
|
821
|
+
for key, value in extra.items():
|
|
822
|
+
if isinstance(key, int):
|
|
823
|
+
params[len(params)] = value # array_merge renumbers integer keys
|
|
824
|
+
else:
|
|
825
|
+
params[key] = value
|
|
826
|
+
response = self._request("GET", "domains", params)
|
|
827
|
+
|
|
828
|
+
def domain_row(item):
|
|
829
|
+
status_code = php.get(item, "statusCode")
|
|
830
|
+
status_text = php.get(item, "statusText")
|
|
831
|
+
if status_code is not None and status_code != "" and not php.is_numeric(status_code):
|
|
832
|
+
status = php.strval(status_code)
|
|
833
|
+
elif status_text is not None and status_text != "" and not php.is_numeric(status_text):
|
|
834
|
+
status = php.strval(status_text)
|
|
835
|
+
else:
|
|
836
|
+
status = _normalize_domain_status(php.coalesce(php.get(item, "status"), ""))
|
|
837
|
+
return {
|
|
838
|
+
"ID": php.intval(php.coalesce(php.get(item, "id"), 0)),
|
|
839
|
+
"Status": status,
|
|
840
|
+
"DomainName": php.coalesce(php.get(item, "domainName"), ""),
|
|
841
|
+
"AuthCode": php.coalesce(php.get(item, "authCode"), ""),
|
|
842
|
+
"LockStatus": "true" if not php.empty(php.get(item, "lockStatus")) else "false",
|
|
843
|
+
"PrivacyProtectionStatus": "true" if not php.empty(php.get(item, "privacyProtectionStatus")) else "false",
|
|
844
|
+
"IsChildNameServer": "true" if not php.empty(php.get(item, "hosts")) else "false",
|
|
845
|
+
"Contacts": {
|
|
846
|
+
"Billing": {"ID": ""},
|
|
847
|
+
"Technical": {"ID": ""},
|
|
848
|
+
"Administrative": {"ID": ""},
|
|
849
|
+
"Registrant": {"ID": ""},
|
|
850
|
+
},
|
|
851
|
+
"Dates": {
|
|
852
|
+
"Start": _format_api_date(php.get(item, "startDate")),
|
|
853
|
+
"Expiration": _format_api_date(php.get(item, "expirationDate")),
|
|
854
|
+
"RemainingDays": php.intval(php.coalesce(php.get(item, "remainingDay"), 0)),
|
|
855
|
+
},
|
|
856
|
+
"NameServers": php.coalesce(php.get(item, "nameServers"), []),
|
|
857
|
+
"Additional": [],
|
|
858
|
+
"ChildNameServers": [],
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
items = php.get(response, "items")
|
|
862
|
+
return {
|
|
863
|
+
"data": {
|
|
864
|
+
"Domains": _map_values(items, domain_row) if items is not None else [],
|
|
865
|
+
},
|
|
866
|
+
"result": RESULT_OK,
|
|
867
|
+
"TotalCount": php.intval(php.coalesce(php.get(response, "totalCount"), 0)),
|
|
868
|
+
}
|
|
869
|
+
except ApiError as e:
|
|
870
|
+
return self._fail(e, "DOMAIN_LIST")
|
|
871
|
+
|
|
872
|
+
def get_tld_list(self, count=20):
|
|
873
|
+
try:
|
|
874
|
+
response = self._request("GET", "products/tlds", {"MaxResultCount": count, "SkipCount": 0})
|
|
875
|
+
|
|
876
|
+
tld_data = []
|
|
877
|
+
id_counter = 1
|
|
878
|
+
tld_items = php.get(response, "items")
|
|
879
|
+
if php.is_array(tld_items):
|
|
880
|
+
price_types = [
|
|
881
|
+
("register", "registration"),
|
|
882
|
+
("renew", "renew"),
|
|
883
|
+
("transfer", "transfer"),
|
|
884
|
+
("restore", "restore"),
|
|
885
|
+
("refund", "refund"),
|
|
886
|
+
("backorder", "backorder"),
|
|
887
|
+
]
|
|
888
|
+
for tld in php.values(tld_items):
|
|
889
|
+
pricing = {}
|
|
890
|
+
currencies = {}
|
|
891
|
+
|
|
892
|
+
def add_price(out_type, info):
|
|
893
|
+
period = php.intval(php.coalesce(php.get(info, "period"), 1))
|
|
894
|
+
if period < 1:
|
|
895
|
+
period = 1
|
|
896
|
+
price = php.get(info, "price")
|
|
897
|
+
pricing.setdefault(out_type, {})[period] = (
|
|
898
|
+
php.number_format(php.floatval(price), 4) if price is not None else "0.0000")
|
|
899
|
+
currencies[out_type] = php.coalesce(php.get(info, "currency"), "")
|
|
900
|
+
|
|
901
|
+
first = php.get(tld, "prices", 0)
|
|
902
|
+
if first is not None and php.is_array(first):
|
|
903
|
+
for api_type, out_type in price_types:
|
|
904
|
+
api_value = php.get(first, api_type)
|
|
905
|
+
if api_value is None:
|
|
906
|
+
continue
|
|
907
|
+
if php.is_array(api_value) and php.get(api_value, 0) is not None:
|
|
908
|
+
for price_info in php.values(api_value):
|
|
909
|
+
if php.is_array(price_info):
|
|
910
|
+
add_price(out_type, price_info)
|
|
911
|
+
elif php.is_array(api_value):
|
|
912
|
+
add_price(out_type, api_value)
|
|
913
|
+
|
|
914
|
+
# Periods in ascending numeric order, as string keys (JSON object keys).
|
|
915
|
+
pricing_out = {}
|
|
916
|
+
for out_type, periods in pricing.items():
|
|
917
|
+
pricing_out[out_type] = {str(k): periods[k] for k in sorted(periods)}
|
|
918
|
+
|
|
919
|
+
tld_data.append({
|
|
920
|
+
"id": id_counter,
|
|
921
|
+
"status": php.coalesce(php.get(tld, "status"), "Active"),
|
|
922
|
+
"maxchar": php.coalesce(php.get(tld, "constraints", "maxLenght"), 63),
|
|
923
|
+
"maxperiod": php.coalesce(php.get(tld, "maxRegistrationPeriod"), 10),
|
|
924
|
+
"minchar": php.coalesce(php.get(tld, "constraints", "minLength"), 1),
|
|
925
|
+
"minperiod": php.coalesce(php.get(tld, "minRegistrationPeriod"), 1),
|
|
926
|
+
"tld": php.get(tld, "name"),
|
|
927
|
+
"pricing": pricing_out,
|
|
928
|
+
"currencies": currencies,
|
|
929
|
+
})
|
|
930
|
+
id_counter += 1
|
|
931
|
+
|
|
932
|
+
return {"data": tld_data, "result": RESULT_OK}
|
|
933
|
+
except ApiError as e:
|
|
934
|
+
return self._fail(e, "TLD_LIST")
|
|
935
|
+
|
|
936
|
+
# ------------------------------------------------------------------
|
|
937
|
+
# Domain
|
|
938
|
+
# ------------------------------------------------------------------
|
|
939
|
+
|
|
940
|
+
def get_details(self, domain_name):
|
|
941
|
+
try:
|
|
942
|
+
response = self._request("GET", "domains/info", {"DomainName": domain_name})
|
|
943
|
+
return self._parse_domain_info(response)
|
|
944
|
+
except ApiError as e:
|
|
945
|
+
return self._fail(e, "DOMAIN_DETAILS")
|
|
946
|
+
|
|
947
|
+
def sync_from_registry(self, domain_name):
|
|
948
|
+
return self.get_details(domain_name)
|
|
949
|
+
|
|
950
|
+
def modify_name_server(self, domain_name, name_servers):
|
|
951
|
+
try:
|
|
952
|
+
payload = {"domainName": domain_name, "nameServers": normalize_host_name_list(name_servers)}
|
|
953
|
+
response = self._request("PUT", "domains/dns/name-server", payload)
|
|
954
|
+
return {
|
|
955
|
+
"result": RESULT_OK,
|
|
956
|
+
"data": {"NameServers": php.coalesce(php.get(response, "nameServers"), payload["nameServers"])},
|
|
957
|
+
}
|
|
958
|
+
except ApiError as e:
|
|
959
|
+
return self._fail(e, "MODIFY_NS")
|
|
960
|
+
|
|
961
|
+
def enable_theft_protection_lock(self, domain_name):
|
|
962
|
+
try:
|
|
963
|
+
# Locking and unlocking are separate endpoints; the body carries the domain only.
|
|
964
|
+
self._request("POST", "domains/lock", {"domainName": domain_name})
|
|
965
|
+
return {"result": RESULT_OK, "data": {"LockStatus": True}}
|
|
966
|
+
except ApiError as e:
|
|
967
|
+
return self._fail(e, "ENABLE_LOCK")
|
|
968
|
+
|
|
969
|
+
def disable_theft_protection_lock(self, domain_name):
|
|
970
|
+
try:
|
|
971
|
+
self._request("POST", "domains/unlock", {"domainName": domain_name})
|
|
972
|
+
return {"result": RESULT_OK, "data": {"LockStatus": False}}
|
|
973
|
+
except ApiError as e:
|
|
974
|
+
return self._fail(e, "DISABLE_LOCK")
|
|
975
|
+
|
|
976
|
+
def modify_privacy_protection_status(self, domain_name, status, reason="Owner request"):
|
|
977
|
+
try:
|
|
978
|
+
# ``reason`` is kept for signature compatibility only: the gateway's
|
|
979
|
+
# DomainPrivacyInput has no reason field. 1/"1"/"on"/"true"/"yes" enable privacy.
|
|
980
|
+
enabled = status if isinstance(status, bool) else php.filter_bool(status)
|
|
981
|
+
payload = {"domainName": domain_name, "privacyStatus": enabled}
|
|
982
|
+
self._request("POST", "domains/privacy", payload)
|
|
983
|
+
return {"data": {"PrivacyProtectionStatus": enabled}, "result": RESULT_OK}
|
|
984
|
+
except ApiError as e:
|
|
985
|
+
return self._fail(e, "MODIFY_PRIVACY")
|
|
986
|
+
|
|
987
|
+
def renew(self, domain_name, period):
|
|
988
|
+
try:
|
|
989
|
+
response = self._request("POST", "domains/renew", {"domainName": domain_name, "period": period})
|
|
990
|
+
failed = self._operation_failure(response, "RENEW_DOMAIN")
|
|
991
|
+
if failed:
|
|
992
|
+
return failed
|
|
993
|
+
if php.truthy(php.coalesce(php.get(response, "expirationDate"), False)):
|
|
994
|
+
return {
|
|
995
|
+
"result": RESULT_OK,
|
|
996
|
+
"data": {"ExpirationDate": _format_api_date(php.get(response, "expirationDate"))},
|
|
997
|
+
}
|
|
998
|
+
self.telemetry.send_error(ApiError("[DOMAIN_RENEW] " + DEFAULT_ERRORS["DOMAIN_RENEW"]["description"], 0))
|
|
999
|
+
return {"result": RESULT_ERROR, "error": self._err("DOMAIN_RENEW")}
|
|
1000
|
+
except ApiError as e:
|
|
1001
|
+
return self._fail(e, "RENEW_DOMAIN")
|
|
1002
|
+
|
|
1003
|
+
def register_with_contact_info(self, domain_name, period, contacts, name_servers=None,
|
|
1004
|
+
epp_lock=True, privacy_lock=False, additional_attributes=None):
|
|
1005
|
+
if name_servers is None:
|
|
1006
|
+
name_servers = []
|
|
1007
|
+
if additional_attributes is None:
|
|
1008
|
+
additional_attributes = []
|
|
1009
|
+
try:
|
|
1010
|
+
payload_contacts = []
|
|
1011
|
+
for type_, details in php.items(contacts):
|
|
1012
|
+
payload_contacts.append(self._parse_contact(details, php.ucfirst(php.strtolower(type_))))
|
|
1013
|
+
|
|
1014
|
+
# Normalise and drop empties first, then fall back to the defaults.
|
|
1015
|
+
name_servers = normalize_host_name_list(name_servers)
|
|
1016
|
+
if not name_servers:
|
|
1017
|
+
name_servers = list(DEFAULT_NAMESERVERS)
|
|
1018
|
+
|
|
1019
|
+
if php.strtolower(php.strval(domain_name))[-3:] == ".tr":
|
|
1020
|
+
additional_attributes = _normalize_tr_attributes(php.array_cast(additional_attributes))
|
|
1021
|
+
|
|
1022
|
+
# The gateway types tldAttributes as a string dictionary.
|
|
1023
|
+
additional_attributes = _stringify_attributes(php.array_cast(additional_attributes))
|
|
1024
|
+
|
|
1025
|
+
payload = {
|
|
1026
|
+
"domainName": domain_name,
|
|
1027
|
+
"period": period,
|
|
1028
|
+
"nameServers": name_servers,
|
|
1029
|
+
"isLocked": epp_lock,
|
|
1030
|
+
"privacyEnabled": privacy_lock,
|
|
1031
|
+
"contacts": payload_contacts,
|
|
1032
|
+
"tldAttributes": php.JsonObject(additional_attributes),
|
|
1033
|
+
}
|
|
1034
|
+
response = self._request("POST", "domains/register-with-contacts", payload)
|
|
1035
|
+
failed = self._operation_failure(response, "REGISTER_DOMAIN")
|
|
1036
|
+
if failed:
|
|
1037
|
+
return failed
|
|
1038
|
+
return self._parse_domain_info(response)
|
|
1039
|
+
except ApiError as e:
|
|
1040
|
+
return self._fail(e, "REGISTER_DOMAIN")
|
|
1041
|
+
|
|
1042
|
+
# ------------------------------------------------------------------
|
|
1043
|
+
# Child nameservers
|
|
1044
|
+
# ------------------------------------------------------------------
|
|
1045
|
+
|
|
1046
|
+
def add_child_name_server(self, domain_name, name_server, ip_address):
|
|
1047
|
+
try:
|
|
1048
|
+
payload = {
|
|
1049
|
+
"domainName": domain_name,
|
|
1050
|
+
"hostName": normalize_host_name(name_server),
|
|
1051
|
+
"ipAddresses": [{
|
|
1052
|
+
"ipAddress": ip_address,
|
|
1053
|
+
"ipVersion": "v4" if _is_ipv4(ip_address) else "v6",
|
|
1054
|
+
}],
|
|
1055
|
+
}
|
|
1056
|
+
self._request("POST", "domains/dns/host", payload)
|
|
1057
|
+
return {"result": RESULT_OK, "data": {"NameServer": name_server, "IPAdresses": [ip_address]}}
|
|
1058
|
+
except ApiError as e:
|
|
1059
|
+
return self._fail(e, "ADD_CHILD_NS")
|
|
1060
|
+
|
|
1061
|
+
def delete_child_name_server(self, domain_name, name_server):
|
|
1062
|
+
try:
|
|
1063
|
+
payload = {"domainName": domain_name, "hostName": normalize_host_name(name_server)}
|
|
1064
|
+
self._request("DELETE", "domains/dns/host", payload)
|
|
1065
|
+
return {"result": RESULT_OK, "data": {"NameServer": name_server}}
|
|
1066
|
+
except ApiError as e:
|
|
1067
|
+
return self._fail(e, "DELETE_CHILD_NS")
|
|
1068
|
+
|
|
1069
|
+
def modify_child_name_server(self, domain_name, name_server, ip_address):
|
|
1070
|
+
try:
|
|
1071
|
+
name_server = normalize_host_name(name_server)
|
|
1072
|
+
payload = {
|
|
1073
|
+
"domainName": domain_name,
|
|
1074
|
+
"hostName": name_server,
|
|
1075
|
+
"newHostName": name_server,
|
|
1076
|
+
"ipAddresses": [{
|
|
1077
|
+
"ipAddress": ip_address,
|
|
1078
|
+
"ipVersion": "v4" if _is_ipv4(ip_address) else "v6",
|
|
1079
|
+
}],
|
|
1080
|
+
}
|
|
1081
|
+
response = self._request("PUT", "domains/dns/host", payload)
|
|
1082
|
+
return {
|
|
1083
|
+
"result": RESULT_OK,
|
|
1084
|
+
"data": {
|
|
1085
|
+
"NameServer": php.coalesce(php.get(response, "hostName"), name_server),
|
|
1086
|
+
"IPAdresses": php.coalesce(php.get(response, "ipAddresses"), [ip_address]),
|
|
1087
|
+
},
|
|
1088
|
+
}
|
|
1089
|
+
except ApiError as e:
|
|
1090
|
+
return self._fail(e, "MODIFY_CHILD_NS")
|
|
1091
|
+
|
|
1092
|
+
# ------------------------------------------------------------------
|
|
1093
|
+
# Contacts
|
|
1094
|
+
# ------------------------------------------------------------------
|
|
1095
|
+
|
|
1096
|
+
def get_contacts(self, domain_name):
|
|
1097
|
+
try:
|
|
1098
|
+
domain_info = self._request("GET", "domains/info", {"DomainName": domain_name})
|
|
1099
|
+
# Matched case-insensitively, with the long and short spellings.
|
|
1100
|
+
type_map = {
|
|
1101
|
+
"admin": "Administrative", "administrative": "Administrative",
|
|
1102
|
+
"billing": "Billing",
|
|
1103
|
+
"tech": "Technical", "technical": "Technical",
|
|
1104
|
+
"registrant": "Registrant", "owner": "Registrant",
|
|
1105
|
+
}
|
|
1106
|
+
contacts = {}
|
|
1107
|
+
contact_items = php.get(domain_info, "contacts")
|
|
1108
|
+
if php.is_array(contact_items):
|
|
1109
|
+
for item in php.values(contact_items):
|
|
1110
|
+
if not php.is_array(item):
|
|
1111
|
+
continue
|
|
1112
|
+
api_type = php.trim(php.strval(php.coalesce(php.get(item, "contactType"), "")))
|
|
1113
|
+
key = type_map.get(php.strtolower(api_type))
|
|
1114
|
+
if key is None:
|
|
1115
|
+
key = php.ucfirst(php.strtolower(api_type))
|
|
1116
|
+
if key == "":
|
|
1117
|
+
continue
|
|
1118
|
+
merged = dict(php.to_dict(item))
|
|
1119
|
+
merged["type"] = key
|
|
1120
|
+
contacts[key] = _parse_contact_info(merged)
|
|
1121
|
+
|
|
1122
|
+
# Administrative, Billing, Registrant, Technical, then any other type (kept).
|
|
1123
|
+
ordered = {}
|
|
1124
|
+
for key in ("Administrative", "Billing", "Registrant", "Technical"):
|
|
1125
|
+
if contacts.get(key) is not None:
|
|
1126
|
+
ordered[key] = contacts.pop(key)
|
|
1127
|
+
ordered.update(contacts)
|
|
1128
|
+
return {"data": {"contacts": ordered}, "result": RESULT_OK}
|
|
1129
|
+
except ApiError as e:
|
|
1130
|
+
return self._fail(e, "GET_CONTACTS")
|
|
1131
|
+
|
|
1132
|
+
def save_contacts(self, domain_name, contacts):
|
|
1133
|
+
try:
|
|
1134
|
+
payload_contacts = []
|
|
1135
|
+
for type_, details in php.items(contacts):
|
|
1136
|
+
payload_contacts.append(self._parse_contact(details, php.ucfirst(php.strtolower(type_))))
|
|
1137
|
+
response = self._request("PUT", "domains/contacts/update",
|
|
1138
|
+
{"domainName": domain_name, "contacts": payload_contacts})
|
|
1139
|
+
parsed = {}
|
|
1140
|
+
response_contacts = php.get(response, "contacts")
|
|
1141
|
+
if php.is_array(response_contacts):
|
|
1142
|
+
for contact in php.values(response_contacts):
|
|
1143
|
+
parsed[php.ucfirst(php.strtolower(php.get(contact, "type")))] = _parse_contact_info(contact)
|
|
1144
|
+
return {"result": RESULT_OK, "data": {"contacts": parsed}}
|
|
1145
|
+
except ApiError as e:
|
|
1146
|
+
return self._fail(e, "SAVE_CONTACTS")
|
|
1147
|
+
|
|
1148
|
+
# ------------------------------------------------------------------
|
|
1149
|
+
# Transfers
|
|
1150
|
+
# ------------------------------------------------------------------
|
|
1151
|
+
|
|
1152
|
+
def transfer(self, domain_name, epp_code, period):
|
|
1153
|
+
"""Start an incoming transfer. Contacts are never sent: the registrar handles them."""
|
|
1154
|
+
try:
|
|
1155
|
+
payload = {
|
|
1156
|
+
"domainName": domain_name,
|
|
1157
|
+
"authCode": epp_code,
|
|
1158
|
+
"period": period,
|
|
1159
|
+
"contacts": [],
|
|
1160
|
+
}
|
|
1161
|
+
response = self._request("POST", "domains/transfer", payload)
|
|
1162
|
+
failed = self._operation_failure(response, "TRANSFER_DOMAIN")
|
|
1163
|
+
if failed:
|
|
1164
|
+
return failed
|
|
1165
|
+
return self._parse_domain_info(response)
|
|
1166
|
+
except ApiError as e:
|
|
1167
|
+
return self._fail(e, "TRANSFER_DOMAIN")
|
|
1168
|
+
|
|
1169
|
+
def _transfer_action(self, domain_name, action, default_status, default_code):
|
|
1170
|
+
try:
|
|
1171
|
+
response = self._request("POST", "domains/transfers/" + action, {"domainName": domain_name})
|
|
1172
|
+
return {
|
|
1173
|
+
"result": RESULT_OK,
|
|
1174
|
+
"data": {
|
|
1175
|
+
"DomainName": domain_name,
|
|
1176
|
+
"Status": php.coalesce(php.get(response, "status"), default_status),
|
|
1177
|
+
},
|
|
1178
|
+
}
|
|
1179
|
+
except ApiError as e:
|
|
1180
|
+
return self._fail(e, default_code)
|
|
1181
|
+
|
|
1182
|
+
def cancel_transfer(self, domain_name):
|
|
1183
|
+
return self._transfer_action(domain_name, "cancel", "Cancelled", "CANCEL_TRANSFER")
|
|
1184
|
+
|
|
1185
|
+
def approve_transfer(self, domain_name):
|
|
1186
|
+
return self._transfer_action(domain_name, "approve", "Approved", "APPROVE_TRANSFER")
|
|
1187
|
+
|
|
1188
|
+
def reject_transfer(self, domain_name):
|
|
1189
|
+
return self._transfer_action(domain_name, "reject", "Rejected", "REJECT_TRANSFER")
|
|
1190
|
+
|
|
1191
|
+
def check_transfer(self, domain_name, auth_code):
|
|
1192
|
+
try:
|
|
1193
|
+
response = self._request("POST", "domains/transfers/check",
|
|
1194
|
+
{"domainName": domain_name, "authCode": auth_code})
|
|
1195
|
+
g = lambda k: php.get(response, k) # noqa: E731
|
|
1196
|
+
available = not php.empty(g("transferAvailabilityStatus"))
|
|
1197
|
+
envelope = {
|
|
1198
|
+
"result": RESULT_OK if available else RESULT_ERROR,
|
|
1199
|
+
"data": {
|
|
1200
|
+
"TransferAvailabilityStatus": available,
|
|
1201
|
+
"AuthCodeIsRequired": not php.empty(g("authCodeIsRequired")),
|
|
1202
|
+
"AuthCodeIsValid": not php.empty(g("authCodeIsValid")),
|
|
1203
|
+
"UserTransferRequired": not php.empty(g("userTransferRequired")),
|
|
1204
|
+
"TransferLock": not php.empty(g("transferLock")),
|
|
1205
|
+
"Message": php.strval(php.coalesce(g("message"), "")),
|
|
1206
|
+
"MessageKey": php.strval(php.coalesce(g("messageKey"), "")),
|
|
1207
|
+
"Additional": php.array_cast(g("additionalAttributes")) if g("additionalAttributes") is not None else [],
|
|
1208
|
+
},
|
|
1209
|
+
}
|
|
1210
|
+
if not available:
|
|
1211
|
+
envelope["error"] = self._err(
|
|
1212
|
+
"TRANSFER_NOT_AVAILABLE",
|
|
1213
|
+
php.coalesce(g("message"), "Domain is not transferable"),
|
|
1214
|
+
php.coalesce(g("messageKey"), g("message"), ""),
|
|
1215
|
+
)
|
|
1216
|
+
return envelope
|
|
1217
|
+
except ApiError as e:
|
|
1218
|
+
return self._fail(e, "CHECK_TRANSFER")
|
|
1219
|
+
|
|
1220
|
+
# ------------------------------------------------------------------
|
|
1221
|
+
# DNS zone
|
|
1222
|
+
# ------------------------------------------------------------------
|
|
1223
|
+
|
|
1224
|
+
def get_resource_records(self, domain_name):
|
|
1225
|
+
try:
|
|
1226
|
+
response = self._request("GET", "domains/zones", {"domainName": domain_name})
|
|
1227
|
+
records = []
|
|
1228
|
+
for rset in php.values(php.array_cast(response)):
|
|
1229
|
+
if not php.is_array(rset):
|
|
1230
|
+
continue
|
|
1231
|
+
name = php.coalesce(php.get(rset, "name"), "")
|
|
1232
|
+
type_ = php.coalesce(php.get(rset, "type"), "")
|
|
1233
|
+
ttl = php.intval(php.coalesce(php.get(rset, "ttl"), 0))
|
|
1234
|
+
rows = php.coalesce(php.get(rset, "records"), [])
|
|
1235
|
+
if not php.is_array(rows) or not rows:
|
|
1236
|
+
records.append({"Name": name, "Type": type_, "TTL": ttl, "Content": "", "Disabled": False})
|
|
1237
|
+
continue
|
|
1238
|
+
for rec in php.values(rows):
|
|
1239
|
+
records.append({
|
|
1240
|
+
"Name": name,
|
|
1241
|
+
"Type": type_,
|
|
1242
|
+
"TTL": ttl,
|
|
1243
|
+
"Content": php.coalesce(php.get(rec, "content"), ""),
|
|
1244
|
+
"Disabled": not php.empty(php.get(rec, "disabled")),
|
|
1245
|
+
})
|
|
1246
|
+
return {"result": RESULT_OK, "data": {"records": records}}
|
|
1247
|
+
except ApiError as e:
|
|
1248
|
+
return self._fail(e, "GET_ZONE")
|
|
1249
|
+
|
|
1250
|
+
def add_resource_record(self, domain_name, name, type_, content, ttl=3600, apply=True):
|
|
1251
|
+
try:
|
|
1252
|
+
why = _unsupported_zone_type(type_)
|
|
1253
|
+
if why:
|
|
1254
|
+
return {"result": RESULT_ERROR, "error": self._err("ADD_ZONE", why, why)}
|
|
1255
|
+
name = _relativize_record_name(name, domain_name)
|
|
1256
|
+
body = {"zoneStruct": _build_zone_struct(name, type_, content, ttl)}
|
|
1257
|
+
endpoint = "domains/zones?" + php.http_build_query({"domainName": domain_name})
|
|
1258
|
+
self._request("POST", endpoint, body)
|
|
1259
|
+
if php.truthy(apply):
|
|
1260
|
+
# A failed apply leaves the change staged but not live: report it.
|
|
1261
|
+
applied = self.apply_resource_records(domain_name)
|
|
1262
|
+
if applied.get("result") != RESULT_OK:
|
|
1263
|
+
return applied
|
|
1264
|
+
return {
|
|
1265
|
+
"result": RESULT_OK,
|
|
1266
|
+
"data": {"Name": name, "Type": body["zoneStruct"]["type"], "TTL": body["zoneStruct"]["ttl"],
|
|
1267
|
+
"Content": content},
|
|
1268
|
+
}
|
|
1269
|
+
except ApiError as e:
|
|
1270
|
+
return self._fail(e, "ADD_ZONE")
|
|
1271
|
+
|
|
1272
|
+
def edit_resource_record(self, domain_name, record_name, name, type_, content, ttl=3600, apply=True):
|
|
1273
|
+
try:
|
|
1274
|
+
why = _unsupported_zone_type(type_)
|
|
1275
|
+
if why:
|
|
1276
|
+
return {"result": RESULT_ERROR, "error": self._err("EDIT_ZONE", why, why)}
|
|
1277
|
+
name = _relativize_record_name(name, domain_name)
|
|
1278
|
+
body = {"zoneStruct": _build_zone_struct(name, type_, content, ttl)}
|
|
1279
|
+
endpoint = "domains/zones?" + php.http_build_query({"domainName": domain_name, "recordName": record_name})
|
|
1280
|
+
self._request("PUT", endpoint, body)
|
|
1281
|
+
if php.truthy(apply):
|
|
1282
|
+
# A failed apply leaves the change staged but not live: report it.
|
|
1283
|
+
applied = self.apply_resource_records(domain_name)
|
|
1284
|
+
if applied.get("result") != RESULT_OK:
|
|
1285
|
+
return applied
|
|
1286
|
+
return {
|
|
1287
|
+
"result": RESULT_OK,
|
|
1288
|
+
"data": {"Name": name, "Type": body["zoneStruct"]["type"], "TTL": body["zoneStruct"]["ttl"],
|
|
1289
|
+
"Content": content},
|
|
1290
|
+
}
|
|
1291
|
+
except ApiError as e:
|
|
1292
|
+
return self._fail(e, "EDIT_ZONE")
|
|
1293
|
+
|
|
1294
|
+
def delete_resource_record(self, domain_name, name, record, record_type, apply=True):
|
|
1295
|
+
try:
|
|
1296
|
+
self._request("DELETE", "domains/zones", {
|
|
1297
|
+
"domainName": domain_name,
|
|
1298
|
+
"Name": name,
|
|
1299
|
+
"Record": record,
|
|
1300
|
+
"RecordType": record_type,
|
|
1301
|
+
})
|
|
1302
|
+
if php.truthy(apply):
|
|
1303
|
+
# A failed apply leaves the change staged but not live: report it.
|
|
1304
|
+
applied = self.apply_resource_records(domain_name)
|
|
1305
|
+
if applied.get("result") != RESULT_OK:
|
|
1306
|
+
return applied
|
|
1307
|
+
return {"result": RESULT_OK, "data": {"Name": name, "Type": record_type, "Content": record}}
|
|
1308
|
+
except ApiError as e:
|
|
1309
|
+
return self._fail(e, "DELETE_ZONE")
|
|
1310
|
+
|
|
1311
|
+
def apply_resource_records(self, domain_name):
|
|
1312
|
+
try:
|
|
1313
|
+
endpoint = "domains/zones/apply?" + php.http_build_query({"domainName": domain_name})
|
|
1314
|
+
self._request("POST", endpoint, {})
|
|
1315
|
+
return {"result": RESULT_OK, "data": {"DomainName": domain_name}}
|
|
1316
|
+
except ApiError as e:
|
|
1317
|
+
return self._fail(e, "APPLY_ZONE")
|
|
1318
|
+
|
|
1319
|
+
def set_resource_record_set(self, domain_name, name, type_, contents, ttl=3600, apply=True):
|
|
1320
|
+
if not isinstance(contents, (list, tuple, dict)):
|
|
1321
|
+
raise TypeError("contents must be a list of record values")
|
|
1322
|
+
try:
|
|
1323
|
+
why = _unsupported_zone_type(type_)
|
|
1324
|
+
if why:
|
|
1325
|
+
return {"result": RESULT_ERROR, "error": self._err("SET_ZONE", why, why)}
|
|
1326
|
+
name = _relativize_record_name(name, domain_name)
|
|
1327
|
+
contents = [v for v in (php.strval(x) for x in php.values(contents)) if php.trim(v) != ""]
|
|
1328
|
+
if not contents:
|
|
1329
|
+
return self.delete_resource_record_set(domain_name, name, type_, apply)
|
|
1330
|
+
|
|
1331
|
+
# POST creates the set when absent and overwrites every value when present.
|
|
1332
|
+
body = {"zoneStruct": _build_zone_struct(name, type_, contents, ttl)}
|
|
1333
|
+
endpoint = "domains/zones?" + php.http_build_query({"domainName": domain_name})
|
|
1334
|
+
self._request("POST", endpoint, body)
|
|
1335
|
+
if php.truthy(apply):
|
|
1336
|
+
# A failed apply leaves the change staged but not live: report it.
|
|
1337
|
+
applied = self.apply_resource_records(domain_name)
|
|
1338
|
+
if applied.get("result") != RESULT_OK:
|
|
1339
|
+
return applied
|
|
1340
|
+
return {
|
|
1341
|
+
"result": RESULT_OK,
|
|
1342
|
+
"data": {"Name": name, "Type": body["zoneStruct"]["type"], "TTL": body["zoneStruct"]["ttl"],
|
|
1343
|
+
"Contents": contents},
|
|
1344
|
+
}
|
|
1345
|
+
except ApiError as e:
|
|
1346
|
+
return self._fail(e, "SET_ZONE")
|
|
1347
|
+
|
|
1348
|
+
def delete_resource_record_set(self, domain_name, name, type_, apply=True, known_value=""):
|
|
1349
|
+
try:
|
|
1350
|
+
type_ = php.strtoupper(php.trim(type_))
|
|
1351
|
+
value = php.strval(known_value)
|
|
1352
|
+
if value == "":
|
|
1353
|
+
current = self.get_resource_records(domain_name)
|
|
1354
|
+
if php.coalesce(current.get("result"), "") != RESULT_OK:
|
|
1355
|
+
return current
|
|
1356
|
+
fqdn = _qualify_record_name(name, domain_name)
|
|
1357
|
+
for rec in current["data"]["records"]:
|
|
1358
|
+
if php.strcasecmp_equal(rec["Name"], fqdn) and php.strcasecmp_equal(rec["Type"], type_):
|
|
1359
|
+
value = php.strval(rec["Content"])
|
|
1360
|
+
break
|
|
1361
|
+
if value == "":
|
|
1362
|
+
# Nothing to remove: success, so callers stay idempotent.
|
|
1363
|
+
return {"result": RESULT_OK, "data": {"Name": name, "Type": type_, "Removed": False}}
|
|
1364
|
+
return self.delete_resource_record(domain_name, _qualify_record_name(name, domain_name), value, type_, apply)
|
|
1365
|
+
except ApiError as e:
|
|
1366
|
+
return self._fail(e, "DELETE_ZONE")
|
|
1367
|
+
|
|
1368
|
+
def _read_resource_record_set(self, domain_name, name, type_):
|
|
1369
|
+
current = self.get_resource_records(domain_name)
|
|
1370
|
+
if php.coalesce(current.get("result"), "") != RESULT_OK:
|
|
1371
|
+
return current
|
|
1372
|
+
fqdn = _qualify_record_name(name, domain_name)
|
|
1373
|
+
type_ = php.strtoupper(php.trim(type_))
|
|
1374
|
+
contents = []
|
|
1375
|
+
ttl = 0
|
|
1376
|
+
for rec in current["data"]["records"]:
|
|
1377
|
+
if not php.strcasecmp_equal(rec["Name"], fqdn) or not php.strcasecmp_equal(rec["Type"], type_):
|
|
1378
|
+
continue
|
|
1379
|
+
if php.strval(rec["Content"]) != "":
|
|
1380
|
+
contents.append(php.strval(rec["Content"]))
|
|
1381
|
+
ttl = php.intval(rec["TTL"])
|
|
1382
|
+
return {"ttl": ttl, "contents": contents}
|
|
1383
|
+
|
|
1384
|
+
def add_resource_record_value(self, domain_name, name, type_, value, ttl=None, apply=True):
|
|
1385
|
+
try:
|
|
1386
|
+
why = _unsupported_zone_type(type_)
|
|
1387
|
+
if why:
|
|
1388
|
+
return {"result": RESULT_ERROR, "error": self._err("ADD_ZONE", why, why)}
|
|
1389
|
+
name = _relativize_record_name(name, domain_name)
|
|
1390
|
+
rset = self._read_resource_record_set(domain_name, name, type_)
|
|
1391
|
+
if "result" in rset:
|
|
1392
|
+
return rset
|
|
1393
|
+
for existing in rset["contents"]:
|
|
1394
|
+
if _same_record_value(type_, existing, value):
|
|
1395
|
+
return {
|
|
1396
|
+
"result": RESULT_OK,
|
|
1397
|
+
"data": {"Name": name, "Type": php.strtoupper(type_), "Contents": rset["contents"], "Changed": False},
|
|
1398
|
+
}
|
|
1399
|
+
contents = list(rset["contents"])
|
|
1400
|
+
contents.append(php.strval(value))
|
|
1401
|
+
return self.set_resource_record_set(domain_name, name, type_, contents,
|
|
1402
|
+
ttl if ttl is not None else (rset["ttl"] or 3600), apply)
|
|
1403
|
+
except ApiError as e:
|
|
1404
|
+
return self._fail(e, "ADD_ZONE")
|
|
1405
|
+
|
|
1406
|
+
def remove_resource_record_value(self, domain_name, name, type_, value, apply=True):
|
|
1407
|
+
try:
|
|
1408
|
+
why = _unsupported_zone_type(type_)
|
|
1409
|
+
if why:
|
|
1410
|
+
return {"result": RESULT_ERROR, "error": self._err("DELETE_ZONE", why, why)}
|
|
1411
|
+
name = _relativize_record_name(name, domain_name)
|
|
1412
|
+
rset = self._read_resource_record_set(domain_name, name, type_)
|
|
1413
|
+
if "result" in rset:
|
|
1414
|
+
return rset
|
|
1415
|
+
remaining = []
|
|
1416
|
+
found = False
|
|
1417
|
+
for existing in rset["contents"]:
|
|
1418
|
+
if not found and _same_record_value(type_, existing, value):
|
|
1419
|
+
found = True
|
|
1420
|
+
continue
|
|
1421
|
+
remaining.append(existing)
|
|
1422
|
+
if not found:
|
|
1423
|
+
return {
|
|
1424
|
+
"result": RESULT_OK,
|
|
1425
|
+
"data": {"Name": name, "Type": php.strtoupper(type_), "Contents": rset["contents"], "Changed": False},
|
|
1426
|
+
}
|
|
1427
|
+
if not remaining:
|
|
1428
|
+
return self.delete_resource_record_set(domain_name, name, type_, apply, php.strval(value))
|
|
1429
|
+
return self.set_resource_record_set(domain_name, name, type_, remaining, rset["ttl"] or 3600, apply)
|
|
1430
|
+
except ApiError as e:
|
|
1431
|
+
return self._fail(e, "DELETE_ZONE")
|
|
1432
|
+
|
|
1433
|
+
def replace_resource_record_value(self, domain_name, name, type_, old_value, new_value, ttl=None, apply=True):
|
|
1434
|
+
try:
|
|
1435
|
+
why = _unsupported_zone_type(type_)
|
|
1436
|
+
if why:
|
|
1437
|
+
return {"result": RESULT_ERROR, "error": self._err("EDIT_ZONE", why, why)}
|
|
1438
|
+
name = _relativize_record_name(name, domain_name)
|
|
1439
|
+
rset = self._read_resource_record_set(domain_name, name, type_)
|
|
1440
|
+
if "result" in rset:
|
|
1441
|
+
return rset
|
|
1442
|
+
contents = []
|
|
1443
|
+
found = False
|
|
1444
|
+
for existing in rset["contents"]:
|
|
1445
|
+
if not found and _same_record_value(type_, existing, old_value):
|
|
1446
|
+
found = True
|
|
1447
|
+
contents.append(php.strval(new_value))
|
|
1448
|
+
continue
|
|
1449
|
+
contents.append(existing)
|
|
1450
|
+
if not found:
|
|
1451
|
+
why = 'No %s record "%s" with value "%s" to edit.' % (php.strtoupper(type_), name, php.strval(old_value))
|
|
1452
|
+
return {"result": RESULT_ERROR, "error": self._err("EDIT_ZONE", why, why)}
|
|
1453
|
+
return self.set_resource_record_set(domain_name, name, type_, contents,
|
|
1454
|
+
ttl if ttl is not None else (rset["ttl"] or 3600), apply)
|
|
1455
|
+
except ApiError as e:
|
|
1456
|
+
return self._fail(e, "EDIT_ZONE")
|
|
1457
|
+
|
|
1458
|
+
# ------------------------------------------------------------------
|
|
1459
|
+
# Forwarding
|
|
1460
|
+
# ------------------------------------------------------------------
|
|
1461
|
+
|
|
1462
|
+
def get_forwarding(self, domain_name):
|
|
1463
|
+
try:
|
|
1464
|
+
response = self._request("GET", "domains/forwards", {"domainName": domain_name})
|
|
1465
|
+
alias = php.get(response, "redirectAlias")
|
|
1466
|
+
return {
|
|
1467
|
+
"result": RESULT_OK,
|
|
1468
|
+
"data": {
|
|
1469
|
+
"Enabled": not php.empty(alias),
|
|
1470
|
+
"RedirectAddress": php.strval(php.coalesce(alias, "")),
|
|
1471
|
+
"ForwardType": php.strval(php.coalesce(php.get(response, "redirectType"), "")),
|
|
1472
|
+
},
|
|
1473
|
+
}
|
|
1474
|
+
except ApiError as e:
|
|
1475
|
+
raw = php.coalesce(php.get(self.last_response, "raw_response"), e.message)
|
|
1476
|
+
# Only the gateway's "no forwarding configured" answer means disabled.
|
|
1477
|
+
if self._is_forward_not_set(e):
|
|
1478
|
+
return {"result": RESULT_OK, "data": {"Enabled": False, "RedirectAddress": "", "ForwardType": ""}}
|
|
1479
|
+
details = php.coalesce(php.get(self.last_parsed_response, "Details"), raw)
|
|
1480
|
+
return {
|
|
1481
|
+
"result": RESULT_ERROR,
|
|
1482
|
+
"error": self._err(self._code(e) or "GET_FORWARD", e.message, details),
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
def set_forwarding(self, domain_name, redirect_address, forward_type="Standard"):
|
|
1486
|
+
try:
|
|
1487
|
+
type_ = php.ucfirst(php.strtolower(php.trim(forward_type)))
|
|
1488
|
+
if type_ not in FORWARD_TYPES:
|
|
1489
|
+
why = 'Forward type "%s" is not supported. Supported types: %s.' % (
|
|
1490
|
+
php.strval(forward_type), ", ".join(FORWARD_TYPES))
|
|
1491
|
+
return {"result": RESULT_ERROR, "error": self._err("SET_FORWARD", why, why)}
|
|
1492
|
+
if php.trim(redirect_address) == "":
|
|
1493
|
+
why = "Redirect address is required."
|
|
1494
|
+
return {"result": RESULT_ERROR, "error": self._err("SET_FORWARD", why, why)}
|
|
1495
|
+
self._request("POST", "domains/forwards", {
|
|
1496
|
+
"domainName": domain_name,
|
|
1497
|
+
"redirectAddress": redirect_address,
|
|
1498
|
+
"forwardType": type_,
|
|
1499
|
+
})
|
|
1500
|
+
return {"result": RESULT_OK, "data": {"RedirectAddress": redirect_address, "ForwardType": type_}}
|
|
1501
|
+
except ApiError as e:
|
|
1502
|
+
return self._fail(e, "SET_FORWARD")
|
|
1503
|
+
|
|
1504
|
+
def delete_forwarding(self, domain_name):
|
|
1505
|
+
try:
|
|
1506
|
+
# PascalCase query key, unlike the GET.
|
|
1507
|
+
self._request("DELETE", "domains/forwards", {"DomainName": domain_name})
|
|
1508
|
+
return {"result": RESULT_OK, "data": {"DomainName": domain_name, "Removed": True}}
|
|
1509
|
+
except ApiError as e:
|
|
1510
|
+
# Nothing configured: deleting is a no-op.
|
|
1511
|
+
if self._is_forward_not_set(e):
|
|
1512
|
+
return {"result": RESULT_OK, "data": {"DomainName": domain_name, "Removed": False}}
|
|
1513
|
+
return self._fail(e, "DELETE_FORWARD")
|
|
1514
|
+
|
|
1515
|
+
def _is_forward_not_set(self, exc):
|
|
1516
|
+
"""The gateway answers "Redirect information not found" when no forwarding is set."""
|
|
1517
|
+
needle = "redirect information not found"
|
|
1518
|
+
raw = php.get(self.last_response, "raw_response")
|
|
1519
|
+
try:
|
|
1520
|
+
encoded = php.json_encode(self.last_response if self.last_response is not None else [])
|
|
1521
|
+
except (TypeError, ValueError):
|
|
1522
|
+
encoded = ""
|
|
1523
|
+
return (php.stripos_found(exc.message, needle)
|
|
1524
|
+
or php.stripos_found(php.strval(raw), needle)
|
|
1525
|
+
or php.stripos_found(encoded, needle))
|
|
1526
|
+
|
|
1527
|
+
def _operation_failure(self, response, code):
|
|
1528
|
+
"""register/transfer/renew may answer 200 with ``success: false``: that is a refusal."""
|
|
1529
|
+
if not isinstance(response, dict) or "success" not in response or response["success"] is not False:
|
|
1530
|
+
return None
|
|
1531
|
+
message = php.trim(php.strval(php.coalesce(response.get("message"), "")))
|
|
1532
|
+
if message == "":
|
|
1533
|
+
message = "The registry did not accept the request."
|
|
1534
|
+
return {"result": RESULT_ERROR, "error": self._err(code, message, message)}
|
|
1535
|
+
|
|
1536
|
+
# ------------------------------------------------------------------
|
|
1537
|
+
# Utilities (no network)
|
|
1538
|
+
# ------------------------------------------------------------------
|
|
1539
|
+
|
|
1540
|
+
def is_tr_tld(self, domain):
|
|
1541
|
+
tail = php.strval(domain).encode("utf-8")[-3:]
|
|
1542
|
+
return tail.translate(bytes.maketrans(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ", b"abcdefghijklmnopqrstuvwxyz")) == b".tr"
|
|
1543
|
+
|
|
1544
|
+
def validate_contact(self, contact):
|
|
1545
|
+
"""Complete and normalise a contact the way the gateway requires it."""
|
|
1546
|
+
defaults = {
|
|
1547
|
+
"FirstName": "Isimyok",
|
|
1548
|
+
"LastName": "Isimyok",
|
|
1549
|
+
"AddressLine1": "Addres yok",
|
|
1550
|
+
"City": "ISTANBUL",
|
|
1551
|
+
"Country": "TR",
|
|
1552
|
+
"ZipCode": "34000",
|
|
1553
|
+
"Phone": "5555555555",
|
|
1554
|
+
"PhoneCountryCode": "90",
|
|
1555
|
+
}
|
|
1556
|
+
contact = php.to_dict(contact)
|
|
1557
|
+
for key, value in defaults.items():
|
|
1558
|
+
if contact.get(key) is None:
|
|
1559
|
+
contact[key] = value
|
|
1560
|
+
# No State default: a missing state falls back to the city below.
|
|
1561
|
+
if contact.get("State") is None:
|
|
1562
|
+
contact["State"] = ""
|
|
1563
|
+
|
|
1564
|
+
contact["FirstName"] = _sanitize_person_name(contact["FirstName"])
|
|
1565
|
+
contact["LastName"] = _sanitize_person_name(contact["LastName"])
|
|
1566
|
+
|
|
1567
|
+
if php.strlen(php.trim(contact["FirstName"])) == 0:
|
|
1568
|
+
contact["FirstName"] = defaults["FirstName"]
|
|
1569
|
+
if php.strlen(php.trim(contact["LastName"])) == 0:
|
|
1570
|
+
contact["LastName"] = contact["FirstName"]
|
|
1571
|
+
if php.strlen(php.trim(contact["AddressLine1"])) == 0:
|
|
1572
|
+
contact["AddressLine1"] = defaults["AddressLine1"]
|
|
1573
|
+
if php.strlen(php.trim(contact["City"])) == 0:
|
|
1574
|
+
contact["City"] = defaults["City"]
|
|
1575
|
+
if php.strlen(php.trim(contact["State"])) == 0:
|
|
1576
|
+
contact["State"] = contact["City"]
|
|
1577
|
+
contact["Country"] = _normalize_country_code(contact["Country"])
|
|
1578
|
+
if php.strlen(php.trim(contact["Country"])) == 0:
|
|
1579
|
+
contact["Country"] = defaults["Country"]
|
|
1580
|
+
if php.strlen(php.trim(contact["ZipCode"])) == 0:
|
|
1581
|
+
contact["ZipCode"] = defaults["ZipCode"]
|
|
1582
|
+
|
|
1583
|
+
phone, phone_cc = _normalize_phone(contact["Phone"], contact["PhoneCountryCode"])
|
|
1584
|
+
contact["Phone"] = phone
|
|
1585
|
+
contact["PhoneCountryCode"] = phone_cc
|
|
1586
|
+
if php.strlen(php.trim(contact["PhoneCountryCode"])) == 0:
|
|
1587
|
+
contact["PhoneCountryCode"] = defaults["PhoneCountryCode"]
|
|
1588
|
+
if php.strlen(php.trim(contact["Phone"])) == 0:
|
|
1589
|
+
contact["Phone"] = defaults["Phone"]
|
|
1590
|
+
|
|
1591
|
+
# Fax is optional: normalise when present, never invent one.
|
|
1592
|
+
fax, fax_cc = _normalize_phone(php.coalesce(contact.get("Fax"), ""), php.coalesce(contact.get("FaxCountryCode"), ""))
|
|
1593
|
+
contact["Fax"] = fax
|
|
1594
|
+
contact["FaxCountryCode"] = "" if fax == "" else fax_cc
|
|
1595
|
+
return contact
|
|
1596
|
+
|
|
1597
|
+
# ------------------------------------------------------------------
|
|
1598
|
+
# Parsing helpers
|
|
1599
|
+
# ------------------------------------------------------------------
|
|
1600
|
+
|
|
1601
|
+
def _parse_domain_info(self, data):
|
|
1602
|
+
if php.empty(data):
|
|
1603
|
+
return {"result": RESULT_ERROR, "error": self._err("RESPONSE")}
|
|
1604
|
+
g = lambda *k: php.get(data, *k) # noqa: E731
|
|
1605
|
+
|
|
1606
|
+
def child(ns):
|
|
1607
|
+
ips = [php.get(ip, "ipAddress") for ip in php.values(php.coalesce(php.get(ns, "ipAddresses"), []))]
|
|
1608
|
+
return {"ns": php.get(ns, "name"), "ip": ips[-1] if ips else ""}
|
|
1609
|
+
|
|
1610
|
+
remaining = g("remainingDay")
|
|
1611
|
+
return {
|
|
1612
|
+
"data": {
|
|
1613
|
+
"ID": php.intval(php.coalesce(g("id"), 0)),
|
|
1614
|
+
"Status": _normalize_domain_status(php.coalesce(g("status"), "")),
|
|
1615
|
+
"DomainName": php.strval(php.coalesce(g("domainName"), g("name"), "")),
|
|
1616
|
+
"AuthCode": php.strval(php.coalesce(g("authCode"), g("eppCode"), "")),
|
|
1617
|
+
"LockStatus": "true" if not php.empty(g("lockStatus")) else "false",
|
|
1618
|
+
"PrivacyProtectionStatus": "true" if not php.empty(g("privacyProtectionStatus")) else "false",
|
|
1619
|
+
"IsChildNameServer": "true" if not php.empty(g("hosts")) else "false",
|
|
1620
|
+
"Contacts": _parse_contact_ids(php.coalesce(g("contacts"), [])),
|
|
1621
|
+
"Dates": {
|
|
1622
|
+
"Start": _format_api_date(g("startDate")),
|
|
1623
|
+
"Expiration": _format_api_date(g("expirationDate")),
|
|
1624
|
+
"RemainingDays": php.intval(remaining) if remaining is not None
|
|
1625
|
+
else _remaining_days_from_date(g("expirationDate")),
|
|
1626
|
+
},
|
|
1627
|
+
"NameServers": _map_values(g("nameservers"), php.strval) if g("nameservers") is not None else [],
|
|
1628
|
+
"Additional": php.array_cast(g("additionalAttributes")) if g("additionalAttributes") is not None else [],
|
|
1629
|
+
"ChildNameServers": _map_values(g("hosts"), child) if g("hosts") is not None else [],
|
|
1630
|
+
},
|
|
1631
|
+
"result": RESULT_OK,
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
def _parse_contact(self, contact, type_):
|
|
1635
|
+
"""Module contact (flat or nested shape) -> gateway ContactLiteDto."""
|
|
1636
|
+
type_map = {"Administrative": "Admin", "Technical": "Tech", "Billing": "Billing", "Registrant": "Registrant"}
|
|
1637
|
+
api_type = type_map.get(type_, type_)
|
|
1638
|
+
g = lambda *k: php.get(contact, *k) # noqa: E731
|
|
1639
|
+
|
|
1640
|
+
address = php.coalesce(g("AddressLine1"), g("Address", "Line1"), "")
|
|
1641
|
+
line2 = php.coalesce(g("AddressLine2"), g("Address", "Line2"), "")
|
|
1642
|
+
if not php.empty(line2):
|
|
1643
|
+
address = php.strval(address) + " " + php.strval(line2)
|
|
1644
|
+
|
|
1645
|
+
phone = php.coalesce(g("Phone"), g("Phone", "Phone", "Number"), "")
|
|
1646
|
+
phone_cc = php.coalesce(g("PhoneCountryCode"), g("Phone", "Phone", "CountryCode"), "")
|
|
1647
|
+
fax = php.coalesce(g("Fax"), g("Phone", "Fax", "Number"), "")
|
|
1648
|
+
fax_cc = php.coalesce(g("FaxCountryCode"), g("Phone", "Fax", "CountryCode"), "")
|
|
1649
|
+
|
|
1650
|
+
# Nested shape: Phone => ['Phone' => [...], 'Fax' => [...]].
|
|
1651
|
+
if php.is_array(phone):
|
|
1652
|
+
phone_cc = php.coalesce(php.get(phone, "Phone", "CountryCode"), "")
|
|
1653
|
+
phone = php.coalesce(php.get(phone, "Phone", "Number"), "")
|
|
1654
|
+
if php.is_array(fax):
|
|
1655
|
+
fax_cc = php.coalesce(php.get(fax, "Fax", "CountryCode"), "")
|
|
1656
|
+
fax = php.coalesce(php.get(fax, "Fax", "Number"), "")
|
|
1657
|
+
|
|
1658
|
+
validated = self.validate_contact({
|
|
1659
|
+
"FirstName": php.coalesce(g("FirstName"), ""),
|
|
1660
|
+
"LastName": php.coalesce(g("LastName"), ""),
|
|
1661
|
+
"AddressLine1": address,
|
|
1662
|
+
"City": php.coalesce(g("City"), g("Address", "City"), ""),
|
|
1663
|
+
"State": php.coalesce(g("State"), g("Address", "State"), ""),
|
|
1664
|
+
"Country": php.coalesce(g("Country"), g("Address", "Country"), ""),
|
|
1665
|
+
"ZipCode": php.coalesce(g("ZipCode"), g("Address", "ZipCode"), ""),
|
|
1666
|
+
"Phone": phone,
|
|
1667
|
+
"PhoneCountryCode": phone_cc,
|
|
1668
|
+
"Fax": fax,
|
|
1669
|
+
"FaxCountryCode": fax_cc,
|
|
1670
|
+
})
|
|
1671
|
+
|
|
1672
|
+
return {
|
|
1673
|
+
"contactType": api_type,
|
|
1674
|
+
"firstName": validated["FirstName"],
|
|
1675
|
+
"lastName": validated["LastName"],
|
|
1676
|
+
"companyName": php.coalesce(g("Company"), ""),
|
|
1677
|
+
"eMail": php.coalesce(g("EMail"), ""),
|
|
1678
|
+
"address": validated["AddressLine1"],
|
|
1679
|
+
"city": validated["City"],
|
|
1680
|
+
"state": validated["State"],
|
|
1681
|
+
"country": validated["Country"],
|
|
1682
|
+
"postalCode": validated["ZipCode"],
|
|
1683
|
+
"phoneCountryCode": validated["PhoneCountryCode"],
|
|
1684
|
+
"phone": validated["Phone"],
|
|
1685
|
+
"faxCountryCode": validated["FaxCountryCode"],
|
|
1686
|
+
"fax": validated["Fax"],
|
|
1687
|
+
# Required non-nullable bool in the gateway ContactLiteDto.
|
|
1688
|
+
"isHidden": php.to_bool(php.coalesce(g("IsHidden"), False)),
|
|
1689
|
+
}
|