cPanel import fails on parked-domain-only accounts (aliases TypeError); `--plan` override also aborts

Environment

ApisCP:

3.2.0
revision: 097c6710d91a8ad731adf8622b31d6b06a8e6a21

Source panel: cPanel 110.0.97 on CentOS 7.

Import command:

ImportDomain --format=cpanel --no-activate /migrations/qeyesoft.tar.gz

Steps to reproduce

  1. On cPanel 110.0.97 / CentOS 7, back up an account whose only secondary domain is a parked domain — userdata/main has a non-empty parked_domains list and an empty addon_domains map:

    ---
    addon_domains: {}
    main_domain: qeyesoftware.com.au
    parked_domains:
      - qeyesoftware.com
    sub_domains: []
    
  2. On ApisCP 3.2.0 run:

    ImportDomain --format=cpanel --no-activate /migrations/qeyesoft.tar.gz
    
  3. Separately, supply a plan override:

    ImportDomain --format=cpanel --plan=business_plus_hosting /migrations/qeyesoft.tar.gz
    

Actual result

The import fails during the post-import aliases_synchronize_changes() pass:

INFO   : AddDomain command: /usr/local/apnscp/bin/AddDomain ... -c 'aliases,aliases.10000'='qeyesoftware.com' ...
INFO   : Following missing domains have been added to account: 0
ERROR  : DataStream::pipeline(): Util_Process::formatDataCallProc(): EditDomain: ERROR  :
         Event\Manager::fire(): Opcenter\Service\Validators\Common\GenericMap::removeMap():
         Argument #1 ($key) must be of type string, int given, called in
         /usr/local/apnscp/lib/Opcenter/Service/Validators/Aliases/Aliases.php on line 149
ERROR  : DataStream::pipeline(): Aliases_Module::_synchronize_changes(): failed to activate domain changes
ERROR  : unknown(): Imported /migrations/qeyesoft.tar.gz,  Failed (severity: ERROR, ...)

Note Following missing domains have been added to account: 0 — the literal integer 0 is being treated as a domain name.

And with a plan override, the import aborts before starting:

(…Exception) INTERNAL REPORT: \TypeError::__set_state(array(
   'message' => 'Opcenter\Account\Import::setOption(): Return value must be of type Opcenter\Account\Import, none returned',
   'file' => '/usr/local/apnscp/lib/Opcenter/Account/Import.php',
   'line' => 118,
))

Root cause

There are two distinct defects.

1. parked_domains is a list, but array_keys() treats it as a map.

lib/Opcenter/Migration/Formats/Cpanel/Pathmap/Userdata.php handles parked_domains the same as addon_domains, but cPanel’s userdata/main gives them different shapes — addon_domains is domain => path, parked_domains is a plain list of domain names:

// Userdata.php:52
$aliases = array_keys($map['addon_domains']) + append_config($map['parked_domains']);

// Userdata.php:64
$missing = array_diff(
    array_keys($map['addon_domains'] + $map['parked_domains']),
    $ctx->getServiceValue(null, 'aliases')
);

For a parked-only account this evaluates to:

  1. [] + [0 => 'qeyesoftware.com'] → [0 => 'qeyesoftware.com']
  2. array_keys(...) → [0] — the integer index, not the domain
  3. array_diff([0], ['qeyesoftware.com']) → [0], so $missing === [0]

The integer is then appended into the alias list and journaled (Userdata.php:70), producing [0 => 'qeyesoftware.com', 1 => 0]. When aliases_synchronize_changes() later calls Aliases::reconfigure(), array_diff() loosely compares (string)0 against "qeyesoftware.com", keeps the 0 in $remove, and calls parent::removeMap(0) against removeMap(string $key) → TypeError.

2. Import::setOption() declares : self but returns nothing.

lib/Opcenter/Account/Import.php:112:

public function setOption(string $option, $val): self
{
    if (!\array_key_exists($option, $this->options)) {
        fatal("Unknown option `%s'", $option);
    }
    $this->options[$option] = $val;
    // no return, and no write to $this->runtimeConfiguration
}

bin/ImportDomain:48-50 calls this whenever --plan is supplied, triggering the TypeError. It also only writes $this->options, whereas exec() forwards $this->runtimeConfiguration to the migration, so the plan wouldn’t reach the importer even if the return were fixed.

Expected result

  • Parked domains import as valid aliases; no integer placeholder is injected into the alias list.
  • --plan=NAME is accepted and applied without a fatal error.

Proposed fix

lib/Opcenter/Migration/Formats/Cpanel/Pathmap/Userdata.php — take the values for parked domains (a list) and keys for addon domains (a map):

// Userdata.php:52
$aliases = array_merge(
    array_keys($map['addon_domains']),
    array_values($map['parked_domains'])
);
$this->bill->set('aliases', 'aliases', append_config($aliases));

// Userdata.php:63-66
$missing = array_diff(
    array_merge(
        array_keys($map['addon_domains']),
        array_values($map['parked_domains'])
    ),
    $ctx->getServiceValue(null, 'aliases') ?? []
);

// Userdata.php:70
$ctx->set('aliases', append_config(array_merge(
    (array)$ctx->getServiceValue(null, 'aliases'),
    array_values($missing)
)));

lib/Opcenter/Account/Import.php — return $this and persist the option into the runtime configuration:

public function setOption(string $option, $val): self
{
    if (!\array_key_exists($option, $this->options)) {
        fatal("Unknown option `%s'", $option);
    }
    $this->options[$option] = $val;
    $this->runtimeConfiguration[$option] = $val;

    return $this;
}

Optional hardening — lib/Opcenter/Service/Validators/Aliases/Aliases.php — normalize the domain list in reconfigure() so legacy/journaled configuration carrying non-string entries can’t raise an uncatchable TypeError:

public function reconfigure($old, $new, SiteConfiguration $svc): bool
{
    $normalize = static function ($v): array {
        return array_values(array_filter(array_map(
            static fn($d) => \is_string($d) ? $d : null,
            (array)$v
        ), static fn($d) => $d !== null && $d !== ''));
    };
    $old = $normalize($old);
    $new = $normalize($new);

    $add    = array_diff($new, $old);
    $remove = array_diff($old, $new);
    // ... rest unchanged
}

Verification

After applying the fixes, the same backup imported cleanly:

SUCCESS: Imported /migrations/qeyesoft.tar.gz,  Succeeded (severity: WARNING, duration: 00:00:36)

Account configured as expected:

# info/current/siteinfo
plan=business_plus_hosting

# info/current/aliases
aliases=['qeyesoftware.com']
max=None

No removeMap()/addMap() TypeError, and --plan=business_plus_hosting was accepted.


AI was used to help diagnose the problem, create the patch and generate this report.

Thanks for the bug report. Both concerns have been resolved in edge.

To update, run upcp.