352 lines
14 KiB
Python
352 lines
14 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2020 Google LLC. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""Contacts utilties for Cloud Domains commands."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import sys
|
|
|
|
from apitools.base.protorpclite import messages as _messages
|
|
|
|
from googlecloudsdk.api_lib.domains import registrations
|
|
from googlecloudsdk.command_lib.domains import flags
|
|
from googlecloudsdk.command_lib.domains import util
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core.console import console_io
|
|
from googlecloudsdk.core.resource import resource_printer
|
|
|
|
|
|
def ParseContactData(api_version, path):
|
|
"""Parses contact data from a yaml file."""
|
|
domains_messages = registrations.GetMessagesModule(api_version)
|
|
|
|
class ContactData(_messages.Message):
|
|
"""Message that should be present in YAML file with contacts data."""
|
|
|
|
# pylint: disable=invalid-name
|
|
allContacts = _messages.MessageField(domains_messages.Contact, 1)
|
|
registrantContact = _messages.MessageField(domains_messages.Contact, 2)
|
|
adminContact = _messages.MessageField(domains_messages.Contact, 3)
|
|
technicalContact = _messages.MessageField(domains_messages.Contact, 4)
|
|
|
|
contacts = util.ParseMessageFromYamlFile(
|
|
path, ContactData,
|
|
'Contact data file \'{}\' does not contain valid contact messages'.format(
|
|
path))
|
|
if not contacts:
|
|
return None
|
|
|
|
parsed_contact = None
|
|
if contacts.allContacts:
|
|
for field in ['registrantContact', 'adminContact', 'technicalContact']:
|
|
if contacts.get_assigned_value(field):
|
|
raise exceptions.Error(
|
|
('Contact data file \'{}\' cannot contain both '
|
|
'allContacts and {} fields.').format(path, field))
|
|
parsed_contact = domains_messages.ContactSettings(
|
|
registrantContact=contacts.allContacts,
|
|
adminContact=contacts.allContacts,
|
|
technicalContact=contacts.allContacts)
|
|
else:
|
|
parsed_contact = domains_messages.ContactSettings(
|
|
registrantContact=contacts.registrantContact,
|
|
adminContact=contacts.adminContact,
|
|
technicalContact=contacts.technicalContact)
|
|
|
|
return parsed_contact
|
|
|
|
|
|
def PromptForContacts(api_version, current_contacts=None):
|
|
"""Interactively prompts for Whois Contact information."""
|
|
domains_messages = registrations.GetMessagesModule(api_version)
|
|
|
|
create_call = (current_contacts is None)
|
|
if not console_io.PromptContinue(
|
|
'Contact data not provided using the --contact-data-from-file flag.',
|
|
prompt_string='Do you want to enter it interactively',
|
|
default=create_call):
|
|
return None
|
|
|
|
if create_call:
|
|
contact = _PromptForSingleContact(domains_messages)
|
|
return domains_messages.ContactSettings(
|
|
registrantContact=contact,
|
|
adminContact=contact,
|
|
technicalContact=contact)
|
|
|
|
choices = [
|
|
'all the contacts to the same value', 'registrant contact',
|
|
'admin contact', 'technical contact'
|
|
]
|
|
# TODO(b/166210862): Make it a loop.
|
|
index = console_io.PromptChoice(
|
|
options=choices,
|
|
cancel_option=True,
|
|
default=0,
|
|
message='Which contact do you want to change?')
|
|
|
|
if index == 0:
|
|
contact = _PromptForSingleContact(domains_messages,
|
|
current_contacts.registrantContact)
|
|
return domains_messages.ContactSettings(
|
|
registrantContact=contact,
|
|
adminContact=contact,
|
|
technicalContact=contact)
|
|
if index == 1:
|
|
contact = _PromptForSingleContact(domains_messages,
|
|
current_contacts.registrantContact)
|
|
return domains_messages.ContactSettings(registrantContact=contact)
|
|
if index == 2:
|
|
contact = _PromptForSingleContact(domains_messages,
|
|
current_contacts.adminContact)
|
|
return domains_messages.ContactSettings(adminContact=contact)
|
|
if index == 3:
|
|
contact = _PromptForSingleContact(domains_messages,
|
|
current_contacts.technicalContact)
|
|
return domains_messages.ContactSettings(technicalContact=contact)
|
|
return None
|
|
|
|
|
|
def _PromptForSingleContact(domains_messages, unused_current_contact=None):
|
|
"""Asks a user for a single contact data."""
|
|
contact = domains_messages.Contact()
|
|
contact.postalAddress = domains_messages.PostalAddress()
|
|
|
|
# TODO(b/166210862): Use defaults from current_contact.
|
|
# But then: How to clear a value?
|
|
# TODO(b/166210862): Better validation: Call validate_only after each prompt.
|
|
contact.postalAddress.recipients.append(
|
|
util.PromptWithValidator(
|
|
validator=util.ValidateNonEmpty,
|
|
error_message=' Name must not be empty.',
|
|
prompt_string='Full name: '))
|
|
contact.postalAddress.organization = console_io.PromptResponse(
|
|
'Organization (if applicable): ')
|
|
contact.email = util.PromptWithValidator(
|
|
validator=util.ValidateEmail,
|
|
error_message=' Invalid email address.',
|
|
prompt_string='Email',
|
|
default=properties.VALUES.core.account.Get())
|
|
contact.phoneNumber = util.PromptWithValidator(
|
|
validator=util.ValidateNonEmpty,
|
|
error_message=' Phone number must not be empty.',
|
|
prompt_string='Phone number: ',
|
|
message='Enter phone number with country code, e.g. "+1.8005550123".')
|
|
contact.faxNumber = util.Prompt(
|
|
prompt_string='Fax number (if applicable): ',
|
|
message='Enter fax number with country code, e.g. "+1.8005550123".')
|
|
contact.postalAddress.regionCode = util.PromptWithValidator(
|
|
validator=util.ValidateRegionCode,
|
|
error_message=(
|
|
' Country / Region code must be in ISO 3166-1 format, e.g. "US" or '
|
|
'"PL".\n See https://support.google.com/business/answer/6270107 for a'
|
|
' list of valid choices.'),
|
|
prompt_string='Country / Region code: ',
|
|
message='Enter two-letter Country / Region code, e.g. "US" or "PL".')
|
|
if contact.postalAddress.regionCode != 'US':
|
|
log.status.Print('Refer to the guidelines for entering address field '
|
|
'information at '
|
|
'https://support.google.com/business/answer/6397478.')
|
|
contact.postalAddress.postalCode = console_io.PromptResponse(
|
|
'Postal / ZIP code: ')
|
|
contact.postalAddress.administrativeArea = console_io.PromptResponse(
|
|
'State / Administrative area (if applicable): ')
|
|
contact.postalAddress.locality = console_io.PromptResponse(
|
|
'City / Locality: ')
|
|
contact.postalAddress.addressLines.append(
|
|
util.PromptWithValidator(
|
|
validator=util.ValidateNonEmpty,
|
|
error_message=' Address Line 1 must not be empty.',
|
|
prompt_string='Address Line 1: '))
|
|
|
|
optional_address_lines = []
|
|
address_line_num = 2
|
|
while len(optional_address_lines) < 4:
|
|
address_line_num = 2 + len(optional_address_lines)
|
|
address_line = console_io.PromptResponse(
|
|
'Address Line {} (if applicable): '.format(address_line_num))
|
|
if not address_line:
|
|
break
|
|
optional_address_lines += [address_line]
|
|
|
|
if optional_address_lines:
|
|
contact.postalAddress.addressLines.extend(optional_address_lines)
|
|
return contact
|
|
|
|
|
|
def ParseContactPrivacy(api_version, contact_privacy):
|
|
domains_messages = registrations.GetMessagesModule(api_version)
|
|
if contact_privacy is None:
|
|
return None
|
|
return flags.ContactPrivacyEnumMapper(domains_messages).GetEnumForChoice(
|
|
contact_privacy)
|
|
|
|
|
|
def PromptForContactPrivacy(api_version, choices, current_privacy=None):
|
|
"""Asks a user for Contacts Privacy.
|
|
|
|
Args:
|
|
api_version: Cloud Domains API version to call.
|
|
choices: List of privacy choices.
|
|
current_privacy: Current privacy. Should be nonempty in update calls.
|
|
|
|
Returns:
|
|
Privacy enum or None if the user cancelled.
|
|
"""
|
|
if not choices:
|
|
raise exceptions.Error('Could not find supported contact privacy.')
|
|
|
|
domains_messages = registrations.GetMessagesModule(api_version)
|
|
# Sort the choices according to the privacy strength.
|
|
choices.sort(key=flags.PrivacyChoiceStrength, reverse=True)
|
|
|
|
if current_privacy:
|
|
if len(choices) == 1:
|
|
log.status.Print(
|
|
'Your current contact privacy is {}. It cannot be changed.'.format(
|
|
current_privacy))
|
|
return None
|
|
else:
|
|
update = console_io.PromptContinue(
|
|
'Your current contact privacy is {}.'.format(current_privacy),
|
|
'Do you want to change it',
|
|
default=False)
|
|
if not update:
|
|
return None
|
|
|
|
current_choice = 0
|
|
for ix, privacy in enumerate(choices):
|
|
if privacy == flags.ContactPrivacyEnumMapper(
|
|
domains_messages).GetChoiceForEnum(current_privacy):
|
|
current_choice = ix
|
|
else:
|
|
current_choice = 0 # The strongest available privacy
|
|
if len(choices) == 1:
|
|
ack = console_io.PromptContinue(
|
|
'The only supported contact privacy is {}.'.format(choices[0]),
|
|
default=True)
|
|
if not ack:
|
|
return None
|
|
return ParseContactPrivacy(api_version, choices[0])
|
|
else:
|
|
index = console_io.PromptChoice(
|
|
options=choices,
|
|
default=current_choice,
|
|
message='Specify contact privacy')
|
|
return ParseContactPrivacy(api_version, choices[index])
|
|
|
|
|
|
def ParsePublicContactsAck(api_version, notices):
|
|
"""Parses Contact Notices. Returns public_contact_ack enum or None."""
|
|
domains_messages = registrations.GetMessagesModule(api_version)
|
|
|
|
if notices is None:
|
|
return False
|
|
for notice in notices:
|
|
enum = flags.ContactNoticeEnumMapper(domains_messages).GetEnumForChoice(
|
|
notice)
|
|
# pylint: disable=line-too-long
|
|
if enum == domains_messages.ConfigureContactSettingsRequest.ContactNoticesValueListEntryValuesEnum.PUBLIC_CONTACT_DATA_ACKNOWLEDGEMENT:
|
|
return enum
|
|
|
|
return None
|
|
|
|
|
|
def MergeContacts(api_version, prev_contacts, new_contacts):
|
|
domains_messages = registrations.GetMessagesModule(api_version)
|
|
if new_contacts is None:
|
|
new_contacts = domains_messages.ContactSettings()
|
|
|
|
return domains_messages.ContactSettings(
|
|
registrantContact=(new_contacts.registrantContact or
|
|
prev_contacts.registrantContact),
|
|
adminContact=(new_contacts.adminContact or prev_contacts.adminContact),
|
|
technicalContact=(new_contacts.technicalContact or
|
|
prev_contacts.technicalContact))
|
|
|
|
|
|
def _SimplifyContacts(contacts):
|
|
"""Returns one contact if all 3 contacts are equal, and all 3 contacts otherwise."""
|
|
if contacts.registrantContact == contacts.adminContact and contacts.registrantContact == contacts.technicalContact:
|
|
return contacts.registrantContact
|
|
return contacts
|
|
|
|
|
|
def PromptForPublicContactsAck(domain, contacts, print_format='default'):
|
|
"""Asks a user for Public Contacts Ack.
|
|
|
|
Args:
|
|
domain: Domain name.
|
|
contacts: Current Contacts. All 3 contacts should be present.
|
|
print_format: Print format, e.g. 'default' or 'yaml'.
|
|
|
|
Returns:
|
|
Boolean: whether the user accepted the notice or not.
|
|
"""
|
|
log.status.Print(
|
|
'You choose to make contact data of domain {} public.\n'
|
|
'Anyone who looks it up in the WHOIS directory will be able to see info\n'
|
|
'for the domain owner and administrative and technical contacts.\n'
|
|
'Make sure it\'s ok with them that their contact data is public.\n'
|
|
'This info will be publicly available:'.format(domain))
|
|
contacts = _SimplifyContacts(contacts)
|
|
resource_printer.Print(contacts, print_format, out=sys.stderr)
|
|
|
|
return console_io.PromptContinue(
|
|
message=None, default=False, throw_if_unattended=True, cancel_on_no=True)
|
|
# TODO(b/110398579): Integrate with ARI.
|
|
|
|
|
|
def PromptForPublicContactsUpdateAck(domain, contacts, print_format='default'):
|
|
"""Asks a user for Public Contacts Ack when the user updates contact settings.
|
|
|
|
Args:
|
|
domain: Domain name.
|
|
contacts: Current Contacts. All 3 contacts should be present.
|
|
print_format: Print format, e.g. 'default' or 'yaml'.
|
|
|
|
Returns:
|
|
Boolean: whether the user accepted the notice or not.
|
|
"""
|
|
log.status.Print(
|
|
'You choose to make contact data of domain {} public.\n'
|
|
'Anyone who looks it up in the WHOIS directory will be able to see info\n'
|
|
'for the domain owner and administrative and technical contacts.\n'
|
|
'Make sure it\'s ok with them that their contact data is public.\n'
|
|
'\n'
|
|
'Please consider carefully any changes to contact privacy settings when\n'
|
|
'changing from "redacted-contact-data" to "public-contact-data."\n'
|
|
'There may be a delay in reflecting updates you make to registrant\n'
|
|
'contact information such that any changes you make to contact privacy\n'
|
|
'(including from "redacted-contact-data" to "public-contact-data")\n'
|
|
'will be applied without delay but changes to registrant contact\n'
|
|
'information may take a limited time to be publicized. This means that\n'
|
|
'changes to contact privacy from "redacted-contact-data" to\n'
|
|
'"public-contact-data" may make the previous registrant contact\n'
|
|
'data public until the modified registrant contact details are '
|
|
'published.\n'
|
|
'\n'
|
|
'This info will be publicly available:'.format(domain))
|
|
contacts = _SimplifyContacts(contacts)
|
|
resource_printer.Print(contacts, print_format, out=sys.stderr)
|
|
|
|
return console_io.PromptContinue(
|
|
message=None, default=False, throw_if_unattended=True, cancel_on_no=True)
|
|
# TODO(b/110398579): Integrate with ARI.
|