From 2da59f1055eaefea6e7473253835599ac02ba343 Mon Sep 17 00:00:00 2001 From: Michael Kaufmann Date: Sun, 2 Dec 2018 20:06:53 +0100 Subject: [PATCH] do not rely on fixed order of import-file for domains-import; first line now has to be the names of the api-parameters used for Domains.add, see https://api.froxlor.org/doc/0.10.0/#v-Domains, fixes #544 Signed-off-by: Michael Kaufmann --- admin_domains.php | 9 +- lib/classes/bulk/abstract.BulkAction.php | 254 ++++++++ lib/classes/bulk/class.DomainBulkAction.php | 624 ++------------------ 3 files changed, 324 insertions(+), 563 deletions(-) create mode 100644 lib/classes/bulk/abstract.BulkAction.php diff --git a/admin_domains.php b/admin_domains.php index 6239035c..cfb56908 100644 --- a/admin_domains.php +++ b/admin_domains.php @@ -658,15 +658,16 @@ if ($page == 'domains' || $page == 'overview') { standard_error('domain_import_error', $e->getMessage()); } - // @FIXME find a way to display $result['notice'] here somehow, - // as it might be important if you've reached your maximum allocation of domains - + if (!empty($bulk->getErrors())) { + dynamic_error(implode("
", $bulk->getErrors())); + } + // update customer/admin counters updateCounters(false); inserttask('1'); inserttask('4'); - $result_str = $result['imported'] . ' / ' . $result['all']; + $result_str = $result['imported'] . ' / ' . $result['all'] . (!empty($result['note']) ? ' ('.$result['note'].')' : ''); standard_success('domain_import_successfully', $result_str, array( 'filename' => $filename, 'action' => '', diff --git a/lib/classes/bulk/abstract.BulkAction.php b/lib/classes/bulk/abstract.BulkAction.php new file mode 100644 index 00000000..336c6758 --- /dev/null +++ b/lib/classes/bulk/abstract.BulkAction.php @@ -0,0 +1,254 @@ + + * @author Froxlor team (2010-) + * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt + * @package Cron + * + * @since 0.10.0 + * + */ + +/** + * Abstract Class BulkAction to mass-import entities + * + * @author Michael Kaufmann (d00p) + * + */ +abstract class BulkAction +{ + + /** + * complete path including filename of file to be imported + * + * @var string + */ + private $_impFile = null; + + /** + * customer id of the user the entity is being added to + * + * @var int + */ + private $_custId = null; + + /** + * array of customer data read from the database + * + * @var array + */ + private $_custData = null; + + /** + * api-function to call for addingg entity + * + * @var string + */ + private $api_call = null; + + /** + * api-function parameter names, read from import-file (first line) + * + * @var array + */ + private $api_params = null; + + /** + * errors while importing + * + * @var array + */ + private $errors = array(); + + /** + * class constructor, optionally sets file and customer-id + * + * @param string $import_file + * @param int $customer_id + * + * @return object BulkAction instance + */ + protected function __construct($import_file = null, $customer_id = 0) + { + if (! empty($import_file)) { + $this->_impFile = makeCorrectFile($import_file); + } + $this->_custId = $customer_id; + } + + /** + * import the parsed import file data with an optional separator other then semicolon + * and offset (maybe for header-line in csv or similar) + * + * @param string $separator + * @param int $offset + * + * @return array 'all' => amount of records processed, 'imported' => number of imported records + */ + abstract public function doImport($separator = ";", $offset = 0); + + /** + * setter for import-file + * + * @param string $import_file + * + * @return void + */ + public function setImportFile($import_file = null) + { + $this->_impFile = makeCorrectFile($import_file); + } + + /** + * setter for customer-id + * + * @param int $customer_id + * + * @return void + */ + public function setCustomer($customer_id = 0) + { + $this->_custId = $customer_id; + } + + /** + * return the list of errors + * + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + /** + * setter for api_call + * + * @param string $api_call + * + * @return void + */ + protected function setApiCall($api_call = "") + { + $this->api_call = $api_call; + } + + protected function importEntity($data_array = null) + { + global $userinfo; + + $module = substr($this->api_call, 0, strpos($this->api_call, ".")); + $function = substr($this->api_call, strpos($this->api_call, ".") + 1); + + $new_data = array(); + foreach ($this->api_params as $idx => $param) { + if (isset($data_array[$idx]) && !empty($data_array[$idx])) + { + $new_data[$param] = $data_array[$idx]; + } + } + + $result = null; + try { + $json_result = $module::getLocal($userinfo, $new_data)->$function(); + $result = json_decode($json_result, true)['data']; + } catch (Exception $e) { + $this->errors[] = $e->getMessage(); + } + return !empty($result); + } + + /** + * reads in the csv import file and returns an array with + * all the entites to be imported + * + * @param string $separator + * + * @return array + */ + protected function _parseImportFile($separator = ";") + { + if (empty($this->_impFile)) { + throw new Exception("No file was given for import"); + } + + if (! file_exists($this->_impFile)) { + throw new Exception("The file '" . $this->_impFile . "' could not be found"); + } + + if (! is_readable($this->_impFile)) { + throw new Exception("Unable to read file '" . $this->_impFile . "'"); + } + + $file_data = array(); + $is_params_line = true; + $fh = @fopen($this->_impFile, "r"); + if ($fh) { + while (($line = fgets($fh)) !== false) { + $tmp_arr = explode($separator, $line); + $data_arr = array(); + foreach ($tmp_arr as $idx => $data) { + if ($is_params_line) { + $this->api_params[$idx] = $data; + } else { + $data_arr[$idx] = $data; + } + } + if ($is_params_line) { + $is_params_line = false; + continue; + } + $file_data[] = array_map("trim", $data_arr); + } + $this->api_params = array_map("trim", $this->api_params); + } else { + throw new Exception("Unable to open file '" . $this->_impFile . "'"); + } + fclose($fh); + + return $file_data; + } + + /** + * to be called first in doImport() to read in customer and entity data + */ + protected function preImport() + { + $this->_readCustomerData(); + + if ($this->_custId <= 0) { + throw new Exception("Invalid customer selected"); + } + + if (is_null($this->_custData)) { + throw new Exception("Failed to read customer data"); + } + } + + /** + * reads customer data from panel_customer by $_custId + * + * @return bool + */ + protected function _readCustomerData() + { + $cust_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `customerid` = :cid"); + $this->_custData = Database::pexecute_first($cust_stmt, array( + 'cid' => $this->_custId + )); + if (is_array($this->_custData) && isset($this->_custData['customerid']) && $this->_custData['customerid'] == $this->_custId) { + return true; + } + $this->_custData = null; + return false; + } +} diff --git a/lib/classes/bulk/class.DomainBulkAction.php b/lib/classes/bulk/class.DomainBulkAction.php index 1331cec5..47a9f7c1 100644 --- a/lib/classes/bulk/class.DomainBulkAction.php +++ b/lib/classes/bulk/class.DomainBulkAction.php @@ -24,580 +24,86 @@ * @author Michael Kaufmann (d00p) * */ -class DomainBulkAction +class DomainBulkAction extends BulkAction { - /** - * complete path including filename of file to be imported - * - * @var string - */ - private $_impFile = null; + /** + * + * @return object DomainBulkAction instance + */ + public function __construct($import_file = null, $customer_id = 0) + { + parent::__construct($import_file, $customer_id); + $this->setApiCall('Domains.add'); + } - /** - * customer id of the user the domains are being added to - * - * @var int - */ - private $_custId = null; + /** + * import the parsed import file data with an optional separator other then semicolon + * and offset (maybe for header-line in csv or similar) + * + * @param string $separator + * @param int $offset + * + * @return array 'all' => amount of records processed, 'imported' => number of imported records + */ + public function doImport($separator = ";", $offset = 0) + { + $this->preImport(); - /** - * array of customer data read from the database - * - * @var array - */ - private $_custData = null; + // get the admins userinfo to check for domains_used, etc. + global $userinfo; - /** - * array of already known domains from the database - * - * @var array - */ - private $_knownDomains = null; - - /** - * array of known ip/port combinations - * - * @var array - */ - private $_knownIpPort = null; - - /** - * array of known IP's to check against - * - * @var array - */ - private $_knownIpPortChk = null; - - /** - * array of fields to import to panel_domains - * - * @var array - */ - private $_required_fields = array ( -/* 1 */ 'domain', -/* 2 */ 'documentroot', -/* 3 */ 'aliasdomain', -/* 4 */ 'isbinddomain', -/* 5 */ 'isemaildomain', -/* 6 */ 'email_only', -/* 7 */ 'iswildcarddomain', -/* 8 */ 'subcanemaildomain', -/* 9 */ 'caneditdomain', -/* 10 */ 'zonefile', -/* 11 */ 'wwwserveralias', -/* 12 */ 'openbasedir', -/* 13 */ 'speciallogfile', -/* 14 */ 'specialsettings', -/* 15 */ 'ssl_redirect', -/* 16 */ 'use_ssl', -/* 17 */ 'registration_date', -/* 18 */ 'ips', -/* 19 */ 'letsencrypt', -/* 20 */ 'hsts', -/* 21 */ 'hsts_sub', -/* 22 */ 'hsts_preload', -/* 23 */ 'ocsp_stapling', -/* 24 */ 'phpenabled', -/* 25 */ 'http2', - /* automatically added */ - 'adminid', - 'customerid', - 'add_date' - ); - - /** - * prepared statements for each domain - * - * @var PDOStatement - */ - private $_ins_stmt = null; - - private $_ipp_ins_stmt = null; - - /** - * class constructor, optionally sets file and customer-id - * - * @param string $import_file - * @param int $customer_id - * - * @return object DomainBulkAction instance - */ - public function __construct($import_file = null, $customer_id = 0) - { - if (! empty($import_file)) { - $this->_impFile = makeCorrectFile($import_file); - } - $this->_custId = $customer_id; - } - - /** - * import the parsed import file data with an optional separator other then semicolon - * and offset (maybe for header-line in csv or similar) - * - * @param string $separator - * @param int $offset - * - * @return array 'all' => amount of records processed, 'imported' => number of imported records - */ - public function doImport($separator = ";", $offset = 0) - { - - // get the admins userinfo to check for domains_used, etc. - global $userinfo; - - if ($userinfo['domains'] == "-1") { - $dom_unlimited = true; - } else { - $dom_unlimited = false; - } - - $domains_used = (int) $userinfo['domains_used']; - $domains_avail = (int) $userinfo['domains']; - - if (empty($separator) || strlen($separator) != 1) { - throw new Exception("Invalid separator specified: '" . $separator . "'"); - } - - if (! is_int($offset) || $offset < 0) { - throw new Exception("Invalid offset specified"); - } - - if ($this->_custId <= 0) { - throw new Exception("Invalid customer selected"); - } - - $this->_readCustomerData(); - - if (is_null($this->_custData)) { - throw new Exception("Failed to read customer data"); - } - - $this->_readIpPortData(); - $this->_readDomainData(); - - try { - $domain_array = $this->_parseImportFile($separator); - } catch (Exception $e) { - throw $e; - } - - if (count($domain_array) <= 0) { - throw new Exception("No domains were read from the file."); - } - - // preapre insert statement as it is used a few times - // leave out aliasdomain for now, cause empty = NULL value which cannot be - // added this easily using prepared statements - $this->_ins_stmt = Database::prepare(" - INSERT INTO `" . TABLE_PANEL_DOMAINS . "` SET - `domain` = :domain, - `adminid` = :adminid, - `customerid` = :customerid, - `documentroot` = :documentroot, - `isbinddomain` = :isbinddomain, - `isemaildomain` = :isemaildomain, - `email_only` = :email_only, - `iswildcarddomain` = :iswildcarddomain, - `subcanemaildomain` = :subcanemaildomain, - `caneditdomain` = :caneditdomain, - `zonefile` = :zonefile, - `wwwserveralias` = :wwwserveralias, - `openbasedir` = :openbasedir, - `speciallogfile` = :speciallogfile, - `specialsettings` = :specialsettings, - `ssl_redirect` = :ssl_redirect, - `registration_date` = :registration_date, - `add_date` = :add_date, - `letsencrypt` = :letsencrypt, - `hsts` = :hsts, - `hsts_sub` = :hsts_sub, - `hsts_preload` = :hsts_preload, - `ocsp_stapling` = :ocsp_stapling, - `phpenabled` = :phpenabled, - `http2` = :http2 - "); - - // prepare insert statement for ip/port <> domain - $this->_ipp_ins_stmt = Database::prepare(" - INSERT INTO `" . TABLE_DOMAINTOIP . "` SET - `id_domain` = :domid, - `id_ipandports` = :ipid - "); - - $global_counter = 0; - $import_counter = 0; - $note = ''; - foreach ($domain_array as $idx => $dom) { - if ($idx >= $offset) { - if ($dom_unlimited || (! $dom_unlimited && $domains_used < $domains_avail)) { - $ins_id = $this->_addSingleDomainToDatabase($dom); - if ($ins_id !== false) { - $import_counter ++; - $domains_used ++; - } - } else { - $note = 'You have reached your maximum allocation of domains (' . $domains_avail . ').'; - break; - } - } - $global_counter ++; - } - - return array( - 'all' => $global_counter, - 'imported' => $import_counter, - 'notice' => $note - ); - } - - /** - * setter for import-file - * - * @param string $import_file - * - * @return void - */ - public function setImportFile($import_file = null) - { - $this->_impFile = makeCorrectFile($import_file); - } - - /** - * setter for customer-id - * - * @param int $customer_id - * - * @return void - */ - public function setCustomer($customer_id = 0) - { - $this->_custId = $customer_id; - } - - /** - * adds a single domain to the database using the given array - * - * @param array $domain_data - * - * @return int last-inserted id or false on error - */ - private function _addSingleDomainToDatabase($domain_data = array()) - { - - // format domain - $idna_convert = new idna_convert_wrapper(); - $domain_data['domain'] = $idna_convert->encode(preg_replace(array( - '/\:(\d)+$/', - '/^https?\:\/\//' - ), '', $domain_data['domain'])); - - // check if it is a valid domain - if (! validateDomain($domain_data['domain'])) { - return false; - } - - // no system-hostname can be added - if ($domain_data['domain'] == Settings::Get('system.hostname')) { - return false; - } - - // no existing domains can be imported - if (in_array($domain_data['domain'], $this->_knownDomains)) { - return false; - } - - // check for alias-domain - $hasAlias = false; - if (! empty($domain_data['aliasdomain'])) { - // format - $domain_data['aliasdomain'] = $idna_convert->encode(preg_replace(array( - '/\:(\d)+$/', - '/^https?\:\/\//' - ), '', $domain_data['aliasdomain'])); - // validate alias-domain - if (! validateDomain($domain_data['aliasdomain'])) { - // invalid-domain lol - skip to be sure we don't add anything weird - return false; - } - // does the domain we want to be an alias of exists? - if (! in_array($domain_data['aliasdomain'], $this->_knownDomains)) { - // it does not - User should respect the order of import so if the domain - // he wants to alias is also part of the import is ABOVE this one - // - we'd better skip - return false; - } - $hasAlias = $domain_data['aliasdomain']; - } - - // check for use_ssl and ssl_redirect - if (!isset($domain_data['use_ssl']) || $domain_data['use_ssl'] == 1) { - // if not set: default is whatever the system says - // if set to 1: set to 0 if system has no ssl enabled - $domain_data['use_ssl'] = (Settings::get('system.use_ssl') == 1 ? 1 : 0); - } - - // use_ssl flag - if ($domain_data['use_ssl'] != 1) { - $domain_data['use_ssl'] = 0; - } - - // ssl_redirect flag - if ($domain_data['ssl_redirect'] != 1) { - $domain_data['ssl_redirect'] = 0; - } - - // if use_ssl is 0 ssl_redirect must be too (no ssl-ip => no ssl-redirect) - if ($domain_data['use_ssl'] == 0 && $domain_data['ssl_redirect'] == 1) { - $domain_data['ssl_redirect'] = 0; - } - - // only check for letsencrypt, hsts and oscp-stapling if ssl is enabled - if ($domain_data['use_ssl'] == 1) { - //lets encrypt - if ($domain_data['letsencrypt'] != 1 || $domain_data['iswildcarddomain'] == 1) { - $domain_data['letsencrypt'] = 0; - } + if ($userinfo['domains'] == "-1") { + $dom_unlimited = true; } else { - $domain_data['letsencrypt'] = 0; + $dom_unlimited = false; } - // hsts - if ($domain_data['hsts'] != 1) { - $domain_data['hsts'] = 0; - } - if ($domain_data['hsts_sub'] != 1) { - $domain_data['hsts_sub'] = 0; - } - if ($domain_data['hsts_preload'] != 1) { - $domain_data['hsts_preload'] = 0; - } - if ($domain_data['ocsp_stapling'] != 1) { - $domain_data['ocsp_stapling'] = 0; + $domains_used = (int) $userinfo['domains_used']; + $domains_avail = (int) $userinfo['domains']; + + if (empty($separator) || strlen($separator) != 1) { + throw new Exception("Invalid separator specified: '" . $separator . "'"); } - if ($domain_data['phpenabled'] != 1) { - $domain_data['phpenabled'] = 0; + if (! is_int($offset) || $offset < 0) { + throw new Exception("Invalid offset specified"); } - if ($domain_data['http2'] != 1) { - $domain_data['http2'] = 0; + try { + $domain_array = $this->_parseImportFile($separator); + } catch (Exception $e) { + throw $e; } - // add to known domains - $this->_knownDomains[] = $domain_data['domain']; - - // docroot (URL allowed, will lead to redirect) - if (! preg_match('/^https?\:\/\//', $domain_data['documentroot'])) { - $domain_data['documentroot'] = makeCorrectDir($this->_custData['documentroot'] . "/" . $domain_data['documentroot']); - } - - // is bind domain? - if (! isset($domain_data['isbinddomain'])) { - $domain_data['isbinddomain'] = (Settings::Get('system.bind_enable') == '1') ? 1 : 0; - } elseif ($domain_data['isbinddomain'] != 1) { - $domain_data['isbinddomain'] = 0; - } - - // zonefile - if (!isset($domain_data['zonefile'])) { - $domain_data['zonefile'] = ""; - } else { - if (!empty($domain_data['zonefile']) && Settings::Get('system.bind_enable') == '1') { - $domain_data['zonefile'] = makeCorrectFile($domain_data['zonefile']); - } else { - $domain_data['zonefile'] = ""; - } - } + if (count($domain_array) <= 0) { + throw new Exception("No domains were read from the file."); + } - // openbasedir flag - if (! isset($domain_data['openbasedir'])) { - $domain_data['openbasedir'] = 1; - } elseif ($domain_data['openbasedir'] != 1) { - $domain_data['openbasedir'] = 0; - } + $global_counter = 0; + $import_counter = 0; + $note = ''; + foreach ($domain_array as $idx => $dom) { + if ($idx >= $offset) { + if ($dom_unlimited || (! $dom_unlimited && $domains_used < $domains_avail)) { - // speciallogfile flag - if (! isset($domain_data['speciallogfile'])) { - $domain_data['speciallogfile'] = 0; - } elseif ($domain_data['speciallogfile'] != 1) { - $domain_data['speciallogfile'] = 0; - } + $result = $this->importEntity($dom); + if ($result) { + $import_counter ++; + $domains_used ++; + } + } else { + $note .= 'You have reached your maximum allocation of domains (' . $domains_avail . ').'; + break; + } + } + $global_counter ++; + } - /* - * automatically set values (not from the file) - */ - // add date - $domain_data['add_date'] = time(); - // set adminid - $domain_data['adminid'] = (int)$this->_custData['adminid']; - // set customerid - $domain_data['customerid'] = (int)$this->_custId; - - // check for required fields - foreach ($this->_required_fields as $rfld) { - if (! isset($domain_data[$rfld])) { - return false; - } - } - - // clean all fields that do not belong to the required fields - $domain_data_tmp = $domain_data; - foreach ($domain_data_tmp as $fld => $val) { - if (! in_array($fld, $this->_required_fields)) { - unset($domain_data[$fld]); - } - } - - // save iplist - $iplist = $domain_data['ips']; - $iplist_arr = array_unique(explode(",", $iplist)); - $knownIPsCheck = array_unique($this->_knownIpPortChk); - // check whether we actually have at least one of the used IP's in our system - $result_iplist = array_intersect($iplist_arr, $knownIPsCheck); - // write back iplist - $iplist = implode(",", $result_iplist); - - // don't need that for the domain-insert-statement - unset($domain_data['ips']); - - // remember use_ssl value - $use_ssl = (bool)$domain_data['use_ssl']; - // don't need that for the domain-insert-statement - unset($domain_data['use_ssl']); - // don't need alias - unset($domain_data['aliasdomain']); - - // finally ADD the domain to panel_domains - Database::pexecute($this->_ins_stmt, $domain_data); - - // get the newly inserted domain-id - $domain_id = Database::lastInsertId(); - - // add alias if any - if ($hasAlias != false) { - $alias_stmt = Database::prepare("UPDATE `".TABLE_PANEL_DOMAINS."` SET `aliasdomain` = :aliasdomain WHERE `id` = :did"); - Database::pexecute($alias_stmt, array('aliasdomain' => $hasAlias, 'did' => $domain_id)); - } - - // insert domain <-> ip/port reference - if (empty($iplist)) { - $iplist = Settings::Get('system.ipaddress'); - } - - // split ip-list and remove duplicates - $iplist_arr = array_unique(explode(",", $iplist)); - foreach ($iplist_arr as $ip) { - // if we know the ip, at all variants (different ports, ssl and non-ssl) of it! - if (isset($this->_knownIpPort[$ip])) { - foreach ($this->_knownIpPort[$ip] as $ipdata) { - // no ssl ip/ports should be used for this domain - if ($use_ssl == false && $ipdata['ssl'] == 1) { - continue; - } - // add domain->ip reference - Database::pexecute($this->_ipp_ins_stmt, array( - 'domid' => $domain_id, - 'ipid' => $ipdata['id'] - )); - } - } - } - - return $domain_id; - } - - /** - * reads in the csv import file and returns an array with - * all the domains to be imported - * - * @param string $separator - * - * @return array - */ - private function _parseImportFile($separator = ";") - { - if (empty($this->_impFile)) { - throw new Exception("No file was given for import"); - } - - if (! file_exists($this->_impFile)) { - throw new Exception("The file '" . $this->_impFile . "' could not be found"); - } - - if (! is_readable($this->_impFile)) { - throw new Exception("Unable to read file '" . $this->_impFile . "'"); - } - - $file_data = array(); - - $fh = @fopen($this->_impFile, "r"); - if ($fh) { - while (($line = fgets($fh)) !== false) { - $tmp_arr = explode($separator, $line); - $data_arr = array(); - foreach ($tmp_arr as $idx => $data) { - // don't include more fields than the ones we use - if ($idx > (count($this->_required_fields) - 4)) // off-by-one + 3 auto-values - break; - $data_arr[$this->_required_fields[$idx]] = $data; - } - $file_data[] = array_map("trim", $data_arr); - } - } else { - throw new Exception("Unable to open file '" . $this->_impFile . "'"); - } - fclose($fh); - - return $file_data; - } - - /** - * reads customer data from panel_customer by $_custId - * - * @return bool - */ - private function _readCustomerData() - { - $cust_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `customerid` = :cid"); - $this->_custData = Database::pexecute_first($cust_stmt, array( - 'cid' => $this->_custId - )); - if (is_array($this->_custData) && isset($this->_custData['customerid']) && $this->_custData['customerid'] == $this->_custId) { - return true; - } - $this->_custData = null; - return false; - } - - /** - * reads domain data from panel_domain - * - * @return void - */ - private function _readDomainData() - { - $knowndom_stmt = Database::prepare("SELECT `domain` FROM `" . TABLE_PANEL_DOMAINS . "` ORDER BY `domain` ASC"); - Database::pexecute($knowndom_stmt); - $this->_knownDomains = array(); - while ($dom = $knowndom_stmt->fetch()) { - $this->_knownDomains[] = $dom['domain']; - } - } - - /** - * reads ip/port data from panel_ipsandports - * - * @return void - */ - private function _readIpPortData() - { - $knownip_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "`"); - Database::pexecute($knownip_stmt); - $this->_knownIpPort = array(); - while ($ipp = $knownip_stmt->fetch()) { - $this->_knownIpPort[$ipp['ip']][] = $ipp; - $this->_knownIpPortChk[] = $ipp['ip']; - } - } + return array( + 'all' => $global_counter, + 'imported' => $import_counter, + 'notice' => $note + ); + } }