// Copyright 2018 The Nomulus Authors. 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. package google.registry.tools; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static google.registry.flows.domain.DomainFlowUtils.newAutorenewBillingEvent; import static google.registry.flows.domain.DomainFlowUtils.newAutorenewPollMessage; import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime; import static google.registry.model.EppResourceUtils.loadByForeignKey; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.util.DateTimeUtils.isBeforeOrAt; import static google.registry.util.DateTimeUtils.leapSafeSubtractYears; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.googlecode.objectify.Key; import google.registry.model.billing.BillingEvent; import google.registry.model.domain.DomainResource; import google.registry.model.domain.Period; import google.registry.model.domain.Period.Unit; import google.registry.model.eppcommon.StatusValue; import google.registry.model.index.ForeignKeyIndex.ForeignKeyDomainIndex; import google.registry.model.poll.PollMessage; import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry.Type; import google.registry.util.Clock; import google.registry.util.NonFinalForTesting; import java.util.List; import java.util.Optional; import javax.inject.Inject; import org.joda.time.DateTime; /** * Command to unrenew a domain. * *

This removes years off a domain's registration period. Note that the expiration time cannot be * set to prior than the present. Reversal of the charges for these years (if desired) must happen * out of band, as they may already have been billed out and thus cannot and won't be reversed in * Datastore. */ @Parameters(separators = " =", commandDescription = "Unrenew a domain.") @NonFinalForTesting class UnrenewDomainCommand extends ConfirmingCommand implements CommandWithRemoteApi { @Parameter( names = {"-p", "--period"}, description = "Number of years to unrenew the registration for (defaults to 1).") int period = 1; @Parameter(description = "Names of the domains to unrenew.", required = true) List mainParameters; @Inject Clock clock; private static final ImmutableSet DISALLOWED_STATUSES = ImmutableSet.of( StatusValue.PENDING_TRANSFER, StatusValue.SERVER_RENEW_PROHIBITED, StatusValue.SERVER_UPDATE_PROHIBITED); @Override protected void init() { checkArgument(period >= 1 && period <= 9, "Period must be in the range 1-9"); DateTime now = clock.nowUtc(); ImmutableSet.Builder domainsNonexistentBuilder = new ImmutableSet.Builder<>(); ImmutableSet.Builder domainsDeletingBuilder = new ImmutableSet.Builder<>(); ImmutableMultimap.Builder domainsWithDisallowedStatusesBuilder = new ImmutableMultimap.Builder<>(); ImmutableMap.Builder domainsExpiringTooSoonBuilder = new ImmutableMap.Builder<>(); for (String domainName : mainParameters) { if (ofy().load().type(ForeignKeyDomainIndex.class).id(domainName).now() == null) { domainsNonexistentBuilder.add(domainName); continue; } Optional domain = loadByForeignKey(DomainResource.class, domainName, now); if (!domain.isPresent() || domain.get().getStatusValues().contains(StatusValue.PENDING_DELETE)) { domainsDeletingBuilder.add(domainName); continue; } domainsWithDisallowedStatusesBuilder.putAll( domainName, Sets.intersection(domain.get().getStatusValues(), DISALLOWED_STATUSES)); if (isBeforeOrAt( leapSafeSubtractYears(domain.get().getRegistrationExpirationTime(), period), now)) { domainsExpiringTooSoonBuilder.put(domainName, domain.get().getRegistrationExpirationTime()); } } ImmutableSet domainsNonexistent = domainsNonexistentBuilder.build(); ImmutableSet domainsDeleting = domainsDeletingBuilder.build(); ImmutableMultimap domainsWithDisallowedStatuses = domainsWithDisallowedStatusesBuilder.build(); ImmutableMap domainsExpiringTooSoon = domainsExpiringTooSoonBuilder.build(); boolean foundInvalidDomains = !(domainsNonexistent.isEmpty() && domainsDeleting.isEmpty() && domainsWithDisallowedStatuses.isEmpty() && domainsExpiringTooSoon.isEmpty()); if (foundInvalidDomains) { System.err.print("Found domains that cannot be unrenewed for the following reasons:\n\n"); } if (!domainsNonexistent.isEmpty()) { System.err.printf("Domains that don't exist: %s\n\n", domainsNonexistent); } if (!domainsDeleting.isEmpty()) { System.err.printf("Domains that are deleted or pending delete: %s\n\n", domainsDeleting); } if (!domainsWithDisallowedStatuses.isEmpty()) { System.err.printf("Domains with disallowed statuses: %s\n\n", domainsWithDisallowedStatuses); } if (!domainsExpiringTooSoon.isEmpty()) { System.err.printf("Domains expiring too soon: %s\n\n", domainsExpiringTooSoon); } checkArgument(!foundInvalidDomains, "Aborting because some domains cannot be unrewed"); } @Override protected String prompt() { return String.format("Unrenew these domain(s) for %d years?", period); } @Override protected String execute() { for (String domainName : mainParameters) { ofy().transact(() -> unrenewDomain(domainName)); System.out.printf("Unrenewed %s\n", domainName); } return "Successfully unrenewed all domains."; } private void unrenewDomain(String domainName) { ofy().assertInTransaction(); DateTime now = ofy().getTransactionTime(); Optional domainOptional = loadByForeignKey(DomainResource.class, domainName, now); // Transactional sanity checks on the off chance that something changed between init() running // and here. checkState( domainOptional.isPresent() && !domainOptional.get().getStatusValues().contains(StatusValue.PENDING_DELETE), "Domain %s was deleted or is pending deletion", domainName); DomainResource domain = domainOptional.get(); checkState( Sets.intersection(domain.getStatusValues(), DISALLOWED_STATUSES).isEmpty(), "Domain %s has prohibited status values", domainName); checkState( leapSafeSubtractYears(domain.getRegistrationExpirationTime(), period).isAfter(now), "Domain %s expires too soon", domainName); DateTime newExpirationTime = leapSafeSubtractYears(domain.getRegistrationExpirationTime(), period); HistoryEntry historyEntry = new HistoryEntry.Builder() .setParent(domain) .setModificationTime(now) .setBySuperuser(true) .setType(Type.SYNTHETIC) .setClientId(domain.getCurrentSponsorClientId()) .setReason("Domain unrenewal") .setPeriod(Period.create(period, Unit.YEARS)) .setRequestedByRegistrar(false) .build(); PollMessage oneTimePollMessage = new PollMessage.OneTime.Builder() .setClientId(domain.getCurrentSponsorClientId()) .setMsg( String.format( "Domain %s was unrenewed by %d years; now expires at %s.", domainName, period, newExpirationTime)) .setParent(historyEntry) .setEventTime(now) .build(); // Create a new autorenew billing event and poll message starting at the new expiration time. BillingEvent.Recurring newAutorenewEvent = newAutorenewBillingEvent(domain) .setEventTime(newExpirationTime) .setParent(historyEntry) .build(); PollMessage.Autorenew newAutorenewPollMessage = newAutorenewPollMessage(domain) .setEventTime(newExpirationTime) .setParent(historyEntry) .build(); // End the old autorenew billing event and poll message now. updateAutorenewRecurrenceEndTime(domain, now); DomainResource newDomain = domain .asBuilder() .setRegistrationExpirationTime(newExpirationTime) .setLastEppUpdateTime(now) .setLastEppUpdateClientId(domain.getCurrentSponsorClientId()) .setAutorenewBillingEvent(Key.create(newAutorenewEvent)) .setAutorenewPollMessage(Key.create(newAutorenewPollMessage)) .build(); // In order to do it'll need to write out a new HistoryEntry (likely of type SYNTHETIC), a new // autorenew billing event and poll message, and a new one time poll message at the present time // informing the registrar of this out-of-band change. ofy() .save() .entities( newDomain, historyEntry, oneTimePollMessage, newAutorenewEvent, newAutorenewPollMessage); } }