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
-
On cPanel 110.0.97 / CentOS 7, back up an account whose only secondary domain is a parked domain —
userdata/mainhas a non-emptyparked_domainslist and an emptyaddon_domainsmap:--- addon_domains: {} main_domain: qeyesoftware.com.au parked_domains: - qeyesoftware.com sub_domains: [] -
On ApisCP 3.2.0 run:
ImportDomain --format=cpanel --no-activate /migrations/qeyesoft.tar.gz -
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:
[] + [0 => 'qeyesoftware.com']→[0 => 'qeyesoftware.com']array_keys(...)→[0]— the integer index, not the domainarray_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=NAMEis 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.